Define the decision the verifier must support
A fictional funds-reporting release arrives with a file, a signature and a provenance statement. APS needs to know whether the evidence matches the received bytes and the authorized process. Define trust roots, signer-builder pairs, source origin, build type and accepted parameters in advance. Do not derive that policy from the envelope being evaluated. In the lab, locally generated keys represent two fictional origins. Signing a statement establishes possession of the corresponding private key within this experiment; it does not establish that the described build happened. Assessment of the actual builder and protection of its credentials remains necessary to trust operational claims.
Verify bytes before interpreting the statement
The exercise envelope uses DSSE: the signature covers payload type and serialized bytes framed by PAE. Lengths count UTF-8 bytes rather than visible characters. The example containing é and ação makes the difference observable. The keyid field is only an unauthenticated hint and does not add a key to the trust set. After verification, interpret the same bytes and confirm the supported type. Reformatting JSON without signing again changes bytes even when fields appear equivalent. The teaching implementation accepts standard and URL-safe base64 but limits the profile to one signature and one subject. It is neither a hardened general parser nor evidence of multiple-signer policies.
Bind provenance to the artifact and expected origin
The in-toto v1 statement identifies the subject by digest and declares the predicate type. The SLSA provenance used here retains predicateType https://slsa.dev/provenance/v1 even though the inspected specification documentation is version 1.2. In the exercise, policy compares file SHA-256, builder, buildType, repository, ref and resolved commit. An equal filename does not repair a different digest. A trusted key for the controlled builder does not automatically authorize the sandbox builder. The resolved commit prevents a moving branch name from being the sole source identity. Unknown external parameters are rejected in this profile to avoid silently accepting an unreviewed debug option or other behavior.
Run failures and interpret the limited result
The code below executes 30 checks: it accepts the original and rejects changed bytes, an unknown signer, wrong types, an incorrect signer-builder pair, an unauthorized fork, a different commit and an extra parameter. Two actual local runs retained the script hash and results. It also accepts the same artifact again: this lab has no freshness or replay policy. Acceptance of a second explicitly authorized pair demonstrates that trust is configuration rather than a property of the builder.id text. All signed content is synthetic and created in the same process. No CI ran, provider was contacted, transparency log queried or SLSA level awarded. Compare each result with its expected reason before using it as learning evidence.
Keep provenance and release authorization distinct
Accepted provenance may still describe vulnerable software, incorrect configuration or an old release. Bind test and component-analysis results to the same final digest, with scope, date, exceptions and ownership. A component inventory does not guarantee absence of vulnerabilities. Before deployment, confirm the bytes actually transferred; a mutable tag may point to another artifact after approval. For rollback, retain the artifact, evidence and compatibility decision for current data and dependencies. An emergency release needs an explicit exception with bounded criteria; removing the verifier does not create equivalent evidence. Summary: signature, byte matching, authorized origin and operational acceptance are separate conditions. Connect this lesson with CI/CD, change management, obsolescence and incidents.
// Original educational lab. Synthetic claims, not a CI build or production verifier.
import assert from 'node:assert/strict'
import {generateKeyPairSync,sign,verify,createHash} from 'node:crypto'
import {readFileSync,writeFileSync} from 'node:fs'
import {fileURLToPath} from 'node:url'
const hash=b=>createHash('sha256').update(b).digest('hex');
const type='application/vnd.in-toto+json'
export function pae(t,b){const tb=Buffer.from(t,'utf8');return Buffer.concat([Buffer.from(`DSSEv1 ${tb.length} `),tb,Buffer.from(` ${b.length} `),b]);}
function decode(s){
if(typeof s!=='string'||!/^[A-Za-z0-9+/_-]*={0,2}$/.test(s)||s.replace(/=+$/,'').length%4===1)throw Error('encoding');
const normalized=s.replaceAll('-','+').replaceAll('_','/'),b=Buffer.from(normalized,'base64');
if(b.toString('base64').replace(/=+$/,'')!==normalized.replace(/=+$/,''))throw Error('encoding');
if(s.includes('=')&&s.length%4!==0)throw Error('encoding');return b;
}
const builder='https://example.invalid/builders/funds-controlled/v1'
const alternateBuilder='https://example.invalid/builders/funds-sandbox/v1'
const buildType='https://example.invalid/buildtypes/funds/v1'
const repo='https://example.invalid/source/funds'
const ref='refs/heads/release'
const commit='71'.repeat(20);
const dependencyURI=`git+${repo}@${ref}`;
const artifact=Buffer.from('BigSavant fictional funds report\namount_minor=1729\ncurrency=EUR\n');
const trusted=generateKeyPairSync('ed25519'),second=generateKeyPairSync('ed25519'),unknown=generateKeyPairSync('ed25519');
const roots=[{key:trusted.publicKey,builder},{key:second.publicKey,builder:alternateBuilder}];
function statement{return {_type:'https://in-toto.io/Statement/v1',subject:[{name:'relatório-fundos',digest:{sha256:hash(artifact)}}],predicateType:'https://slsa.dev/provenance/v1',predicate:{buildDefinition:{buildType,externalParameters:{repository:repo,ref},resolvedDependencies:[{uri:dependencyURI,digest:{gitCommit:commit}}]},runDetails:{builder:{id:builder}}}};}
function envelope(s,key=trusted.privateKey,payloadType=type){const body=Buffer.from(JSON.stringify(s));return{payloadType,payload:body.toString('base64'),signatures:[{keyid:'a hint, not a trust anchor',sig:sign(null,pae(payloadType,body),key).toString('base64')}]};}
export function accept(e,bytes,trust=roots){
// Deliberately narrow one-signature / one-subject / one-dependency teaching profile.
try{
if(!e||typeof e.payloadType!=='string'||!Array.isArray(e.signatures)||e.signatures.length!==1)throw Error('envelope-profile');
const body=decode(e.payload),signature=decode(e.signatures[0].sig);
const recognized=trust.filter(r=>verify(null,pae(e.payloadType,body),r.key,signature));
if(!recognized.length)throw Error('signature-or-trust');
if(e.payloadType!==type)throw Error('payload-type');
const s=JSON.parse(new TextDecoder('utf-8',{fatal:true}).decode(body));
if(s._type!=='https://in-toto.io/Statement/v1')throw Error('statement-type');
if(s.predicateType!=='https://slsa.dev/provenance/v1')throw Error('predicate-type');
if(!Array.isArray(s.subject)||s.subject.length!==1||s.subject[0]?.digest?.sha256!==hash(bytes))throw Error('artifact-digest');
const p=s.predicate,b=p?.buildDefinition;
if(!recognized.some(r=>r.builder===p?.runDetails?.builder?.id))throw Error('signer-builder-pair');
if(b?.buildType!==buildType)throw Error('build-type');
const params=b.externalParameters;
if(!params||Object.keys(params).sort.join(',')!=='ref,repository'||params.repository!==repo||params.ref!==ref)throw Error('external-parameters');
const d=b.resolvedDependencies;
if(!Array.isArray(d)||d.length!==1||d[0]?.uri!==dependencyURI||d[0]?.digest?.gitCommit!==commit)throw Error('source-pin');
return{accepted:true,reason:'local-policy-met'};
}catch(error){return{accepted:false,reason:error.message};}
}
const checks=[];
function check(id,e,bytes,accepted,reason){const got=accept(e,bytes);assert.equal(got.accepted,accepted,id);assert.equal(got.reason,reason,id);checks.push({id,...got,pass:true});}
const good=envelope(statement);
check('trusted-original',good,artifact,true,'local-policy-met');
check('changed-artifact',good,Buffer.concat([artifact,Buffer.from('extra')]),false,'artifact-digest');
const altered=structuredClone(good);const alteredStatement=statement;alteredStatement.subject[0].name='other'altered.payload=Buffer.from(JSON.stringify(alteredStatement)).toString('base64');
check('changed-payload-without-resigning',altered,artifact,false,'signature-or-trust');
check('untrusted-signing-key',envelope(statement,unknown.privateKey),artifact,false,'signature-or-trust');
const spoof=envelope(statement,unknown.privateKey);spoof.signatures[0].keyid='trusted'check('keyid-does-not-create-trust',spoof,artifact,false,'signature-or-trust');
const ignored=structuredClone(good);ignored.signatures[0].keyid='different-hint'check('keyid-does-not-change-trusted-signature',ignored,artifact,true,'local-policy-met');
check('signed-unsupported-payload-type',envelope(statement,trusted.privateKey,'application/json'),artifact,false,'payload-type');
function mutation(id,fn,reason,key=trusted.privateKey){const s=statement;fn(s);check(id,envelope(s,key),artifact,false,reason);}
mutation('wrong-statement-type',s=>s._type='https://in-toto.io/Statement/v0.1','statement-type');
mutation('wrong-predicate-type',s=>s.predicateType='https://example.invalid/other','predicate-type');
mutation('signed-wrong-digest',s=>s.subject[0].digest.sha256='00'.repeat(32),'artifact-digest');
mutation('missing-subject',s=>delete s.subject,'artifact-digest');
mutation('wrong-builder',s=>s.predicate.runDetails.builder.id=alternateBuilder,'signer-builder-pair');
check('trusted-key-wrong-builder-pair',envelope(statement,second.privateKey),artifact,false,'signer-builder-pair');
const sandbox=statement;sandbox.predicate.runDetails.builder.id=alternateBuilder;check('second-approved-pair',envelope(sandbox,second.privateKey),artifact,true,'local-policy-met');
mutation('wrong-build-type',s=>s.predicate.buildDefinition.buildType+='-other','build-type');
mutation('unofficial-fork',s=>s.predicate.buildDefinition.externalParameters.repository+='-fork','external-parameters');
mutation('different-ref',s=>s.predicate.buildDefinition.externalParameters.ref='refs/heads/dev','external-parameters');
mutation('unexpected-parameter',s=>s.predicate.buildDefinition.externalParameters.debug='true','external-parameters');
mutation('missing-parameters',s=>delete s.predicate.buildDefinition.externalParameters,'external-parameters');
mutation('changed-source-pin',s=>s.predicate.buildDefinition.resolvedDependencies[0].digest.gitCommit='ab'.repeat(20),'source-pin');
mutation('missing-resolved-source',s=>s.predicate.buildDefinition.resolvedDependencies=[],'source-pin');
mutation('wrong-dependency-uri',s=>s.predicate.buildDefinition.resolvedDependencies[0].uri+='-other','source-pin');
check('malformed-payload-base64',{...good,payload:'%%%not-base64'},artifact,false,'encoding');
const badsig=structuredClone(good);badsig.signatures[0].sig='%%%%'check('malformed-signature-base64',badsig,artifact,false,'encoding');
const urlsafe=structuredClone(good);urlsafe.payload=decode(urlsafe.payload).toString('base64url');urlsafe.signatures[0].sig=decode(urlsafe.signatures[0].sig).toString('base64url');check('base64url-envelope',urlsafe,artifact,true,'local-policy-met');
const unpadded=structuredClone(good);unpadded.payload=unpadded.payload.replace(/=+$/,'');unpadded.signatures[0].sig=unpadded.signatures[0].sig.replace(/=+$/,'');check('unpadded-base64-envelope',unpadded,artifact,true,'local-policy-met');
assert.equal(pae('é',Buffer.from('ação')).toString('utf8'),'DSSEv1 2 é 6 ação');checks.push({id:'pae-counts-utf8-bytes',pass:true});
const reformatted=structuredClone(good);reformatted.payload=Buffer.from(JSON.stringify(statement,null,2)).toString('base64');check('reformatted-same-json-invalidates-signature',reformatted,artifact,false,'signature-or-trust');
const cleanRename=statement;cleanRename.subject[0].name='renamed-report'check('subject-name-is-not-content-identity',envelope(cleanRename),artifact,true,'local-policy-met');
check('same-artifact-accepted-again-no-replay-policy',good,artifact,true,'local-policy-met');
const report={executedAt:new Date.toISOString,runtime:process.version,platform:process.platform,scriptSHA256:hash(readFileSync(fileURLToPath(import.meta.url))),artifactSHA256:hash(artifact),checks,passed:checks.length,failed:0,scope:'Actual local Ed25519 signing and DSSE-style verification with synthetic in-toto/SLSA claims and explicit local acceptance policy.',limits:['No real CI build or builder attestation','No SLSA level awarded or independently assessed','No cloud DLP scanner or real customer data','No certificate authority, transparency log, KMS or deployment','No freshness, replay, revocation or threshold-signature policy','Narrow single-signature/single-subject teaching profile; not a hardened general-purpose verifier','No independent specialist review']};
if(process.argv[2])writeFileSync(process.argv[2],JSON.stringify(report,null,2)+'\n');
console.log(JSON.stringify({passed:report.passed,failed:report.failed,scriptSHA256:report.scriptSHA256}))A valid signature for a different digest or an unauthorized builder is insufficient; the lab rejects both conditions.
Common pitfalls
Trusting keyid; comparing names alone; accepting any builder under the same key; confusing signatures with secure code; awarding a SLSA level to a simulation.
Related topics: CI/CD and release criteria · Software supply chain and incident management
Connect signature, digest, origin and policy before accepting a release.
Reference: SLSA Build: Verifying artifacts · CCSP examination outline effective 2026-08-01; January2026 V2 PDF