You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
188 lines
7.8 KiB
188 lines
7.8 KiB
// 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<string, SignerInfo>(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 };
|
|
}
|