// 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.16 §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 and, for S5, the reason why // it does not prove that it came before the opening date. 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, tokenIsBTSP, } 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 the seal proves that it came before round_time: it carries * accuracy and t plus the accuracy is before round_time (spec v0.16, * §29.11). For a valid signer whose seal does not, reason says why. */ readonly before: boolean; readonly reason?: SealReason; } /** * Why a valid seal does not prove that it came before the opening date (spec * v0.16, §29.7): the reason of S5, and of the line of a signer of F6 that does * not say «antes de la fecha de apertura», the first that holds, as * SealReason of Go: * - 'late': t plus the accuracy, 0 without one, is not before round_time; * - 'no accuracy, BTSP': the token carries no accuracy, and its policy is * the BTSP of ETSI EN 319 421, which requires it; * - 'no accuracy': the token carries no accuracy. */ export type SealReason = 'late' | 'no accuracy, BTSP' | 'no accuracy'; /** 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. sealReason is why it does not prove that it came before * round_time (S5), undefined when it does (S4). */ readonly sealHolder?: string; readonly sealTime?: Instant; readonly sealReason?: SealReason; } // 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 reason of a token that verifies, the first that holds (spec v0.16, // §29.7): late, then without accuracy under BTSP, then without accuracy; // undefined when it proves that it came before round_time. function sealReason(tok: Token, roundTime: Instant | undefined): SealReason | undefined { if (roundTime === undefined || compareInstants(addInstants(tok.genTime, tok.accuracy), roundTime) >= 0) return 'late'; if (!tok.hasAccuracy) return tokenIsBTSP(tok) ? 'no accuracy, BTSP' : 'no accuracy'; return undefined; } // 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' }; const reason = sealReason(tok, roundTime); const line = { ...base, result: 'valid', sealHolder: holderText(certHolder(tok.tsa), tok.tsa.hash), sealTime: tok.genTime }; return reason === undefined ? { ...line, before: true } : { ...line, reason }; } /** * 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 v0.16, §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: S4 only when the token carries accuracy and * t plus the accuracy is before round_time, and otherwise S5 with its reason. * `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; sealReason?: SealReason } { 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 valid = { sealHolder: holderText(certHolder(tok.tsa), tok.tsa.hash), sealTime: tok.genTime }; const reason = sealReason(tok, roundTime); return reason === undefined ? { seal: 'S4', ...valid } : { seal: 'S5', ...valid, sealReason: reason }; }