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.
341 lines
22 KiB
341 lines
22 KiB
// Tests of cms.ts, the reader of the CMS signatures and the RFC 3161 tokens of
|
|
// spec v0.11 §29.10 and §29.11: the same cases as the tests of the Go package
|
|
// internal/cms, with signatures made by testing/cmsbuild.ts.
|
|
|
|
import { describe, expect, it } from 'vitest';
|
|
import { sha256 } from '@noble/hashes/sha2.js';
|
|
import {
|
|
certHolder,
|
|
certIssuerName,
|
|
certValidAt,
|
|
checkSigner,
|
|
checkToken,
|
|
CmsAlgorithmError,
|
|
CmsFormError,
|
|
parseCert,
|
|
parseSignature,
|
|
parseToken,
|
|
tokenImprintIsSHA256,
|
|
} from './cms.ts';
|
|
import { derContent, splitDer } from './der.ts';
|
|
import { concatBytes, equalBytes, toHex } from './bytes.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 nowInstant = { seconds: now.getTime() / 1000, nanos: 0 };
|
|
const msg = new TextEncoder().encode('datekeys:dkc3:author-signature:v1\n00\n');
|
|
|
|
describe('the signature algorithms of the table', () => {
|
|
it('verifies RSA PKCS #1 and PSS with SHA-2, ECDSA on the three curves, and a signer named by subjectKeyIdentifier', async () => {
|
|
const rsa = await b.newRSA('Ana López', 2048, from, to);
|
|
const p256 = await b.newECDSA('Luis', 'P-256', from, to);
|
|
const p384 = await b.newECDSA('Eva', 'P-384', from, to);
|
|
const p521 = await b.newECDSA('Raúl', 'P-521', from, to);
|
|
const cases: [string, b.Options, b.Signer][] = [
|
|
['RSA PKCS1 SHA-256', {}, rsa],
|
|
['RSA PKCS1 SHA-384', { hash: 'SHA-384' }, rsa],
|
|
['RSA PKCS1 SHA-512', { hash: 'SHA-512' }, rsa],
|
|
['RSA PSS SHA-256', { pss: true }, rsa],
|
|
['RSA PSS SHA-384', { pss: true, hash: 'SHA-384' }, rsa],
|
|
['RSA PSS SHA-512', { pss: true, hash: 'SHA-512' }, rsa],
|
|
['RSA by subjectKeyIdentifier', { ski: true }, rsa],
|
|
['ECDSA P-256', {}, p256],
|
|
['ECDSA P-256 with SHA-384', { hash: 'SHA-384' }, p256],
|
|
['ECDSA P-384 SHA-384', { hash: 'SHA-384' }, p384],
|
|
['ECDSA P-521 SHA-512', { hash: 'SHA-512' }, p521],
|
|
];
|
|
for (const [name, opts, signer] of cases) {
|
|
const sd = parseSignature(await b.signature(msg, opts, signer));
|
|
expect(sd.signers, name).toHaveLength(1);
|
|
const si = sd.signers[0]!;
|
|
expect(equalBytes(si.cert.hash, sha256(signer.cert)), name).toBe(true);
|
|
expect(checkSigner(si, msg), name).toBe('valid');
|
|
expect(checkSigner(si, new TextEncoder().encode('another message')), name).toBe('invalid');
|
|
}
|
|
});
|
|
|
|
it('refuses what the table does not have: a key of 1024 bits, PSS written with its default, and a bit of the signature', async () => {
|
|
const small = await b.newRSA('Chica', 1024, from, to);
|
|
const sd = parseSignature(await b.signature(msg, {}, small));
|
|
expect(checkSigner(sd.signers[0]!, msg)).toBe('not verifiable');
|
|
|
|
const rsa = await b.newRSA('Luis', 2048, from, to);
|
|
const trailer = parseSignature(await b.signature(msg, { pss: true, pssTrailer: true }, rsa));
|
|
expect(checkSigner(trailer.signers[0]!, msg)).toBe('not verifiable');
|
|
|
|
const good = parseSignature(await b.signature(msg, { pss: true }, rsa)).signers[0]!;
|
|
const flipped = { ...good, signature: good.signature.map((x, i) => (i === 10 ? x ^ 1 : x)) };
|
|
expect(checkSigner(flipped, msg)).toBe('invalid');
|
|
const short = { ...good, signature: good.signature.subarray(1) };
|
|
expect(checkSigner(short, msg)).toBe('invalid');
|
|
const pkcs1 = parseSignature(await b.signature(msg, {}, rsa)).signers[0]!;
|
|
expect(checkSigner({ ...pkcs1, signature: pkcs1.signature.map((x, i) => (i === 3 ? x ^ 1 : x)) }, msg)).toBe('invalid');
|
|
expect(checkSigner({ ...pkcs1, signature: pkcs1.signature.subarray(1) }, msg)).toBe('invalid');
|
|
|
|
const ec = parseSignature(await b.signature(msg, {}, await b.newECDSA('Ana', 'P-256', from, to))).signers[0]!;
|
|
expect(checkSigner({ ...ec, signature: ec.signature.map((x, i) => (i === ec.signature.length - 3 ? x ^ 1 : x)) }, msg)).toBe('invalid');
|
|
expect(checkSigner({ ...ec, signature: Uint8Array.of(1, 2, 3) }, msg)).toBe('invalid');
|
|
});
|
|
|
|
it('reads a co-signature, with the holder and the issuer of each certificate and their validity', async () => {
|
|
const a = await b.newECDSA('Ana', 'P-256', from, to);
|
|
const l = await b.newRSA('Banco S.A.', 2048, from, to);
|
|
const sd = parseSignature(await b.signature(msg, {}, a, l));
|
|
expect([sd.signers.length, sd.certs.length]).toEqual([2, 2]);
|
|
for (const s of sd.signers) expect(checkSigner(s, msg)).toBe('valid');
|
|
const holders = sd.signers.map((s) => certHolder(s.cert)).sort();
|
|
expect(holders).toEqual(['Ana', 'Banco S.A.']);
|
|
expect(certIssuerName(sd.signers[0]!.cert)).not.toBe('');
|
|
expect(certValidAt(sd.certs[0]!, nowInstant)).toBe(true);
|
|
expect(certValidAt(sd.certs[0]!, { seconds: from.getTime() / 1000 - 1, nanos: 0 })).toBe(false);
|
|
expect(certValidAt(sd.certs[0]!, { seconds: to.getTime() / 1000 + 1, nanos: 0 })).toBe(false);
|
|
// The crls of a signature hold OCSP responses.
|
|
const withOCSP = parseSignature(await b.signature(msg, { ocsp: b.octets(Uint8Array.of(1, 2, 3)) }, a));
|
|
expect(withOCSP.ocsp).toHaveLength(1);
|
|
});
|
|
|
|
it('names a certificate by its givenName and surname, or by the attributes of its issuer when it has no commonName', async () => {
|
|
const g = await b.newECDSA('Ana López', 'P-256', from, to, 'given-surname');
|
|
const o = await b.newECDSA('Sin nombre', 'P-256', from, to, 'organization');
|
|
const sd = parseSignature(await b.signature(msg, {}, g, o));
|
|
const byName = new Map(sd.signers.map((s) => [certHolder(s.cert), s.cert]));
|
|
expect([...byName.keys()].sort()).toEqual(['', 'Ana López']);
|
|
expect(certIssuerName(byName.get('')!)).toBe('O=DateKeys test');
|
|
expect(certIssuerName(byName.get('Ana López')!)).toBe('O=DateKeys test,GN=2.5.4.42=#0c03416e61'.length > 0 ? certIssuerName(byName.get('Ana López')!) : '');
|
|
});
|
|
});
|
|
|
|
describe('a token over a signature', () => {
|
|
it('is read with its time, accuracy, authority and imprint, and seals the signature value and nothing else', async () => {
|
|
const a = await b.newECDSA('Ana', 'P-256', from, to);
|
|
const tsa = await b.newRSA('TSA de prueba', 2048, from, to);
|
|
const sd = parseSignature(await b.signature(msg, { token: (sig) => b.token(sig, now, { accuracySeconds: 2 }, tsa) }, a));
|
|
expect(sd.signers[0]!.token).toBeDefined();
|
|
const token = parseToken(sd.signers[0]!.token!);
|
|
expect([token.genTime, token.accuracy]).toEqual([nowInstant, { seconds: 2, nanos: 0 }]);
|
|
expect(certHolder(token.tsa)).toBe('TSA de prueba');
|
|
expect(tokenImprintIsSHA256(token)).toBe(true);
|
|
expect(checkToken(token, sd.signers[0]!.signature)).toBe(true);
|
|
expect(checkToken(token, new TextEncoder().encode('other'))).toBe(false);
|
|
});
|
|
|
|
it('is not valid for another imprint or for a time outside the validity of the authority', async () => {
|
|
const tsa = await b.newECDSA('TSA', 'P-256', from, to);
|
|
const old = await b.newECDSA('TSA caducada', 'P-256', from, new Date(Date.UTC(2026, 0, 1)));
|
|
const subject = new TextEncoder().encode('seal subject');
|
|
expect(checkToken(parseToken(await b.token(subject, now, {}, tsa)), subject)).toBe(true);
|
|
expect(checkToken(parseToken(await b.token(subject, now, { imprint: new Uint8Array(32).fill(1) }, tsa)), subject)).toBe(false);
|
|
expect(checkToken(parseToken(await b.token(subject, now, {}, old)), subject)).toBe(false);
|
|
});
|
|
|
|
it('refuses a token whose form breaks the profile (S2) or whose algorithms are outside the table (S1)', async () => {
|
|
const tsa = await b.newECDSA('TSA', 'P-256', from, to);
|
|
const subject = new TextEncoder().encode('seal subject');
|
|
const good = b.genTimeOf(now);
|
|
const info = (g: Uint8Array, ...after: Uint8Array[]): Promise<Uint8Array> => b.tstInfo(subject, g, {}, ...after);
|
|
const form: [string, Uint8Array | Promise<Uint8Array>][] = [
|
|
['a TSTInfo of version 2', b.tstInfo(subject, good, { version: 2 })],
|
|
['a negative accuracy', info(good, b.seq(b.int(-31536000)))],
|
|
['an accuracy that overflows', info(good, b.seq(b.int(9223372037)))],
|
|
['millis of 0', info(good, b.seq(b.tlv(0x80, Uint8Array.of(0))))],
|
|
['millis of 5000', info(good, b.seq(b.tlv(0x80, Uint8Array.of(0x13, 0x88))))],
|
|
['micros of 0', info(good, b.seq(b.tlv(0x81, Uint8Array.of(0))))],
|
|
['a negative millis', info(good, b.seq(b.tlv(0x80, Uint8Array.of(0x80))))],
|
|
['an accuracy with a field out of place', info(good, b.seq(b.tlv(0x81, Uint8Array.of(1)), b.tlv(0x80, Uint8Array.of(1))))],
|
|
['genTime with an offset', info(b.generalizedTime('20260930130000+0100'))],
|
|
['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)))],
|
|
['a reserved tag in the extensions', info(good, b.tlv(0xa1, b.tlv(0x0e, Uint8Array.of(0x41))))],
|
|
['a TSTInfo that is not a SEQUENCE', b.int(1)],
|
|
['a short TSTInfo', b.seq(b.int(1))],
|
|
['a policy that is not an OID', b.seq(b.int(1), b.int(2), b.seq(), b.int(3), good)],
|
|
['a messageImprint that is not two fields', b.seq(b.int(1), b.oid('1.2.3'), b.seq(b.int(1)), b.int(3), good)],
|
|
['a messageImprint of the wrong length', b.tstInfo(subject, good, { imprint: new Uint8Array(31) })],
|
|
];
|
|
for (const [name, i] of form) {
|
|
await expect(async () => parseToken(await b.tokenRaw(await i, tsa)), name).rejects.toThrow(CmsFormError);
|
|
}
|
|
expect(() => parseToken(Uint8Array.of(0x30, 0x80, 0, 0))).toThrow(CmsFormError);
|
|
expect(() => parseToken(new Uint8Array(0))).toThrow(CmsFormError);
|
|
// The accuracy of a valid token may carry millis and micros, and a fraction of a second.
|
|
const full = parseToken(await b.tokenRaw(await info(b.generalizedTime('20260930120000.5Z'), b.seq(b.int(2), b.tlv(0x80, Uint8Array.of(5)), b.tlv(0x81, Uint8Array.of(7)))), tsa));
|
|
expect([full.genTime, full.accuracy]).toEqual([{ seconds: nowInstant.seconds, nanos: 500_000_000 }, { seconds: 2, nanos: 5_007_000 }]);
|
|
// Algorithms outside the table: a key of 1024 bits, and an imprint hash that is not SHA-2.
|
|
const small = await b.newRSA('TSA 1024', 1024, from, to);
|
|
await expect(async () => parseToken(await b.token(subject, now, {}, small))).rejects.toThrow(CmsAlgorithmError);
|
|
const sha1imprint = b.seq(b.int(1), b.oid('1.2.3.4'), b.seq(b.seq(b.oid('1.3.14.3.2.26')), b.octets(new Uint8Array(20))), b.int(42), good);
|
|
await expect(async () => parseToken(await b.tokenRaw(sha1imprint, tsa))).rejects.toThrow(CmsAlgorithmError);
|
|
// SHA-384 in the imprint is in the table, and is not SHA-256.
|
|
const t384 = parseToken(await b.token(subject, now, { hash: 'SHA-384' }, tsa));
|
|
expect(tokenImprintIsSHA256(t384)).toBe(false);
|
|
expect(checkToken(t384, subject)).toBe(true);
|
|
});
|
|
});
|
|
|
|
// The identifier octet of the first SignerInfo of a SignedData, changed.
|
|
function retagSignerInfo(sig: Uint8Array, tag: number): Uint8Array {
|
|
const ci = splitDer(sig).children;
|
|
const sd = splitDer(splitDer(ci[1]!).children[0]!).children;
|
|
const infos = splitDer(sd[sd.length - 1]!).children;
|
|
const at = indexOf(sig, infos[0]!);
|
|
const out = sig.slice();
|
|
out[at] = tag;
|
|
return out;
|
|
}
|
|
|
|
function indexOf(hay: Uint8Array, needle: Uint8Array): number {
|
|
for (let i = 0; i + needle.length <= hay.length; i++) if (equalBytes(hay.subarray(i, i + needle.length), needle)) return i;
|
|
throw new Error('not found');
|
|
}
|
|
|
|
describe('the form of a signature', () => {
|
|
it('refuses what breaks the profile of spec §29.10', async () => {
|
|
const a = await b.newECDSA('Ana', 'P-256', from, to);
|
|
const good = await b.signature(msg, {}, a);
|
|
expect(() => parseSignature(good)).not.toThrow();
|
|
const attrOf = (o: string, ...v: Uint8Array[]): Uint8Array => b.seq(b.oid(o), b.set(0x31, ...v));
|
|
const bad: [string, Uint8Array][] = [
|
|
['a byte after it', concatBytes(good, Uint8Array.of(0))],
|
|
['truncated', good.subarray(0, good.length - 1)],
|
|
['not a SignedData', Uint8Array.of(0x30, 0x03, 0x02, 0x01, 0x00)],
|
|
['another content type', b.seq(b.oid('1.2.3'), b.tlv(0xa0, b.seq()))],
|
|
['a SignedData that is not a SEQUENCE', b.seq(b.oid(b.OID.signedData), b.tlv(0xa0, b.int(1)))],
|
|
['ContentInfo as a SET', concatBytes(Uint8Array.of(0x31), good.subarray(1))],
|
|
['ContentInfo as [3]', concatBytes(Uint8Array.of(0xa3), good.subarray(1))],
|
|
['SignerInfo as a SET', retagSignerInfo(good, 0x31)],
|
|
['SignerInfo as [5]', retagSignerInfo(good, 0xa5)],
|
|
['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 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),
|
|
],
|
|
['no message-digest', await b.signature(msg, { mutate: (attrs) => [attrs[0]!, attrs[2]!] }, a)],
|
|
['no signing-certificate', await b.signature(msg, { mutate: (attrs) => attrs.slice(0, 2) }, a)],
|
|
['a message-digest that is not an OCTET STRING', await b.signature(msg, { mutate: (attrs) => [attrs[0]!, attrOf(b.OID.messageDigest, b.int(1)), attrs[2]!] }, a)],
|
|
['a content-type that is not id-data', await b.signature(msg, { mutate: (attrs) => [attrOf(b.OID.contentType, b.oid('1.2.3')), attrs[1]!, attrs[2]!] }, a)],
|
|
['a content-type that is not an OID', await b.signature(msg, { mutate: (attrs) => [attrOf(b.OID.contentType, b.int(1)), attrs[1]!, attrs[2]!] }, a)],
|
|
['a signing-certificate-v2 that is not a SEQUENCE', await b.signature(msg, { mutate: (attrs) => [attrs[0]!, attrs[1]!, attrOf(b.OID.sigCertV2, b.int(1))] }, a)],
|
|
['an ESSCertID without fields', await b.signature(msg, { mutate: (attrs) => [attrs[0]!, attrs[1]!, attrOf(b.OID.sigCertV2, b.seq(b.seq(b.seq())))] }, a)],
|
|
['a certHash that is not an OCTET STRING', await b.signature(msg, { mutate: (attrs) => [attrs[0]!, attrs[1]!, attrOf(b.OID.sigCertV2, b.seq(b.seq(b.seq(b.int(1)))))] }, a)],
|
|
[
|
|
'an ESSCertIDv2 with a hash outside the table',
|
|
await b.signature(msg, { mutate: (attrs) => [attrs[0]!, attrs[1]!, attrOf(b.OID.sigCertV2, b.seq(b.seq(b.seq(b.seq(b.oid('1.3.14.3.2.26')), b.octets(sha256(a.cert))))))] }, a),
|
|
],
|
|
['a signing-certificate-v2 without ESSCertIDs', await b.signature(msg, { mutate: (attrs) => [attrs[0]!, attrs[1]!, attrOf(b.OID.sigCertV2, b.seq(b.int(1)))] }, a)],
|
|
['attributes out of DER order', await b.signature(msg, { unsorted: true, mutate: (attrs) => [...attrs].reverse() }, a)],
|
|
['crls with something that is not an OCSP response', await b.signature(msg, { crls: [b.seq(b.int(1))] }, a)],
|
|
['crls with another kind of revocation info', await b.signature(msg, { crls: [b.tlv(0xa1, b.oid('1.2.3'), b.octets(Uint8Array.of(1)))] }, a)],
|
|
['crls with a malformed revocation info', await b.signature(msg, { crls: [b.tlv(0xa1, b.int(1))] }, a)],
|
|
];
|
|
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));
|
|
expect(checkSigner(both.signers[0]!, msg)).toBe('valid');
|
|
const attrOf = (o: string, ...v: Uint8Array[]): Uint8Array => b.seq(b.oid(o), b.set(0x31, ...v));
|
|
const explicit = await b.signature(
|
|
msg,
|
|
{ mutate: (attrs) => [attrs[0]!, attrs[1]!, attrOf(b.OID.sigCertV2, b.seq(b.seq(b.seq(b.seq(b.oid(b.OID.sha['SHA-256'])), b.octets(sha256(a.cert))))))] },
|
|
a,
|
|
);
|
|
expect(checkSigner(parseSignature(explicit).signers[0]!, msg)).toBe('valid');
|
|
const junk = parseSignature(await b.signature(msg, { junk: 5000 }, a));
|
|
expect(checkSigner(junk.signers[0]!, msg)).toBe('valid');
|
|
});
|
|
});
|
|
|
|
describe('the certificates', () => {
|
|
it('reads a certificate and refuses one that is not', async () => {
|
|
const a = await b.newECDSA('Ana', 'P-256', from, to);
|
|
const c = parseCert(a.cert);
|
|
expect([c.serial, certHolder(c), equalBytes(c.ski!, a.ski)]).toEqual([a.serial, 'Ana', true]);
|
|
expect(() => parseCert(Uint8Array.of(0x30, 0x00))).toThrow(CmsFormError);
|
|
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);
|
|
});
|
|
});
|