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

255 lines
11 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.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<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 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 };
}

Powered by TurnKey Linux.