// 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.12 §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 { compareBytes, equalBytes, toHex } from './bytes.ts'; import { type Decoder, Encoder, unmarshal } from './cbor.ts'; import { addInstants, certHolder, certIssuerHash, certIssuerName, certValidAt, checkSigner, checkToken, CmsAlgorithmError, CmsFormError, parseSignature, parseToken, type SignerInfo, type Token, 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; /** The most code points of a name of a certificate that §29.7 shows, the upper bound of a commonName in X.520. */ export const MAX_NAME_LEN = 64; /** 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 a name of a certificate. */ readonly holder: string; /** The issuer that the certificate says, with the same rules, and the SHA-256 of the DER of its Name when it does not meet them; '' for an absent signer. */ readonly issuer: string; /** 'valid', 'invalid', 'absent', 'not verifiable', 'without seal', 'invalid seal' or 'out of validity'. */ readonly result: string; /** The holder of the certificate of the authority of its seal, with the same rules, and t; undefined without a seal that verifies. */ readonly sealHolder?: string; 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 && compareBytes(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; } /** * SIGNERS, the content of key 1 of a signature of alg 2: the SHA-256 of the * certificate of each required signer, sorted in strictly ascending order of * bytes, as EncodeSigners of Go, with its texts when there are none, more than * 16 or two equal ones (spec §29.10). */ export function encodeSigners(hashes: readonly Uint8Array[]): Uint8Array { if (hashes.length < 1 || hashes.length > MAX_SIGNERS) throw new Error(`capsule: SIGNERS holds from 1 to ${MAX_SIGNERS} certificates`); const sorted = [...hashes].sort(compareBytes); for (let i = 1; i < sorted.length; i++) if (equalBytes(sorted[i - 1]!, sorted[i]!)) throw new Error('capsule: SIGNERS names a certificate twice'); const e = new Encoder(); e.array(sorted.length); for (const h of sorted) e.bstr(h); return e.out(); } // The number of code points of a well-formed string. function codePointCount(s: string): number { let n = 0; for (let i = 0; i < s.length; i++) { const c = s.charCodeAt(i); if (c < 0xd800 || c > 0xdbff) n++; } return n; } /** * How §29.7 shows a name of a certificate: the name, when it meets the rules of * the declared author, has at most MAX_NAME_LEN code points and no two spaces * in a row, and the SHA-256 given otherwise. A name cannot then line up, with * spaces, a text of its own where a terminal breaks the line. */ function holderText(name: string, hash: Uint8Array): string { if (name !== '' && name.isWellFormed() && codePointCount(name) <= MAX_NAME_LEN && !name.includes(' ')) { try { checkAuthor(name); return name; } catch (err) { /* v8 ignore next -- @preserve: checkAuthor throws only its own error */ if (!(err instanceof Error)) throw err; } } return toHex(hash); } // The time of a seal, plus its accuracy, against round_time: whether it precedes it. function before(tok: Token, roundTime: Instant | undefined): boolean { return roundTime !== undefined && compareInstants(addInstants(tok.genTime, tok.accuracy), roundTime) < 0; } // One SignerInfo as §29.10 orders: not verifiable, invalid, without seal, with an invalid seal, out of validity, or valid. // The issuer is text of the certificate, as the holder is: it gets the same rules, and the SHA-256 of its Name when it // fails them, so that no escape, no control and no bidirectional character reaches a line of the verdicts. 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), certIssuerHash(s.cert)), before: false }; const r = checkSigner(s, msg); if (r !== 'valid') return { ...base, result: r }; if (s.token === undefined) return { ...base, result: 'without seal' }; let tok: Token | undefined; try { tok = parseToken(s.token); } catch (err) { if (!(err instanceof CmsFormError || err instanceof CmsAlgorithmError)) throw err; } if (tok === undefined || !checkToken(tok, s.signature)) return { ...base, result: 'invalid seal' }; if (!certValidAt(s.cert, tok.genTime)) return { ...base, result: 'out of validity' }; return { ...base, result: 'valid', sealHolder: holderText(certHolder(tok.tsa), tok.tsa.hash), sealTime: tok.genTime, before: before(tok, roundTime) }; } /** * 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' }; return { seal: before(tok, roundTime) ? 'S4' : 'S5', sealHolder: holderText(certHolder(tok.tsa), tok.tsa.hash), sealTime: tok.genTime }; }