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.
DateKeys-App/src/lib/dkc/securitycms.ts

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 };
}

Powered by TurnKey Linux.