CMS as Go reads it: the issuer by its Name, uncompressed keys, the BOM, linear readers

Fixes T1, T3, T13 and the CMS part of T5 of the review of the session of
1 and 2 October (docs, spec_v0.11/revision_sesion_1_2_octubre.md):

- securitycms.ts: an issuer that breaks the rules of the declared author
  shows the SHA-256 of the DER of its Name, Go's sha256.Sum256(RawIssuer),
  and no longer that of the certificate (cms.certIssuerHash).
- cms.ts: an ECDSA key counts only with its point uncompressed, 0x04 and
  the two coordinates, the only form that Go's x509.ParsePKIXPublicKey
  reads: a compressed one makes a signer not verifiable (F5) and a seal S1.
- cms.ts: a UTF8String and the times of a certificate and of a token keep a
  leading U+FEFF, as Go reads the bytes: such a name shows the hash, and
  such a time breaks the profile (F1, S2).
- cms.ts: oidOf and intOf take time linear in the length of the element.
  An arc of up to seven digits accumulates in a number, a longer one and
  every INTEGER are read whole from hexadecimal, never by a shift per byte,
  which took some 700 ms for 60 KB; attributes of one type are appended,
  not copied.
- security.ts: evaluateSecurity never throws. A fault while it evaluates
  the signature gives F1, one while it evaluates the seal S2, each apart,
  and one while it decodes the area X, as the Go reference will from v0.12.
- Tests: securitycms.test.ts and security.failure.test.ts are new.
  cms.test.ts now refuses a second content-type and two signature-time-
  stamps for the rule of the count, with every SET OF in DER order, and
  der.test.ts tests the depth at its boundary. cmsbuild.ts makes names,
  validities and compressed points of its own.

capsule.EvaluateSecurityIn of the Go reference at spec-v0.11 gives the same
verdicts, signer lines and Spanish lines on 25 areas made with cmsbuild.ts:
issuers with ESC, U+202E, empty, of 300 bytes or with a leading U+FEFF;
names and times with a leading U+FEFF; and compressed keys on P-256, P-384
and P-521, as signers and as authorities of a seal. HEAD gave other ones in
19 of them. npm run verify passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main
dev 6 days ago
parent 3f80e589fa
commit 06e992fa74

@ -18,7 +18,7 @@ import {
tokenImprintIsSHA256,
} from './cms.ts';
import { derContent, splitDer } from './der.ts';
import { concatBytes, equalBytes } from './bytes.ts';
import { concatBytes, equalBytes, toHex } from './bytes.ts';
import * as b from './testing/cmsbuild.ts';
const from = new Date(Date.UTC(2025, 0, 1));
@ -148,6 +148,8 @@ describe('a token over a signature', () => {
['genTime with a trailing zero', info(b.generalizedTime('20260930120000.50Z'))],
['genTime without seconds', info(b.generalizedTime('202609301200Z'))],
['genTime that does not exist', info(b.generalizedTime('20261331120000Z'))],
// Go reads the bytes, and its genTime does not parse; a TextDecoder would drop the U+FEFF.
['genTime after a byte order mark', info(b.generalizedTime('\u{feff}20260930120000Z'))],
['ordering FALSE written', info(good, b.tlv(0x01, Uint8Array.of(0)))],
['an extra INTEGER at the end', info(good, b.int(7), b.int(8), b.int(9))],
['a field out of order', info(good, b.int(7), b.seq(b.int(1)))],
@ -213,11 +215,6 @@ describe('the form of a signature', () => {
['no certificate of the signer', await b.signature(msg, { omitCert: true }, a)],
['a version that does not match the sid', await b.signature(msg, { version: 3 }, a)],
['an attribute without a value', await b.signature(msg, { extraAttrs: [b.seq(b.oid(b.OID.contentType), b.set(0x31))] }, a)],
['a second content-type', await b.signature(msg, { extraAttrs: [attrOf(b.OID.contentType, b.oid(b.OID.data))] }, a)],
[
'two timestamp attributes',
await b.signature(msg, { token2: true, token: () => Promise.resolve(b.seq(b.oid(b.OID.data))) }, a),
],
[
'a signing-certificate with another hash',
await b.signature(msg, { mutate: (attrs) => [...attrs.slice(0, 2), attrOf(b.OID.sigCertV2, b.seq(b.seq(b.seq(b.octets(new Uint8Array(32))))))] }, a),
@ -243,6 +240,36 @@ describe('the form of a signature', () => {
for (const [name, der] of bad) expect(() => parseSignature(der), name).toThrow(CmsFormError);
});
// Each attribute of the profile is one attribute with one value: a second attribute of the type, or a second value of
// the same attribute, breaks the rule of the count, with sets of values that stay in DER order (spec §29.10 rule 4).
it('counts the attributes of a type apart from their values, and wants one of each', async () => {
const a = await b.newECDSA('Ana', 'P-256', from, to);
const attrOf = (o: string, ...v: Uint8Array[]): Uint8Array => b.seq(b.oid(o), b.set(0x31, ...v));
const token = (): Promise<Uint8Array> => Promise.resolve(b.seq(b.oid('1.2.3')));
const cases: [string, Uint8Array, string][] = [
[
'a second content-type attribute',
await b.signature(msg, { extraAttrs: [attrOf(b.OID.contentType, b.oid('1.2.3'))] }, a),
'content-type: 2 attributes with 2 values, not one with one',
],
[
'a content-type with two values',
await b.signature(msg, { mutate: (attrs) => [attrOf(b.OID.contentType, b.oid(b.OID.data), b.oid('1.2.3')), attrs[1]!, attrs[2]!] }, a),
'content-type: 1 attributes with 2 values, not one with one',
],
[
'a second message-digest attribute',
await b.signature(msg, { extraAttrs: [attrOf(b.OID.messageDigest, b.octets(new Uint8Array(32)))] }, a),
'message-digest: 2 attributes with 2 values, not one with one',
],
['two signature-time-stamp attributes', await b.signature(msg, { token2: 'attribute', token }, a), 'signature-time-stamp: 2 attributes with 2 values, not one with one'],
['a signature-time-stamp with two values', await b.signature(msg, { token2: 'value', token }, a), 'signature-time-stamp: 1 attributes with 2 values, not one with one'],
];
for (const [name, der, rule] of cases) expect(() => parseSignature(der), name).toThrow(`cms: the form breaks the profile: ${rule}`);
// One signature-time-stamp is the token, read as it is.
expect(parseSignature(await b.signature(msg, { token }, a)).signers[0]!.token).toEqual(b.seq(b.oid('1.2.3')));
});
it('accepts a signing-certificate beside the v2, an explicit SHA-256 hashAlgorithm and a signature with junk in its unsigned attributes', async () => {
const a = await b.newECDSA('Ana', 'P-256', from, to);
const both = parseSignature(await b.signature(msg, { sigCertV1: true }, a));
@ -268,4 +295,46 @@ describe('the certificates', () => {
expect(() => parseCert(Uint8Array.of(1))).toThrow(CmsFormError);
expect(() => parseCert(concatBytes(a.cert, Uint8Array.of(0)))).toThrow(CmsFormError);
});
// Go reads the bytes of a name and of a time: a leading U+FEFF stays in the name, for the rules of text to refuse, and
// a time that starts with it does not parse. A TextDecoder without ignoreBOM would drop it.
it('keeps a byte order mark at the start of a UTF8String, and refuses a time that starts with one', async () => {
const bom = Uint8Array.of(0xef, 0xbb, 0xbf);
const cn = b.rdnName(['2.5.4.3', b.tlv(0x0c, bom, new TextEncoder().encode('Ana'))]);
const named = parseCert((await b.newECDSA('Ana', 'P-256', from, to, 'cn', { subject: cn, issuer: cn })).cert);
expect([certHolder(named), certIssuerName(named)]).toEqual(['\u{feff}Ana', '\u{feff}Ana']);
const utc = (s: Uint8Array): Uint8Array => b.tlv(0x17, s);
const validity = b.seq(utc(concatBytes(bom, new TextEncoder().encode('250101000000Z'))), utc(new TextEncoder().encode('300101000000Z')));
const late = await b.newECDSA('Ana', 'P-256', from, to, 'cn', { validity });
expect(() => parseCert(late.cert)).toThrow(CmsFormError);
const signature = await b.signature(msg, {}, late);
expect(() => parseSignature(signature)).toThrow(CmsFormError);
});
// An INTEGER and an OBJECT IDENTIFIER of tens of kilobytes are read in time linear in their length: a shift per byte
// took some 700 ms for each.
it('reads a serial number and an arc of an OID of 60 KB, whole', () => {
const te = new TextEncoder();
// A negative serial: 0x80 and 59 999 bytes more, in two's complement.
const serial = Uint8Array.from({ length: 60_000 }, (_, i) => (i * 7 + 1) & 0xff);
serial[0] = 0x80;
// The issuer names a type 2.25 and a UUID, an arc of 19 digits of base 128, and a type 2.48 and an arc of
// 60 000 digits, 2^420000 - 1.
const uuid = b.oid('2.25.329800735698586629295641978511506172918');
const digits = new Uint8Array(60_000).fill(0xff);
digits[digits.length - 1] = 0x7f;
const huge = b.tlv(0x06, Uint8Array.of(0x81, 0x00), digits);
const name = b.seq(b.set(0x31, b.seq(uuid, b.utf8('a'))), b.set(0x31, b.seq(huge, b.utf8('b'))));
const validity = b.seq(b.tlv(0x17, te.encode('250101000000Z')), b.tlv(0x17, te.encode('300101000000Z')));
const spki = b.seq(b.seq(b.oid('1.2.840.10045.2.1'), b.oid('1.2.840.10045.3.1.7')), b.tlv(0x03, Uint8Array.of(0, 4)));
const tbs = b.seq(b.tlv(0xa0, b.int(2)), b.tlv(0x02, serial), b.seq(b.oid(b.OID.ecdsa['SHA-256'])), name, validity, name, spki);
const der = b.seq(tbs, b.seq(b.oid(b.OID.ecdsa['SHA-256'])), b.tlv(0x03, Uint8Array.of(0)));
const start = performance.now();
const c = parseCert(der);
const issuer = certIssuerName(c);
const ms = performance.now() - start;
expect(c.serial).toBe(BigInt(`0x${toHex(serial.subarray(1))}`) - (1n << 479_999n));
expect(issuer).toBe(`2.48.${(1n << 420_000n) - 1n}=b,2.25.329800735698586629295641978511506172918=a`);
expect(ms).toBeLessThan(500);
});
});

@ -15,7 +15,7 @@
import { sha1 } from '@noble/hashes/legacy.js';
import { sha256, sha384, sha512 } from '@noble/hashes/sha2.js';
import { p256, p384, p521 } from '@noble/curves/nist.js';
import { concatBytes, equalBytes } from './bytes.ts';
import { concatBytes, decodeUtf8, equalBytes, toHex } from './bytes.ts';
import { compareInstants, type Instant } from './datekey.ts';
import { checkDer, derContent, DerError, setOfSorted, splitDer } from './der.ts';
@ -36,32 +36,63 @@ export class CmsAlgorithmError extends Error {
}
// ---- small DER readers ------------------------------------------------------
//
// Both read in time linear in the length of the element: a bigint built by a
// shift per byte costs the square of it, some 700 ms for 60 KB, and the
// security area holds up to 64 KiB.
// The longest arc, in digits of base 128, that accumulates in a number: 49
// bits, which a double holds exactly.
const MAX_NUMBER_ARC = 7;
const HEX_DIGITS = '0123456789abcdef';
// The dotted text of an OBJECT IDENTIFIER, as Go's ObjectIdentifier.String.
function oidOf(el: Uint8Array): string {
const c = derContent(el);
const parts: string[] = [];
let v = 0n;
let first = true;
for (const b of c) {
v = (v << 7n) | BigInt(b & 0x7f);
if ((b & 0x80) === 0) {
if (first) {
const x = v < 40n ? 0n : v < 80n ? 1n : 2n;
parts.push(String(x), String(v - 40n * x));
first = false;
} else {
parts.push(String(v));
}
v = 0n;
let start = 0;
for (let i = 0; i < c.length; i++) {
if ((c[i]! & 0x80) !== 0) continue;
const v = arcValue(c.subarray(start, i + 1));
if (parts.length === 0) {
const x = v < 40n ? 0n : v < 80n ? 1n : 2n;
parts.push(String(x), String(v - 40n * x));
} else {
parts.push(String(v));
}
start = i + 1;
}
return parts.join('.');
}
// The value of an arc from its digits of base 128. An arc of more than seven
// digits, which no OID of the profile has, is read whole from its bits as
// hexadecimal, never by a shift per digit.
function arcValue(d: Uint8Array): bigint {
if (d.length <= MAX_NUMBER_ARC) {
let n = 0;
for (const x of d) n = n * 128 + (x & 0x7f);
return BigInt(n);
}
const nibbles: string[] = [];
let acc = 0;
let bits = 0;
for (let i = d.length - 1; i >= 0; i--) {
acc |= (d[i]! & 0x7f) << bits;
bits += 7;
for (; bits >= 4; bits -= 4) {
nibbles.push(HEX_DIGITS[acc & 15]!);
acc >>>= 4;
}
}
nibbles.push(HEX_DIGITS[acc]!);
return BigInt(`0x${nibbles.reverse().join('')}`);
}
// An INTEGER, in two's complement, read whole from its hexadecimal.
function intOf(el: Uint8Array): bigint {
const c = derContent(el);
let v = 0n;
for (const b of c) v = (v << 8n) | BigInt(b);
const v = BigInt(`0x0${toHex(c)}`);
return c.length > 0 && (c[0]! & 0x80) !== 0 ? v - (1n << BigInt(8 * c.length)) : v;
}
@ -159,18 +190,12 @@ export interface Cert {
readonly issuer: readonly Rdn[];
}
const UTF8 = new TextDecoder('utf-8', { fatal: true });
// The text of an attribute value that is a string type; undefined for any other.
function attrText(el: Uint8Array): string | undefined {
const c = derContent(el);
switch (el[0]) {
case 0x0c: // UTF8String
try {
return UTF8.decode(c);
} catch {
return undefined;
}
case 0x0c: // UTF8String, whose leading U+FEFF stays, as in Go, for the rules of text to refuse
return decodeUtf8(c);
case 0x13: // PrintableString
case 0x16: // IA5String
case 0x1a: // VisibleString
@ -200,9 +225,12 @@ function parseName(el: Uint8Array): readonly Rdn[] {
});
}
// The bytes of a time as text, a leading U+FEFF kept: no time starts with it.
const TIME_TEXT = new TextDecoder('utf-8', { ignoreBOM: true });
// UTCTime and GeneralizedTime as a certificate has them, YYMMDDHHMMSSZ and YYYYMMDDHHMMSSZ.
function parseCertTime(el: Uint8Array): Instant {
const s = new TextDecoder().decode(derContent(el));
const s = TIME_TEXT.decode(derContent(el));
const m = el[0] === 0x17 ? /^(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)Z$/.exec(s) : el[0] === 0x18 ? /^(\d{4})(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)Z$/.exec(s) : null;
if (m === null) throw form('a time of a certificate');
let year = Number(m[1]);
@ -324,6 +352,15 @@ export function certIssuerName(c: Cert): string {
return cn !== '' ? cn : nameString(c.issuer).trim();
}
/**
* The SHA-256 of the DER of the Name of the issuer, which a reader shows in
* place of an issuer that breaks the rules of the declared author (spec
* §29.7), as Go's sha256.Sum256(RawIssuer).
*/
export function certIssuerHash(c: Cert): Uint8Array {
return sha256(c.rawIssuer);
}
/** Whether `t` is in the validity period of the certificate. */
export function certValidAt(c: Cert, t: Instant): boolean {
return compareInstants(t, c.notBefore) >= 0 && compareInstants(t, c.notAfter) <= 0;
@ -527,7 +564,10 @@ function attrs(b: Uint8Array): AttrSet {
const oid = oidOf(p[0]!);
const vals = splitDer(p[1]!).children;
if (vals.length === 0 || !setOfSorted(vals)) throw form('the values of an attribute are not a non-empty SET OF in DER order');
out.vals.set(oid, [...(out.vals.get(oid) ?? []), ...vals]);
// Appended, not copied: thousands of attributes of one type fit in the area.
const list = out.vals.get(oid);
if (list === undefined) out.vals.set(oid, vals);
else for (const v of vals) list.push(v);
out.count.set(oid, (out.count.get(oid) ?? 0) + 1);
}
return out;
@ -694,7 +734,8 @@ type PublicKey = { kind: 'rsa'; n: bigint; e: bigint } | { kind: 'ec'; curve: ty
// The key of the certificate when it is in the table: RSA of 2048 to 4096
// bits with an odd exponent from 3 to 2^31 - 1, or ECDSA on P-256, P-384 or
// P-521.
// P-521 with an uncompressed point, the only form that Go's
// x509.ParsePKIXPublicKey reads.
function publicKey(c: Cert): PublicKey | undefined {
try {
checkDer(c.spki);
@ -719,6 +760,8 @@ function publicKey(c: Cert): PublicKey | undefined {
const curveOid = oidOf(alg.params);
const curve = curveOid === OID.p256 ? p256 : curveOid === OID.p384 ? p384 : curveOid === OID.p521 ? p521 : undefined;
if (curve === undefined) return undefined;
// 0x04 and the two coordinates: noble would also read a compressed point.
if (key[0] !== 0x04 || key.length !== 1 + 2 * curve.Point.Fp.BYTES) return undefined;
curve.Point.fromBytes(key); // a point of the curve, or it throws
return { kind: 'ec', curve, point: key };
}
@ -914,7 +957,7 @@ function parseTSTInfo(b: Uint8Array): { genTime: Instant; accuracy: Instant; imp
// A GeneralizedTime as RFC 3161 and DER write it: YYYYMMDDHHMMSS, a fraction without a trailing zero, and Z.
function parseGenTime(el: Uint8Array): Instant {
const s = new TextDecoder().decode(derContent(el));
const s = TIME_TEXT.decode(derContent(el));
const m = /^(\d{4})(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(\.(\d+))?Z$/.exec(s);
if (m === null) throw form('the TSTInfo: genTime is not in UTC with the letter Z');
const whole = utc(Number(m[1]), Number(m[2]), Number(m[3]), Number(m[4]), Number(m[5]), Number(m[6]));

@ -7,6 +7,13 @@ import { h } from './testing/testdata.ts';
const zeros = (n: number): string => '00'.repeat(n);
// n SEQUENCEs, each holding the next, as hex; the innermost holds `inner`, or nothing.
function nested(n: number, inner = ''): string {
let hex = `30${(inner.length / 2).toString(16).padStart(2, '0')}${inner}`;
for (let i = 1; i < n; i++) hex = `30${(hex.length / 2).toString(16).padStart(2, '0')}${hex}`;
return hex;
}
describe('checkDer', () => {
const cases: [name: string, hex: string, ok: boolean][] = [
['sequence of an integer and a null', '3005020101' + '0500', true],
@ -46,7 +53,8 @@ describe('checkDer', () => {
['a reserved universal tag', '0e0141', false],
['a SEQUENCE of the end of contents', '30020000', false],
['UTF8String', '0c026162', true],
['too deep', '30'.repeat(40).replace(/30/g, '30') + '', false],
['33 SEQUENCEs, each holding the next', nested(33), true],
['34 SEQUENCEs, each holding the next', nested(34), false],
];
it.each(cases)('%s', (_name, hex, ok) => {
const b = h(hex);
@ -54,11 +62,13 @@ describe('checkDer', () => {
else expect(() => checkDer(b)).toThrow(DerError);
});
it('refuses an element nested more than 32 levels', () => {
// 34 SEQUENCEs, each holding the next, with the lengths filled in.
let b = h('3000');
for (let i = 0; i < 33; i++) b = Uint8Array.of(0x30, b.length, ...b);
expect(() => checkDer(b)).toThrow(/nested too deep/);
// The outermost element is at depth 0: an element at depth 33, constructed or not, is refused for its depth alone,
// since everything else about it is valid.
it('refuses an element nested more than 32 levels below the outermost one', () => {
expect(() => checkDer(h(nested(33)))).not.toThrow();
expect(() => checkDer(h(nested(34)))).toThrow('der: nested too deep');
expect(() => checkDer(h(nested(32, '0500')))).not.toThrow();
expect(() => checkDer(h(nested(33, '0500')))).toThrow('der: nested too deep');
});
});

@ -0,0 +1,102 @@
// Faults of the evaluation of the security area that no input reaches today:
// the decoder, the strict verification of alg 1 and the evaluators of alg 2
// and of the seal are replaced by functions that throw, in a file of its own
// because vi.mock replaces a module for the whole file. evaluateSecurity never
// throws, since the security area never decides the opening (spec §29.3): a
// fault while it evaluates the signature gives F1, and one while it evaluates
// the seal, S2, each without touching the other verdict; one while it decodes
// the area gives X. The Go reference does the same from v0.12.
import { afterEach, describe, expect, it, vi } from 'vitest';
import { controlCommit, headDigest } from './author.ts';
import { peek } from './cbor.ts';
import { decodeControl } from './control.ts';
import { parseRFC3339 } from './datekey.ts';
import { verifyStrict } from './ed25519strict.ts';
import { FORMAT_3 } from './framing.ts';
import { evaluateSecurity, type SecurityContext } from './security.ts';
import { evaluateCMS, evaluateSeal } from './securitycms.ts';
import { h, readJSON } from './testing/testdata.ts';
vi.mock('./cbor.ts', async (importOriginal) => {
const m = await importOriginal<typeof import('./cbor.ts')>();
return { ...m, peek: vi.fn(m.peek) };
});
vi.mock('./ed25519strict.ts', async (importOriginal) => {
const m = await importOriginal<typeof import('./ed25519strict.ts')>();
return { ...m, verifyStrict: vi.fn(m.verifyStrict) };
});
vi.mock('./securitycms.ts', async (importOriginal) => {
const m = await importOriginal<typeof import('./securitycms.ts')>();
return { ...m, evaluateCMS: vi.fn(m.evaluateCMS), evaluateSeal: vi.fn(m.evaluateSeal) };
});
interface Record {
control_cbor: string;
head_cbor: string;
security_cbor: string;
unlock_at: string;
}
// The security area of a fixture of the Go reference, in the context of its capsule.
function fixture(name: string): { area: Uint8Array; context: SecurityContext } {
const fx = readJSON<Record>(`fixtures/${name}.json`);
const context = {
controlCommit: controlCommit(decodeControl(h(fx.control_cbor), FORMAT_3), FORMAT_3),
headDigest: headDigest(h(fx.head_cbor)),
roundTime: parseRFC3339(fx.unlock_at),
};
return { area: h(fx.security_cbor), context };
}
const fault = (): never => {
throw new Error('a fault that no input should cause');
};
afterEach(() => {
vi.mocked(peek).mockReset();
vi.mocked(verifyStrict).mockReset();
vi.mocked(evaluateCMS).mockReset();
vi.mocked(evaluateSeal).mockReset();
});
describe('evaluateSecurity, when an evaluator throws', () => {
// format3_sealed holds a signature of alg 1, F4, and a seal of seal_type 2 from before the round, S4.
it('gives the verdicts of the fixtures when nothing throws', () => {
const sealed = fixture('format3_sealed');
expect(evaluateSecurity(sealed.area, sealed.context)).toMatchObject({ signature: 'F4', seal: 'S4' });
const cms = fixture('format3_signed_cms');
expect(evaluateSecurity(cms.area, cms.context)).toMatchObject({ signature: 'F6', seal: 'S0' });
});
it('gives F1 for a fault in a signature of alg 1, and the seal still verifies', () => {
const { area, context } = fixture('format3_sealed');
vi.mocked(verifyStrict).mockImplementationOnce(fault);
const v = evaluateSecurity(area, context);
expect([v.signature, v.seal, v.authorKey, v.detail?.sealHolder]).toEqual(['F1', 'S4', undefined, 'Autoridad de Sellado de prueba']);
});
it('gives F1 for a fault in a signature of alg 2, with no signer named', () => {
const { area, context } = fixture('format3_signed_cms');
vi.mocked(evaluateCMS).mockImplementationOnce(() => {
throw 'not even an Error';
});
expect(evaluateSecurity(area, context)).toEqual({ signature: 'F1', seal: 'S0' });
});
it('gives S2 for a fault in the seal, and the signature still verifies', () => {
const { area, context } = fixture('format3_sealed');
vi.mocked(evaluateSeal).mockImplementationOnce(fault);
const v = evaluateSecurity(area, context);
expect([v.signature, v.seal, v.authorKey === undefined, v.detail]).toEqual(['F4', 'S2', false, undefined]);
});
it('gives F1 and S2 for faults in both, and X for a fault while it decodes the area', () => {
const { area, context } = fixture('format3_sealed');
vi.mocked(verifyStrict).mockImplementationOnce(fault);
vi.mocked(evaluateSeal).mockImplementationOnce(fault);
expect(evaluateSecurity(area, context)).toEqual({ signature: 'F1', seal: 'S2' });
vi.mocked(peek).mockImplementationOnce(fault);
expect(evaluateSecurity(area, context)).toEqual({ signature: 'X', seal: 'X' });
});
});

@ -1,11 +1,13 @@
// The security area of a format 3 capsule (spec §29.3, §29.7), as
// EncodeSecurity, EvaluateSecurity and the verdicts of the Go package
// capsule at spec-v0.10 (format3.go). SECURITY_CBOR is the map
// EncodeSecurity, EvaluateSecurityIn and the verdicts of the Go package
// capsule at spec-v0.11 (format3.go, signature.go). SECURITY_CBOR is the map
// {0: "datekeys-security", 1: 1, ? 2: author-signature, ? 3: seal}, whose
// keys 2 and 3 hold CBOR encoded apart. This version implements no alg and
// no seal_type, and writes the area empty. Its evaluation never fails: the
// security area never decides the opening, and its verdicts carry no error
// code. Internal: index.ts does not re-export it.
// keys 2 and 3 hold CBOR encoded apart. In the context of a capsule it checks
// the signature of alg 1 with the strict profile of ed25519strict.ts, and the
// signature of alg 2 and the seal of seal_type 2 with securitycms.ts; the
// writer of this library does not sign, and writes the area empty. Its
// evaluation never throws: the security area never decides the opening, and
// its verdicts carry no error code. Internal: index.ts does not re-export it.
import { ALG_CMS, ALG_ED25519, authorMessage, signersDigest } from './author.ts';
import { bech32Encode } from './bech32.ts';
@ -33,9 +35,18 @@ const SEAL_TYPE_RFC3161 = 2;
* - F0: no signature (no key 2);
* - F1: a signature that is not checked: it does not decode, breaks its
* schema or has an alg this reader does not implement;
* - F2: a signature that does not correspond to this content: of alg 1, it
* does not verify; of alg 2, a required signer has an invalid one;
* - F3 and F4: a valid signature of alg 1, with a key that the person saved,
* whose label it names, or with another;
* - F5: a signature of alg 2 without a signature or a seal that it demands;
* - F6: a signature of alg 2 whose required signers are all valid and sealed;
* - S0: no seal (no key 3); nothing is shown about the date;
* - S1: a seal_type this reader does not implement;
* - S2: a seal that does not decode or breaks its schema.
* - S1: a seal_type this reader does not implement, or a token with an
* algorithm outside the table;
* - S2: a seal that does not decode or breaks its schema;
* - S3: a seal that does not correspond to this content;
* - S4 and S5: a valid seal, from before the time of the round or not.
*/
export type Verdict = 'X' | 'F0' | 'F1' | 'F2' | 'F3' | 'F4' | 'F5' | 'F6' | 'S0' | 'S1' | 'S2' | 'S3' | 'S4' | 'S5';
@ -272,35 +283,42 @@ function decodeAlg(d: Decoder): number {
return v;
}
// Runs a decoding whose failure is a verdict, not an error: its result, or
// undefined when it fails. Anything but a DateKeysError is a bug and
// propagates.
function attempt<T>(decode: () => T): T | undefined {
// What the evaluation of the signature gives, and that of the seal.
type SignatureVerdicts = Pick<Verdicts, 'signature' | 'authorKey' | 'authorLabel' | 'detail'>;
type SealVerdicts = { seal: Verdict; sealHolder?: string; sealTime?: Instant };
// Runs an evaluation whose failure is a verdict, never an error: its result,
// or `failed` when it throws, whatever it throws. The decoders throw a
// DateKeysError for content that breaks its schema; anything else is a fault
// that no input should cause, of this library or of noble, and gives the
// same verdict, since the security area never decides the opening (spec
// §29.3).
function attempt<T, F>(evaluate: () => T, failed: F): T | F {
try {
return decode();
} catch (err) {
/* v8 ignore next -- @preserve: the decoders throw only DateKeysError */
if (!(err instanceof DateKeysError)) throw err;
return undefined;
return evaluate();
} catch {
return failed;
}
}
/**
* Reads SECURITY_CBOR and returns its verdicts (spec §29.3, §29.7). It never
* fails: the security area never decides the opening. For the signature and
* throws: the security area never decides the opening. For the signature and
* for the seal apart, the first row of the table of §29.7 that holds
* decides: alg and seal_type are read only from content that decodes and
* meets its schema.
* meets its schema. An exception while the signature is evaluated gives F1,
* and one while the seal is, S2, each without touching the other verdict.
*/
export function evaluateSecurity(b: Uint8Array, context?: SecurityContext): Verdicts {
const w = attempt(() => {
const h = peek(b);
return h.typeTag === SECURITY_TYPE_TAG && h.version === SECURITY_VERSION ? unmarshal(b, decodeWire, encodeWire) : undefined;
});
}, undefined);
if (w === undefined) return { signature: 'X', seal: 'X' };
const { signature, seal } = w;
const sig = signature === undefined ? { signature: 'F0' as const } : evaluateSignature(signature, seal !== undefined, context);
const sealed = seal === undefined ? { seal: 'S0' as const } : evaluateSealArea(seal, signature, context);
const sig: SignatureVerdicts =
signature === undefined ? { signature: 'F0' } : attempt(() => evaluateSignature(signature, seal !== undefined, context), { signature: 'F1' as const });
const sealed: SealVerdicts = seal === undefined ? { seal: 'S0' } : attempt(() => evaluateSealArea(seal, signature, context), { seal: 'S2' as const });
const detail: Detail | undefined =
sig.detail === undefined && sealed.sealHolder === undefined
? undefined
@ -314,26 +332,23 @@ export function evaluateSecurity(b: Uint8Array, context?: SecurityContext): Verd
// The verdict of the content of key 3 (spec §29.7, §29.11): S2 for content that does not decode, S1 for a seal_type
// that is not 2 and, without the context of a capsule, as in v0.10, for seal_type 2 too; otherwise the token is checked.
function evaluateSealArea(
seal: Uint8Array,
signature: Uint8Array | undefined,
context: SecurityContext | undefined,
): { seal: Verdict; sealHolder?: string; sealTime?: Instant } {
const s = attempt(() => unmarshal(seal, decodeSeal, encodeSeal));
function evaluateSealArea(seal: Uint8Array, signature: Uint8Array | undefined, context: SecurityContext | undefined): SealVerdicts {
const s = attempt(() => unmarshal(seal, decodeSeal, encodeSeal), undefined);
if (s === undefined) return { seal: 'S2' };
if (s.sealType !== SEAL_TYPE_RFC3161 || context === undefined) return { seal: 'S1' };
return evaluateSeal(s.token, signature, context.controlCommit, context.headDigest, context.roundTime);
}
// The verdict of the content of key 2 (spec §29.7, §29.9): F1 for content
// that does not decode, an alg this reader does not implement, or a key or a
// signature of another length, and without the context of a capsule, as in
// v0.10; F2 when the signature does not verify with the strict profile; F3
// or F4 when it does. This version implements alg 1 only.
function evaluateSignature(content: Uint8Array, hasSeal: boolean, context: SecurityContext | undefined): Pick<Verdicts, 'signature' | 'authorKey' | 'authorLabel' | 'detail'> {
// The verdict of the content of key 2 (spec §29.7, §29.9, §29.10): F1 for
// content that does not decode, an alg this reader does not implement, or a
// key or a signature of alg 1 of another length, and without the context of
// a capsule, as in v0.10; alg 2, a signature with certificates, goes to
// evaluateCMS, which gives F1, F2, F5 or F6; alg 1 is F2 when the signature
// does not verify with the strict profile, and F3 or F4 when it does.
function evaluateSignature(content: Uint8Array, hasSeal: boolean, context: SecurityContext | undefined): SignatureVerdicts {
const unchecked = { signature: 'F1' } as const;
if (context === undefined) return unchecked;
const a = attempt(() => unmarshal(content, decodeAuthorSignature, encodeAuthorSignature));
const a = attempt(() => unmarshal(content, decodeAuthorSignature, encodeAuthorSignature), undefined);
if (a === undefined) return unchecked;
if (a.alg === ALG_CMS) {
const r = evaluateCMS(a.key, a.value, hasSeal, context.controlCommit, context.headDigest, context.roundTime);

@ -0,0 +1,152 @@
// Tests of securitycms.ts, the verdicts of a signature of alg 2 and of a seal
// of seal_type 2 (spec v0.11 §29.7, §29.10, §29.11), through evaluateSecurity
// in the context of a capsule, on signatures and tokens that
// testing/cmsbuild.ts makes: what a signer line shows of a certificate, a byte
// order mark at the start of a name or a time, and keys whose point is
// compressed. capsule.EvaluateSecurityIn of the Go reference at spec-v0.11
// gives the same verdicts, signer lines and Spanish lines on areas made the
// same way: an oracle compared them, and the hashes of the issuers below are
// the ones it gave for these Names.
import { sha256 } from '@noble/hashes/sha2.js';
import { describe, expect, it } from 'vitest';
import { ALG_CMS, authorMessage, sealSubject, signersDigest } from './author.ts';
import { compareBytes, toHex } from './bytes.ts';
import { Encoder } from './cbor.ts';
import { evaluateSecurity, type SecurityContext, type Verdicts, verdictLines } from './security.ts';
import * as b from './testing/cmsbuild.ts';
const from = new Date(Date.UTC(2025, 0, 1));
const to = new Date(Date.UTC(2030, 0, 1));
const now = new Date(Date.UTC(2026, 8, 30, 12));
const context: SecurityContext = {
controlCommit: new Uint8Array(32).fill(1),
headDigest: new Uint8Array(32).fill(2),
roundTime: { seconds: Date.UTC(2026, 9, 1) / 1000, nanos: 0 },
};
const te = new TextEncoder();
const BOM = Uint8Array.of(0xef, 0xbb, 0xbf);
// SECURITY_CBOR with the contents of keys 2 and 3 given.
function area(signature: Uint8Array | undefined, seal: Uint8Array | undefined): Uint8Array {
const e = new Encoder();
e.map(2 + (signature === undefined ? 0 : 1) + (seal === undefined ? 0 : 1));
e.uint(0);
e.text('datekeys-security');
e.uint(1);
e.uint(1);
if (signature !== undefined) {
e.uint(2);
e.bstr(signature);
}
if (seal !== undefined) {
e.uint(3);
e.bstr(seal);
}
return e.out();
}
// The verdicts of a signature of alg 2 by the signers, all of them required,
// each with a signature-time-stamp of `tsa` when it is given.
async function signed(tsa: b.Signer | undefined, ...signers: b.Signer[]): Promise<Verdicts> {
const hashes = signers.map((s) => sha256(s.cert)).sort(compareBytes);
const list = new Encoder();
list.array(hashes.length);
for (const x of hashes) list.bstr(x);
const key1 = list.out();
const msg = authorMessage(context.controlCommit, context.headDigest, signersDigest(ALG_CMS, key1));
const cms = await b.signature(msg, tsa === undefined ? {} : { token: (sig) => b.token(sig, now, {}, tsa) }, ...signers);
const e = new Encoder();
e.map(3);
e.uint(0);
e.uint(ALG_CMS);
e.uint(1);
e.bstr(key1);
e.uint(2);
e.bstr(cms);
return evaluateSecurity(area(e.out(), undefined), context);
}
// The verdicts of a seal of seal_type 2 by `tsa`, without a signature.
async function sealed(tsa: b.Signer): Promise<Verdicts> {
const token = await b.token(sealSubject(context.controlCommit, context.headDigest, undefined), now, {}, tsa);
const e = new Encoder();
e.map(2);
e.uint(0);
e.uint(2);
e.uint(1);
e.bstr(token);
return evaluateSecurity(area(undefined, e.out()), context);
}
const cn = (s: string): Uint8Array => b.rdnName(['2.5.4.3', b.utf8(s)], ['2.5.4.10', b.utf8('DateKeys test')]);
describe('the issuer of a signer', () => {
// The issuer is text of the certificate, as the holder is: one that breaks the rules of the declared author shows
// the SHA-256 of the DER of its Name, Go's sha256.Sum256(RawIssuer), and never that of the certificate.
it('shows the SHA-256 of the Name of an issuer that breaks the rules of the declared author, as Go', async () => {
const tsa = await b.newECDSA('TSA de prueba', 'P-256', from, to);
const issuers: [string, Uint8Array, string][] = [
['an issuer with ESC', cn('Ana\x1b[2J'), '53213e495daf7cc473c94da46283bae22d5d54b58681a0635cd22b96abde7c3f'],
['an issuer with U+202E', cn('Ana\u{202e}gpj.exe'), 'b1772664c1dbb3470b8d419f2275839241987889333ce17f223414a939634559'],
['an empty issuer', b.seq(), 'e4f60d0aa6d7f3d3b6a6494b1c861b99f649c6f9ec51abaf201b20f297327c95'],
['an issuer of 300 bytes', cn('x'.repeat(300)), '01ce813ba635fe45b8125d46b368eec8eee9a1d00f4c0b066c387c1b04a2ee92'],
['an issuer that starts with U+FEFF', b.rdnName(['2.5.4.3', b.tlv(0x0c, BOM, te.encode('Autoridad'))]), '2bcf35767b63b7b0370d61cf63620623adac5a09662763ebd331316d4d4c6097'],
];
for (const [name, issuer, hash] of issuers) {
const s = await b.newECDSA('Ana', 'P-256', from, to, 'cn', { issuer });
const v = await signed(tsa, s);
expect([v.signature, v.seal], name).toEqual(['F6', 'S0']);
const line = v.detail!.signers[0]!;
expect([line.holder, line.issuer, line.result], name).toEqual(['Ana', hash, 'valid']);
expect(toHex(sha256(issuer)), name).toBe(hash);
expect(verdictLines(v)[1], name).toBe(` Ana (emisor según su certificado: ${hash}), sellado el 2026-09-30T12:00:00Z, antes de la fecha de apertura.`);
}
// An issuer that meets the rules shows its name.
const good = await signed(tsa, await b.newECDSA('Ana', 'P-256', from, to, 'cn', { issuer: cn('Autoridad de prueba') }));
expect(good.detail!.signers[0]!.issuer).toBe('Autoridad de prueba');
});
});
describe('a byte order mark at the start of a name or a time', () => {
// Go reads the bytes: the U+FEFF of a UTF8String stays, the rules of text refuse it, and the hash is shown; a time
// that starts with it does not parse, and the certificate breaks the profile.
it('keeps it in a name, which then shows the hash of the certificate, as Go', async () => {
const tsa = await b.newECDSA('TSA de prueba', 'P-256', from, to);
const subject = b.rdnName(['2.5.4.3', b.tlv(0x0c, BOM, te.encode('Ana'))]);
const s = await b.newECDSA('Ana', 'P-256', from, to, 'cn', { subject });
const v = await signed(tsa, s);
expect([v.signature, v.detail!.signers[0]!.holder]).toEqual(['F6', toHex(sha256(s.cert))]);
const authority = await b.newECDSA('TSA', 'P-256', from, to, 'cn', { subject: b.rdnName(['2.5.4.3', b.tlv(0x0c, BOM, te.encode('TSA'))]) });
const seal = await sealed(authority);
expect([seal.seal, seal.detail!.sealHolder]).toEqual(['S4', toHex(sha256(authority.cert))]);
});
it('refuses a time of a certificate that starts with it: F1 for a signer, S2 for the authority of a seal, as Go', async () => {
const tsa = await b.newECDSA('TSA de prueba', 'P-256', from, to);
const utc = (s: Uint8Array): Uint8Array => b.tlv(0x17, s);
const validity = b.seq(utc(new Uint8Array([...BOM, ...te.encode('250101000000Z')])), utc(te.encode('300101000000Z')));
const v = await signed(tsa, await b.newECDSA('Ana', 'P-256', from, to, 'cn', { validity }));
expect([v.signature, v.seal, v.detail]).toEqual(['F1', 'S0', undefined]);
const seal = await sealed(await b.newECDSA('TSA', 'P-256', from, to, 'cn', { validity }));
expect([seal.signature, seal.seal]).toEqual(['F0', 'S2']);
});
});
describe('an ECDSA key with its point compressed', () => {
// Go's x509.ParsePKIXPublicKey reads only the uncompressed point, 0x04 and the two coordinates: a key written
// otherwise is outside the table, though noble would read it.
it.each(['P-256', 'P-384', 'P-521'] as const)('is outside the table on %s: F5 for a signer, S1 for the authority of a seal, as Go', async (curve) => {
const tsa = await b.newECDSA('TSA de prueba', 'P-256', from, to);
const compressed = await b.newECDSA('Ana', curve, from, to, 'cn', { compressed: true });
const v = await signed(tsa, compressed);
expect([v.signature, v.detail!.signers[0]!.result]).toEqual(['F5', 'not verifiable']);
const plain = await b.newECDSA('Ana', curve, from, to);
expect((await signed(tsa, plain)).signature).toBe('F6');
const authority = await b.newECDSA('TSA', curve, from, to, 'cn', { compressed: true });
expect((await sealed(authority)).seal).toBe('S1');
const late = await signed(authority, plain);
expect([late.signature, late.detail!.signers[0]!.result]).toEqual(['F5', 'invalid seal']);
expect((await sealed(await b.newECDSA('TSA', curve, from, to))).seal).toBe('S4');
});
});

@ -10,6 +10,7 @@ import { type Decoder, Encoder, unmarshal } from './cbor.ts';
import {
addInstants,
certHolder,
certIssuerHash,
certIssuerName,
certValidAt,
checkSigner,
@ -33,7 +34,7 @@ const MAX_AUTHOR_LEN = 256;
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. */
/** 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. */
readonly issuer: string;
/** 'valid', 'invalid', 'absent', 'not verifiable', 'without seal', 'invalid seal' or 'out of validity'. */
readonly result: string;
@ -94,8 +95,10 @@ function holderText(name: string, hash: Uint8Array): string {
}
// 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: an issuer that breaks the rules shows the SHA-256 of its Name,
// 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), s.cert.hash) };
const base = { holder: holderText(certHolder(s.cert), s.cert.hash), issuer: holderText(certIssuerName(s.cert), certIssuerHash(s.cert)) };
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 };

@ -8,6 +8,7 @@
// issued a certificate.
import { concatBytes, compareBytes } from '../bytes.ts';
import { derContent, splitDer } from '../der.ts';
// WebCrypto takes a BufferSource over an ArrayBuffer, which a Uint8Array view does not promise.
const bs = (b: Uint8Array): ArrayBuffer => b.slice().buffer as ArrayBuffer;
@ -102,6 +103,19 @@ function name(cn: string, kind: NameKind = 'cn'): Uint8Array {
return seq(set(0x31, seq(oid('2.5.4.3'), utf8(cn))), org);
}
/** A Name of one attribute per RDN, each given as its type and the DER of its value. */
export function rdnName(...attributes: [type: string, value: Uint8Array][]): Uint8Array {
return seq(...attributes.map(([type, value]) => set(0x31, seq(oid(type), value))));
}
/** What a test changes in a certificate: the DER of its subject, of its issuer and of its Validity, and the point of an ECDSA key, compressed. */
export interface CertOptions {
readonly subject?: Uint8Array;
readonly issuer?: Uint8Array;
readonly validity?: Uint8Array;
readonly compressed?: boolean;
}
function utcTime(t: Date): Uint8Array {
const p = (n: number, w = 2): string => String(n).padStart(w, '0');
const y = t.getUTCFullYear();
@ -109,21 +123,30 @@ function utcTime(t: Date): Uint8Array {
return y < 2050 ? tlv(0x17, new TextEncoder().encode(`${p(y % 100)}${body}`)) : tlv(0x18, new TextEncoder().encode(`${p(y, 4)}${body}`));
}
async function signerOf(kind: Signer['kind'], bits: number, cn: string, notBefore: Date, notAfter: Date, nameKind: NameKind = 'cn'): Promise<Signer> {
// The SubjectPublicKeyInfo of an ECDSA key with its point 04 || x || y written compressed, 02 or 03 || x.
function compressPoint(spki: Uint8Array): Uint8Array {
const [alg, key] = splitDer(spki).children;
const point = derContent(key!).subarray(1);
const x = point.subarray(1, 1 + (point.length - 1) / 2);
return seq(alg!, tlv(0x03, Uint8Array.of(0, 2 + (point[point.length - 1]! & 1)), x));
}
async function signerOf(kind: Signer['kind'], bits: number, cn: string, notBefore: Date, notAfter: Date, nameKind: NameKind = 'cn', o: CertOptions = {}): Promise<Signer> {
const algorithm = kind === 'rsa' ? { name: 'RSASSA-PKCS1-v1_5', modulusLength: bits, publicExponent: Uint8Array.of(1, 0, 1), hash: 'SHA-256' } : { name: 'ECDSA', namedCurve: kind };
const pair = (await crypto.subtle.generateKey(algorithm, true, ['sign', 'verify'])) as CryptoKeyPair;
const spki = new Uint8Array(await crypto.subtle.exportKey('spki', pair.publicKey));
const exported = new Uint8Array(await crypto.subtle.exportKey('spki', pair.publicKey));
const spki = o.compressed === true ? compressPoint(exported) : exported;
const pkcs8 = new Uint8Array(await crypto.subtle.exportKey('pkcs8', pair.privateKey));
const ski = new TextEncoder().encode(cn);
const issuer = name(cn, nameKind);
const issuer = o.issuer ?? name(cn, nameKind);
const serial = BigInt(Math.floor(Math.random() * 2 ** 40) + 1);
const tbs = seq(
tlv(0xa0, int(2)),
int(serial),
seq(oid(OID.ecdsa['SHA-256'])),
issuer,
seq(utcTime(notBefore), utcTime(notAfter)),
issuer,
o.validity ?? seq(utcTime(notBefore), utcTime(notAfter)),
o.subject ?? name(cn, nameKind),
spki,
tlv(0xa3, seq(seq(oid('2.5.29.14'), octets(octets(ski))))),
);
@ -134,8 +157,8 @@ async function signerOf(kind: Signer['kind'], bits: number, cn: string, notBefor
/** A signer with an RSA key of `bits` bits. */
export const newRSA = (cn: string, bits: number, notBefore: Date, notAfter: Date): Promise<Signer> => signerOf('rsa', bits, cn, notBefore, notAfter);
/** A signer with an ECDSA key on a NIST curve. */
export const newECDSA = (cn: string, curve: 'P-256' | 'P-384' | 'P-521', notBefore: Date, notAfter: Date, nameKind: NameKind = 'cn'): Promise<Signer> =>
signerOf(curve, 0, cn, notBefore, notAfter, nameKind);
export const newECDSA = (cn: string, curve: 'P-256' | 'P-384' | 'P-521', notBefore: Date, notAfter: Date, nameKind: NameKind = 'cn', o: CertOptions = {}): Promise<Signer> =>
signerOf(curve, 0, cn, notBefore, notAfter, nameKind, o);
// ECDSA from WebCrypto is r || s; a CMS signature is the DER of the two integers.
function ecdsaDER(raw: Uint8Array): Uint8Array {
@ -177,8 +200,13 @@ export interface Options {
message?: Uint8Array;
/** Edits the signedAttrs, as a list of the DER of each attribute, before they are signed. */
mutate?: (attrs: Uint8Array[]) => Uint8Array[];
/** Puts two signature-time-stamp attributes, to break the profile. */
token2?: boolean;
/**
* Puts a second signature-time-stamp beside the token, to break the
* profile: in an attribute of its own, or as a second value of the same
* attribute. It is a ContentInfo of id-data, which the token must not be,
* so that the SET OF stays in DER order.
*/
token2?: 'attribute' | 'value';
/** Adds this response in crls. */
ocsp?: Uint8Array;
/** The elements of crls as they are, instead of an OCSP response. */
@ -235,7 +263,9 @@ async function signerInfo(message: Uint8Array, o: Options, token: boolean, s: Si
if (o.junk !== undefined && o.junk > 0) unsigned.push(attr('1.2.3.4.5', octets(new Uint8Array(o.junk))));
if (o.token !== undefined) {
const t = await o.token(signature);
unsigned.push(o.token2 ? seq(oid(OID.timeStamp), set(0x31, t, seq(oid(OID.data)))) : attr(OID.timeStamp, t));
const second = seq(oid(OID.data));
if (o.token2 === 'attribute') unsigned.push(attr(OID.timeStamp, t), attr(OID.timeStamp, second));
else unsigned.push(o.token2 === 'value' ? attr(OID.timeStamp, t, second) : attr(OID.timeStamp, t));
}
if (unsigned.length > 0) f.push(set(0xa1, ...unsigned));
return seq(...f);

Loading…
Cancel
Save

Powered by TurnKey Linux.