← CCNP Security: SCOR core and operations
20 / 25 · 55 MIN

Email: signatures, alignment and response

Verify local signatures and distinguish domain authentication, instruction legitimacy and effective remediation.

1. Identify what was authenticated

A fictional message requests an urgent change to payment details. The display name says “Operations”, but that text must be distinguished from the From domain, MAIL FROM and the signature’s DKIM d= domain. SPF evaluates origin authorization for the relevant envelope identity; DKIM associates a signature with a domain and signed message parts. DMARC relates an authenticated result to the author domain. It does not authenticate the request’s business intent. RFC 9989, published in May 2026, replaced RFCs 7489 and 9091; actual product adoption must be checked. The course uses the current reference without assuming every gateway already implements it.

2. Execute signing and observe changes

The Node.js lab generates two 2048-bit RSA keys in memory and uses SHA-256 to sign and verify controlled fields. It implements only unique ASCII headers, relaxed header canonicalization and simple body canonicalization. The h list includes From, To and Subject; bh covers this example’s full body without l=. Changing Amount or adding a nonempty footer fails the body hash. Changing Subject fails the signature. Equivalent folding and spaces in a header remain valid, as do trailing empty lines under simple body canonicalization. A new header absent from h does not invalidate this signature: protection of every field must not be claimed.

3. Interpret alignment and forwarding

In the strict model, compare domains case-insensitively and require exact equality. A valid signature from vendor.example does not align with From at funds.example. A subdomain also differs from its parent under strict alignment. One passing, aligned mechanism suffices for DMARC pass; SPF and DKIM need not both pass. During forwarding, SPF can fail because of the origin reaching the receiver while a preserved, aligned DKIM signature still enables pass. Exercise SPF results are supplied as fictional data: there is no DNS resolution or SPF evaluation. The model implements neither relaxed alignment nor policy discovery or the current RFC’s DNS tree walk.

4. Rotate keys and confirm remediation

Selector next initially has no key in the local map and verification fails. After adding the public key, the new signature verifies; the old signature remains valid during overlap. Removing the old key makes it unavailable in that map but does not represent real receivers’ DNS caches. Plan rotation around publication, propagation, queues and validation before retiring the earlier key. In the Cisco policy chapter consulted, Microsoft 365 Read integration provides visibility without remediation; Read/Write also requires suitable policy and scope. A phishing classification and a requested action do not prove the message left the mailbox. Confirm the outcome and relevant tenant exceptions before reporting containment.

5. Decide under fraud and bounded evidence

A lookalike domain can correctly authenticate its own email. A compromised legitimate account can also send false instructions with valid authentication. For an unexpected financial change, use confirmation through a previously known channel and the approval procedure rather than relying only on contact details inside the message. Retain identifiers, times, trusted receiver results and actual action. The lab executes 36 checks and sends no messages, queries no DNS, applies no Cisco policy and proves no delivery. It is not a complete DKIM/DMARC parser. Summary: separate integrity of signed parts, domain alignment, content risk and observed remediation; each conclusion needs its own evidence.

/** 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))
IN PRACTICE

Valid DKIM for vendor.example does not align with From funds.example in strict mode. A lookalike domain can pass for itself without legitimizing the instruction.

Common pitfalls

Display name as identity; DKIM pass as alignment; DMARC as absence of fraud; local key removal as immediate global revocation.

Related topics: SPF, DKIM and DMARC · BEC and phishing · Rotation and response

Take this idea with you

Email authentication helps assess origin and integrity; operational decisions require context and remediation outcomes.

Create account

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

CCNP® and Cisco® are registered trademarks of Cisco Systems, Inc. and/or its affiliates. bigsavant.com is an independent preparation platform and is not affiliated with, associated with, sponsored, authorised or endorsed by Cisco. Content and questions are original, are not official exam questions, and completing our tests does not award or guarantee any certification. Names are used only to identify the subject. All other trademarks belong to their respective owners.