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/cms.test.ts

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

Powered by TurnKey Linux.