← CCNP Security: núcleo SCOR e operação
20 / 25 · 55 MIN

Email: assinatura, alinhamento e resposta

Verifica assinaturas locais e distingue autenticação de domínio, legitimidade de uma instrução e remediação efetiva.

1. Identificar o que foi autenticado

Uma mensagem fictícia pede alteração urgente de dados de pagamento. O nome visível diz “Operações”, mas é necessário distinguir esse texto do domínio From, do MAIL FROM e do domínio d= da assinatura DKIM. SPF avalia autorização da origem para a identidade relevante do envelope; DKIM liga uma assinatura a um domínio e a partes assinadas da mensagem. DMARC relaciona um resultado autenticado com o domínio do autor. Não autentica a intenção comercial do pedido. O RFC 9989, publicado em maio de 2026, substituiu os RFC 7489 e 9091; a adoção concreta por um produto deve ser verificada. O curso usa a referência atual sem pressupor que todo o gateway já a implementa.

2. Executar a assinatura e observar alterações

O laboratório Node.js gera duas chaves RSA de 2048 bits em memória e usa SHA-256 para assinar e verificar campos controlados. Implementa apenas headers únicos em ASCII, canonicalização relaxed dos headers e simple do corpo. A lista h inclui From, To e Subject; o hash bh cobre o corpo completo deste exemplo, sem l=. Alterar o valor Amount ou acrescentar um rodapé não vazio falha no hash do corpo. Alterar Subject falha na assinatura. Uma dobra e espaços equivalentes num header continuam válidos, tal como linhas vazias finais no corpo simple. Um header novo não listado em h não invalida esta assinatura: não se deve alegar proteção de todos os campos.

3. Interpretar alinhamento e encaminhamento

No modelo estrito, compara domínios sem distinguir maiúsculas de minúsculas e exige igualdade exata. Uma assinatura válida de vendor.example não alinha com From em funds.example. Um subdomínio também não é igual ao domínio pai para alinhamento estrito. Basta um mecanismo com pass e alinhamento para o resultado DMARC pass; não é necessário que SPF e DKIM passem ambos. Num encaminhamento, SPF pode falhar devido à origem que chega ao recetor, enquanto uma assinatura DKIM preservada e alinhada ainda permite pass. Os resultados SPF do exercício são fornecidos como dados fictícios: não há resolução DNS nem avaliação SPF. O modelo não implementa alinhamento relaxado, descoberta de política ou DNS tree walk do RFC atual.

4. Rodar chaves e confirmar remediação

O seletor next começa sem chave no mapa local e a verificação falha. Depois de adicionar a chave pública, a nova assinatura verifica; a antiga continua válida durante sobreposição. Remover a chave antiga torna-a indisponível nesse mapa, mas não representa caches DNS de recetores reais. Planeia rotação com publicação, propagação, filas e validação antes de retirar a chave anterior. No capítulo Cisco de política consultado, integração Microsoft 365 em modo Read oferece visibilidade sem remediação; Read/Write exige também política e âmbito adequados. Uma classificação de phishing e uma ação pedida não provam que a mensagem saiu da caixa. Confirma o resultado e as exceções relevantes no tenant antes de reportar contenção.

5. Decidir perante fraude e evidência limitada

Um domínio parecido pode autenticar corretamente o seu próprio email. Uma conta legítima comprometida também pode enviar instruções falsas com autenticação válida. Para uma alteração financeira inesperada, segue confirmação por canal previamente conhecido e o procedimento de aprovação, sem usar apenas o contacto indicado na mensagem. Conserva identificadores, tempos, resultados confiáveis do recetor e ação efetiva. O laboratório executa 36 verificações e não envia mensagens, consulta DNS, aplica políticas Cisco ou comprova entrega. Não é um parser completo DKIM/DMARC. Resumo: separa integridade das partes assinadas, alinhamento do domínio, risco de conteúdo e remediação observada; cada conclusão precisa da sua evidência.

/** Original bounded DKIM-style signature experiment; no mail or DNS sent. */
import {createHash,generateKeyPairSync,sign,verify} from 'node:crypto';
import fs from 'node:fs';
import assert from 'node:assert/strict';
const checks=[];function check(name,value){assert(value,name);checks.push(name);}
function bodySimple(text){if(/(?<!\r)\n|\r(?!\n)/.test(text))throw Error('network CRLF required');return text.replace(/(?:\r\n)*$/,'')+'\r\n';}
function headerRelaxed([name,value]){if(!/^[A-Za-z0-9-]+$/.test(name))throw Error('unsupported field name');const unfolded=value.replace(/\r\n(?=[ \t])/g,'');if(/[\r\n]/.test(unfolded))throw Error('invalid field folding');return name.toLowerCase()+':'+unfolded.replace(/[ \t]+/g,' ').trim()+'\r\n';}
const hash=s=>createHash('sha256').update(s).digest('base64');
const headers=[['From','Ops <ops@funds.example>'],['To','Reviewer <reviewer@recipient.example>'],['Subject','Synthetic close report']];
const body='Synthetic reference R-42\r\nAmount: 100\r\n';
const oldKey=generateKeyPairSync('rsa',{modulusLength:2048}),newKey=generateKeyPairSync('rsa',{modulusLength:2048});
const keyMap=new Map([['old._domainkey.funds.example',oldKey.publicKey]]);
function payload(h,sig){const names=sig.h.split(':');if(new Set(names).size!==names.length||!names.includes('from'))throw Error('bounded unique header list required');return names.map(name=>{const found=h.filter(x=>x[0].toLowerCase()===name);if(found.length!==1)throw Error('exactly one signed field required');return headerRelaxed(found[0]);}).join('')+headerRelaxed(['DKIM-Signature',sig.empty]).slice(0,-2);}
function signature(h,b,key,domain='funds.example',selector='old'){
 const s={d:domain,s:selector,a:'rsa-sha256',c:'relaxed/simple',h:'from:to:subject',bh:hash(bodySimple(b))};
 s.empty=`v=1; a=${s.a}; c=${s.c}; d=${s.d}; s=${s.s}; h=${s.h}; bh=${s.bh}; b=`;
 s.b=sign('RSA-SHA256',Buffer.from(payload(h,s)),key.privateKey).toString('base64');return s;
}
function verifyLocal(h,b,s,map=keyMap){
 if(s.a!=='rsa-sha256'||s.c!=='relaxed/simple')return{valid:false,reason:'unsupported-algorithm'};
 const expected=`v=1; a=${s.a}; c=${s.c}; d=${s.d}; s=${s.s}; h=${s.h}; bh=${s.bh}; b=`;
 if(s.empty!==expected)return{valid:false,reason:'inconsistent-signature-fields'};
 const key=map.get(s.s+'._domainkey.'+s.d);if(!key)return{valid:false,reason:'no-local-key'};
 if(hash(bodySimple(b))!==s.bh)return{valid:false,reason:'body-hash'};
 try{return verify('RSA-SHA256',Buffer.from(payload(h,s)),key,Buffer.from(s.b,'base64'))?{valid:true,reason:'verified'}:{valid:false,reason:'signature'};}catch{return{valid:false,reason:'unsupported-headers'};}
}
// Only strict alignment of supplied authentication results, not full DMARC processing.
function aligned(a,b){return typeof a==='string'&&typeof b==='string'&&a.toLowerCase()===b.toLowerCase();}
function strictVerdict(author,spf,dkim){return (spf.pass&&aligned(author,spf.domain))||dkim.some(x=>x.pass&&aligned(author,x.domain))?'pass':'fail';}
const sig=signature(headers,body,oldKey),valid=verifyLocal(headers,body,sig);
check('RSA signing keys have 2048 bits',oldKey.publicKey.asymmetricKeyDetails.modulusLength===2048&&newKey.publicKey.asymmetricKeyDetails.modulusLength===2048);
check('empty simple body matches published SHA256 vector',hash(bodySimple(''))==='frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN/XKdLCPjaYaY=');
check('trailing empty lines collapse',bodySimple('x\r\n\r\n')==='x\r\n');
check('simple body preserves meaningful spaces',bodySimple(' x \r\n')===' x \r\n');
check('relaxed header unfolds and compresses whitespace',headerRelaxed(['SUBJect',' A\r\n\t B  '])==='subject:A B\r\n');
check('baseline signature verifies using actual RSA SHA256',valid.valid);
check('extra trailing empty body lines remain valid',verifyLocal(headers,body+'\r\n',sig).valid);
check('changed amount fails body hash',verifyLocal(headers,body.replace('100','900'),sig).reason==='body-hash');
check('appended nonempty footer fails body hash',verifyLocal(headers,body+'Footer\r\n',sig).reason==='body-hash');
check('body whitespace change fails simple canonicalization',verifyLocal(headers,body.replace('Amount: ','Amount:  '),sig).reason==='body-hash');
const changedSubject=structuredClone(headers);changedSubject[2][1]='Different report';
check('changed signed subject fails signature',verifyLocal(changedSubject,body,sig).reason==='signature');
const changedFrom=structuredClone(headers);changedFrom[0][1]='Ops <ops@other.example>';
check('changed signed From fails signature',verifyLocal(changedFrom,body,sig).reason==='signature');
check('unsigned added header leaves this signature valid',verifyLocal([...headers,['X-Note','not covered by h']],body,sig).valid);
const folded=structuredClone(headers);folded[2]=['SUBJect',' Synthetic\r\n\t close  report '];
check('equivalent relaxed header remains valid',verifyLocal(folded,body,sig).valid);
check('unrelated public key fails verification',verifyLocal(headers,body,sig,new Map([['old._domainkey.funds.example',newKey.publicKey]])).reason==='signature');
check('unpublished selector has no local key',verifyLocal(headers,body,signature(headers,body,newKey,'funds.example','next')).reason==='no-local-key');
keyMap.set('next._domainkey.funds.example',newKey.publicKey);const nextSig=signature(headers,body,newKey,'funds.example','next');
check('new selector verifies after local key publication',verifyLocal(headers,body,nextSig).valid);
check('old selector remains valid during overlap',verifyLocal(headers,body,sig).valid);
keyMap.delete('old._domainkey.funds.example');
check('removed old key no longer verifies through local resolver',verifyLocal(headers,body,sig).reason==='no-local-key');
check('new selector survives old key removal',verifyLocal(headers,body,nextSig).valid);
check('legacy SHA1 rejected by bounded verifier',verifyLocal(headers,body,{...nextSig,a:'rsa-sha1'}).reason==='unsupported-algorithm');
check('inconsistent signature metadata rejected',verifyLocal(headers,body,{...nextSig,d:'other.example'}).reason==='inconsistent-signature-fields');
check('duplicate signed From rejected by bounded parser',verifyLocal([...headers,headers[0]],body,nextSig).reason==='unsupported-headers');
const noSPF={pass:false,domain:'relay.example'},dkimGood={pass:valid.valid,domain:sig.d};
check('aligned DKIM can pass despite supplied SPF failure',strictVerdict('funds.example',noSPF,[dkimGood])==='pass');
check('unaligned SPF pass alone fails strict DMARC model',strictVerdict('funds.example',{pass:true,domain:'vendor.example'},[])==='fail');
check('aligned SPF pass can suffice despite DKIM failure',strictVerdict('funds.example',{pass:true,domain:'funds.example'},[{pass:false,domain:'funds.example'}])==='pass');
check('valid unaligned DKIM alone fails strict model',strictVerdict('funds.example',noSPF,[{pass:true,domain:'vendor.example'}])==='fail');
check('both authentication failures fail strict model',strictVerdict('funds.example',noSPF,[{pass:false,domain:'funds.example'}])==='fail');
check('domain comparison is case insensitive',strictVerdict('FUNDS.EXAMPLE',noSPF,[dkimGood])==='pass');
check('a subdomain is not exact strict alignment',strictVerdict('funds.example',noSPF,[{pass:true,domain:'mail.funds.example'}])==='fail');
check('one valid aligned signature among several suffices',strictVerdict('funds.example',noSPF,[{pass:false,domain:'funds.example'},{pass:true,domain:'vendor.example'},dkimGood])==='pass');
check('tampered body with SPF failure fails model',strictVerdict('funds.example',noSPF,[{pass:verifyLocal(headers,body+'Footer\r\n',nextSig).valid,domain:nextSig.d}])==='fail');
check('lookalike domain can authenticate its own identity',strictVerdict('funds-notices.example',{pass:true,domain:'funds-notices.example'},[])==='pass');
check('RSA signature is 256 bytes',Buffer.from(sig.b,'base64').length===256);
const damaged=Buffer.from(nextSig.b,'base64');damaged[0]^=1;
check('modified signature bytes fail verification',verifyLocal(headers,body,{...nextSig,b:damaged.toString('base64')}).reason==='signature');
let rejected=false;try{bodySimple('bare\nline');}catch{rejected=true;}
check('non-network line endings rejected rather than normalized silently',rejected);
console.log(JSON.stringify({passed:checks.length,failed:0,checks,node:process.version,openssl:process.versions.openssl,scriptSHA256:createHash('sha256').update(fs.readFileSync(new URL(import.meta.url))).digest('hex'),actualCryptographicSignVerify:true,actualDNS:false,actualSMTP:false,actualSPFEvaluation:false,actualCiscoTenant:false,cloudResourcesCreated:false,fullDKIMConformanceClaimed:false,fullDMARCConformanceClaimed:false,alignmentMode:'strict-only',observations:{rsaBits:2048,hashAlgorithm:'sha256',canonicalization:'relaxed/simple',baseline:'verified',bodyTamper:'body-hash',subjectTamper:'signature',unsignedAddedHeader:'verified',rotation:'new key accepted; old key absent after removal',spfResults:'supplied fictional fixtures',keys:'ephemeral memory only',privateKeysWritten:false},scope:'Original bounded signature experiment over controlled unique ASCII fields, plus strict alignment of supplied results. No wire-message/DNS parsing, relaxed alignment, DNS tree walk, receiver policy, reporting, SMTP, cloud or Cisco execution.'},null,2));
NA PRÁTICA

DKIM válido para vendor.example não alinha com From funds.example no modo estrito. Já um domínio parecido pode passar para si próprio sem legitimar a instrução.

Armadilhas comuns

Nome visível como identidade; DKIM pass como alinhamento; DMARC como ausência de fraude; remoção local de chave como revogação global imediata.

Tópicos relacionados: SPF, DKIM e DMARC · BEC e phishing · Rotação e resposta

Leva esta ideia contigo

Autenticação de email ajuda a avaliar origem e integridade; a decisão operacional exige contexto e resultado de remediação.

Criar conta

Referência: Domain-Based Message Authentication, Reporting, and Conformance · 350-701 SCOR v2.0, effective 2026-08-27; core component of CCNP Security

CCNP® e Cisco® são marcas registadas da Cisco Systems, Inc. e/ou das suas afiliadas. A dr.pt é uma plataforma de preparação independente e não está afiliada, associada, patrocinada, autorizada nem aprovada por Cisco. Os conteúdos e as perguntas são originais, não são perguntas oficiais de exame, e concluir os nossos testes não atribui nem garante qualquer certificação. Os nomes são usados apenas para identificar o tema. Todas as outras marcas pertencem aos respetivos titulares.