// The verdicts of a signature of alg 2 (CMS with certificates) and of a seal of // seal_type 2 (RFC 3161), as the Go package capsule gives them (signature2.go, // spec v0.11 §29.7, §29.10, §29.11): F1, F2, F5 and F6 with the signers named, // and S1 to S5 with the authority of a valid seal. Internal: index.ts does not // re-export it. import { ALG_CMS, authorMessage, sealSubject, signersDigest } from './author.ts'; import { equalBytes, toHex, utf8Length } from './bytes.ts'; import { type Decoder, Encoder, unmarshal } from './cbor.ts'; import { addInstants, certHolder, certIssuerName, certValidAt, checkSigner, checkToken, CmsAlgorithmError, CmsFormError, parseSignature, parseToken, type SignerInfo, tokenImprintIsSHA256, } from './cms.ts'; import { compareInstants, type Instant } from './datekey.ts'; import { DateKeysError } from './errors.ts'; import { checkAuthor } from './pathrule.ts'; /** The most required signers of an alg 2 signature (spec §29.10). */ export const MAX_SIGNERS = 16; const MAX_AUTHOR_LEN = 256; /** A signer of an alg 2 signature as a reader shows it (spec §29.7, §29.10). */ export interface SignerLine { /** The name of the certificate as §29.7 shows it, or the SHA-256 of the certificate in hexadecimal when it does not meet the rules of the declared author. */ readonly holder: string; /** The issuer that the certificate says, with the same rules. */ readonly issuer: string; /** 'valid', 'invalid', 'absent', 'not verifiable', 'without seal', 'invalid seal' or 'out of validity'. */ readonly result: string; /** t, undefined without a seal that verifies. */ readonly sealTime?: Instant; /** Whether t plus the accuracy of the seal is before round_time. */ readonly before: boolean; } /** What the texts of F6, S4 and S5 name (spec §29.7, §29.10). */ export interface Detail { /** The required signers, in the order of SIGNERS, and the SignerInfo of other certificates, which never count. */ readonly signers: readonly SignerLine[]; readonly foreign: readonly SignerLine[]; /** The holder of the certificate of the authority of a valid seal, as §29.7 writes it, and t. */ readonly sealHolder?: string; readonly sealTime?: Instant; } // SIGNERS: a CBOR array of 1 to 16 strings of 32 bytes in strictly ascending order of bytes (spec §29.10). Throws a DateKeysError when it is not. function decodeSigners(b: Uint8Array): Uint8Array[] { const out: Uint8Array[] = []; const decode = (d: Decoder): void => { const n = d.array(MAX_SIGNERS); if (n < 1) throw new DateKeysError('ERR_NON_CANONICAL_CBOR', 'SIGNERS is empty'); for (let i = 0; i < n; i++) { const h = d.bstr(32, 32); const last = out[out.length - 1]; if (last !== undefined && compare(last, h) >= 0) throw new DateKeysError('ERR_NON_CANONICAL_CBOR', 'SIGNERS is not in strictly ascending order'); out.push(h); } }; const encode = (e: Encoder): void => { e.array(out.length); for (const h of out) e.bstr(h); }; unmarshal(b, decode, encode); return out; } function compare(a: Uint8Array, b: Uint8Array): number { for (let i = 0; i < Math.min(a.length, b.length); i++) if (a[i] !== b[i]) return a[i]! < b[i]! ? -1 : 1; return a.length - b.length; } // How §29.7 shows a name: the name, when it meets the rules of the declared author, and the SHA-256 otherwise. function holderText(name: string, hash: Uint8Array): string { if (name !== '' && utf8Length(name) <= MAX_AUTHOR_LEN) { try { checkAuthor(name); return name; } catch (err) { /* v8 ignore next -- @preserve: checkAuthor throws only its own error */ if (!(err instanceof DateKeysError || err instanceof Error)) throw err; } } return toHex(hash); } // One SignerInfo as §29.10 orders: not verifiable, invalid, without seal, with an invalid seal, out of validity, or valid. function signerLine(s: SignerInfo, msg: Uint8Array, roundTime: Instant | undefined): SignerLine { const base = { holder: holderText(certHolder(s.cert), s.cert.hash), issuer: holderText(certIssuerName(s.cert), s.cert.hash) }; const r = checkSigner(s, msg); if (r === 'not verifiable') return { ...base, result: 'not verifiable', before: false }; if (r === 'invalid') return { ...base, result: 'invalid', before: false }; if (s.token === undefined) return { ...base, result: 'without seal', before: false }; let ok = false; let tok; try { tok = parseToken(s.token); ok = checkToken(tok, s.signature); } catch (err) { if (!(err instanceof CmsFormError || err instanceof CmsAlgorithmError)) throw err; } if (!ok || tok === undefined) return { ...base, result: 'invalid seal', before: false }; if (!certValidAt(s.cert, tok.genTime)) return { ...base, result: 'out of validity', before: false }; return { ...base, result: 'valid', sealTime: tok.genTime, before: roundTime !== undefined && compareInstants(addInstants(tok.genTime, tok.accuracy), roundTime) < 0 }; } /** * The verdict of a signature of alg 2 (spec §29.10): undefined for F1 (content * that breaks its profile), F2 when the signature of a required signer is * invalid, F5 when something the capsule demands is missing, F6 when every * required signer is valid and sealed. `signers` and `value` are keys 1 and 2 * of the author-signature; `hasSeal` is whether key 3 exists, which an alg 2 * signature forbids. */ export function evaluateCMS( signers: Uint8Array, value: Uint8Array, hasSeal: boolean, controlCommit: Uint8Array, headDigest: Uint8Array, roundTime: Instant | undefined, ): { signature: 'F2' | 'F5' | 'F6'; detail: Detail } | undefined { let required: Uint8Array[]; let sd; try { required = decodeSigners(signers); sd = parseSignature(value); } catch (err) { if (!(err instanceof DateKeysError || err instanceof CmsFormError)) throw err; return undefined; } const msg = authorMessage(controlCommit, headDigest, signersDigest(ALG_CMS, signers)); const byHash = new Map(sd.signers.map((s) => [toHex(s.cert.hash), s])); let invalid = false; let incomplete = hasSeal; const lines: SignerLine[] = []; for (const h of required) { const s = byHash.get(toHex(h)); if (s === undefined) { lines.push({ holder: toHex(h), issuer: '', result: 'absent', before: false }); incomplete = true; continue; } const line = signerLine(s, msg, roundTime); if (line.result === 'invalid') invalid = true; else if (line.result !== 'valid') incomplete = true; lines.push(line); } const foreign = sd.signers.filter((s) => !required.some((h) => equalBytes(h, s.cert.hash))).map((s) => signerLine(s, msg, roundTime)); return { signature: invalid ? 'F2' : incomplete ? 'F5' : 'F6', detail: { signers: lines, foreign } }; } /** * The verdict of a seal of seal_type 2 (spec §29.11): S2 or S1 for the form and * the algorithms, S3 when it does not verify, and S4 or S5 when it does, with * the authority and t. `signature` is the content of key 2, undefined without it. */ export function evaluateSeal( token: Uint8Array, signature: Uint8Array | undefined, controlCommit: Uint8Array, headDigest: Uint8Array, roundTime: Instant | undefined, ): { seal: 'S1' | 'S2' | 'S3' | 'S4' | 'S5'; sealHolder?: string; sealTime?: Instant } { let tok; try { tok = parseToken(token); } catch (err) { if (err instanceof CmsFormError) return { seal: 'S2' }; if (err instanceof CmsAlgorithmError) return { seal: 'S1' }; throw err; } if (!tokenImprintIsSHA256(tok)) return { seal: 'S1' }; if (!checkToken(tok, sealSubject(controlCommit, headDigest, signature))) return { seal: 'S3' }; const sealHolder = holderText(certHolder(tok.tsa), tok.tsa.hash); const before = roundTime !== undefined && compareInstants(addInstants(tok.genTime, tok.accuracy), roundTime) < 0; return { seal: before ? 'S4' : 'S5', sealHolder, sealTime: tok.genTime }; }