Verificações de Assinante (Signer Checks)
Verificações de assinante são o equivalente digital de exigir uma assinatura manuscrita — elas provam que um titular de conta realmente autorizou uma transação em vez de outra pessoa agindo em seu nome. No ambiente trustless da Solana, essa prova criptográfica é a única forma de verificar uma autorização autêntica.
Isso se torna crítico ao lidar com Program Derived Accounts (PDAs) e operações protegidas por autoridade. A maioria das contas de programa armazena um campo authority que determina quem pode modificá-las, e muitos PDAs são derivados de contas de usuários específicas. Sem verificação de assinante, seu programa não tem como distinguir entre proprietários legítimos e impostores maliciosos.
As consequências de verificações de assinante ausentes são devastadoras: qualquer conta pode realizar operações que deveriam ser restritas a autoridades específicas, levando a acesso não autorizado, contas drenadas e perda completa de controle sobre o estado do programa.
Anchor
Considere esta instrução vulnerável que transfere a propriedade de uma conta de programa:
#[program]
pub mod insecure_update{
use super::*;
//..
pub fn update_ownership(ctx: Context<UpdateOwnership>) -> Result<()> {
ctx.accounts.program_account.owner = ctx.accounts.new_owner.key();
Ok(())
}
//..
}
#[derive(Accounts)]
pub struct UpdateOwnership<'info> {
/// CHECK: Esta conta não será verificada pelo Anchor
pub owner: UncheckedAccount<'info>,
/// CHECK: Esta conta não será verificada pelo Anchor
pub new_owner: UncheckedAccount<'info>,
#[account(
mut,
has_one = owner
)]
pub program_account: Account<'info, ProgramAccount>,
}
#[account]
pub struct ProgramAccount {
owner: Pubkey,
}À primeira vista, isso parece seguro. A restrição has_one = owner garante que a conta owner passada à instrução corresponde ao campo owner armazenado no program_account. A validação de dados é perfeita, mas há uma falha fatal.
Note que owner é uma UncheckedAccount, não um Signer. Isso significa que, embora o Anchor verifique se a conta fornecida corresponde ao proprietário armazenado, ele nunca verifica se essa conta realmente assinou a transação.
Um invasor pode explorar isso:
Encontrando qualquer conta de programa que deseja sequestrar
Lendo a chave pública do proprietário atual dos dados da conta
Criando uma transação que passa a chave pública do proprietário real como parâmetro owner
Configurando a si mesmo como
new_ownerEnviando a transação sem a assinatura do proprietário real
A restrição has_one passa porque as chaves públicas correspondem, mas como não há verificação de assinante, o invasor transfere a propriedade para si mesmo sem o consentimento do proprietário legítimo. Uma vez no controle da conta, ele pode realizar qualquer operação como a nova autoridade.
Felizmente, o Anchor torna super fácil realizar essa verificação diretamente na struct de conta, basta mudar UncheckedAccount para Signer assim:
#[derive(Accounts)]
pub struct UpdateOwnership<'info> {
pub owner: Signer<'info>,
/// CHECK: Esta conta não será verificada pelo Anchor
pub new_owner: UncheckedAccount<'info>,
#[account(
mut,
has_one = owner
)]
pub program_account: Account<'info, ProgramAccount>,
}Ou você pode adicionar a restrição de conta signer assim:
#[derive(Accounts)]
pub struct UpdateOwnership<'info> {
#[account(signer)]
/// CHECK: Esta conta não será verificada pelo Anchor
pub owner: UncheckedAccount<'info>,
/// CHECK: Esta conta não será verificada pelo Anchor
pub new_owner: UncheckedAccount<'info>,
#[account(
mut,
has_one = owner
)]
pub program_account: Account<'info, ProgramAccount>,
}Ou você pode simplesmente adicionar uma verificação de assinante na instrução usando a verificação ctx.accounts.owner.is_signer assim:
pub fn update_ownership(ctx: Context<UpdateOwnership>) -> Result<()> {
if !ctx.accounts.owner.is_signer {
return Err(ProgramError::MissingRequiredSignature.into());
}
ctx.accounts.program_account.owner = ctx.accounts.new_owner.key();
Ok(())
}Ao adicionar essa verificação, o handler da instrução só prosseguirá se a conta de autoridade tiver assinado a transação. Se a conta não estiver assinada, a transação falhará.
Pinocchio
No Pinocchio, como não temos a possibilidade de adicionar verificações de segurança diretamente dentro da struct de conta, somos obrigados a fazê-lo na lógica da instrução.
Podemos fazer isso de forma muito semelhante ao Anchor usando a função is_signer() assim:
if !self.accounts.owner.is_signer() {
return Err(ProgramError::MissingRequiredSignature.into());
}