Follow the draft v0.12: testdata at 601e6d2, the reader of certificates, the texts of the verdicts

testdata is synced with the head of the branch v0.12 of datekeys-go
(601e6d2); SPEC_VERSION stays 0.11 until the author approves the draft.

- CMS as Go reads it at the draft v0.12 (review of 2 October, T2, T6 to T8
  and the TypeScript side of E7 to E9):
  - the certificate field by field with the profile of section 29.10, its
    errors in CertificateError with the texts of Go; one that breaks it
    decides nothing unless a SignerInfo names it, and two copies are one;
  - the text of a name only from UTF8String, PrintableString, IA5String,
    TeletexString in ASCII and BMPString without surrogates, never from an
    attribute that appears twice; the holder by givenName and surname
    before the commonName, the issuer by its commonName or its
    organizationName;
  - object identifiers by the bytes of their DER; a SET OF may repeat an
    element; RSA with NULL parameters and an odd modulus; a key of another
    scheme than its algorithm is invalid; a messageImprint of another
    length is S3; the crls of a token decide nothing;
  - DER: UTCTime and GeneralizedTime in their X.690 forms with a date that
    exists, the restricted string types as primitive, the accuracy as
    minimal INTEGERs. The test of cms.test.ts that compared a function
    with itself has an expected value of its own.
- The verdicts in the texts of the draft: names between « and », shown
  with at most 64 code points and no two spaces in a row, or their SHA-256;
  in F6 the authority of each seal and the warning that nobody checks who
  issued it; foreign signers in Spanish; times with their fraction.
- security.json in its context with lines, and the 135 cases of
  security_cms.json, compared field by field, lines included.
- The 218 cases of mutations.json, with the texts of capsule.Open
  regenerated by scripts/mutation-go-texts.go, and ibe-vectors.json with
  the fixtures format3_note, format3_unsigned and the new
  format3_seal_unsupported; its frozen values do not change.
- note.json, run with checkNoteData, publicNote and unusableNote.
- inspect reads the public note on demand, only for a header with one, so
  that the Unicode tables never load with /inspect; the view of inspect
  -json gives public_note and public_note_unusable, as Go.

A Go/TypeScript differential of 63,623 security areas, made from the
vectors, edited element by element and signed afresh with varied
certificates, tokens and authorities, gave no difference in verdicts,
results or lines; the code before this change differed in 13,296 of the
first 42,986.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main
dev 2 days ago
parent 997f3318c8
commit 95329eef4a

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -1,11 +1,14 @@
// Tests of der.ts, the strict check of DER that the CMS reader starts with: the // Tests of der.ts, the strict check of DER that the CMS reader starts with: the
// same cases as the Go package internal/der. // same cases as the Go package internal/der at the draft v0.12, the forms of
// the times and the restricted string types among them.
import { describe, expect, it } from 'vitest'; import { describe, expect, it } from 'vitest';
import { checkDer, derContent, DerError, setOfSorted, splitDer } from './der.ts'; import { checkDer, derContent, DerError, parseTime, setOfSorted, splitDer } from './der.ts';
import { h } from './testing/testdata.ts'; import { h } from './testing/testdata.ts';
const zeros = (n: number): string => '00'.repeat(n); const zeros = (n: number): string => '00'.repeat(n);
// The hexadecimal of the bytes of an ASCII text.
const hexOf = (s: string): string => [...s].map((c) => c.charCodeAt(0).toString(16).padStart(2, '0')).join('');
// n SEQUENCEs, each holding the next, as hex; the innermost holds `inner`, or nothing. // n SEQUENCEs, each holding the next, as hex; the innermost holds `inner`, or nothing.
function nested(n: number, inner = ''): string { function nested(n: number, inner = ''): string {
@ -20,39 +23,110 @@ describe('checkDer', () => {
['a long length', '04' + '8180' + zeros(128), true], ['a long length', '04' + '8180' + zeros(128), true],
['a long form under 128', '0481' + '01' + '00', false], ['a long form under 128', '0481' + '01' + '00', false],
['a length with a leading zero', '04820001' + '00', false], ['a length with a leading zero', '04820001' + '00', false],
['a length of 128 with a leading zero', '04820080' + zeros(128), false],
['an indefinite length', '30800000', false], ['an indefinite length', '30800000', false],
['a length of 0xff', '04ff' + zeros(127), false],
['a length of five bytes', '0485' + '0100000000' + '00', false],
['a length of nine bytes that wraps around to 128', '0489' + '010000000000000080' + zeros(128), false],
['a length whose bytes are missing', '0482' + '01', false],
['a lone identifier octet', '04', false],
['nothing', '', false],
['a high tag number', '1f0100', false], ['a high tag number', '1f0100', false],
['a length of 0xff', '04ff', false],
['a length that does not fit', '0485010000', false],
['truncated', '0402aa', false], ['truncated', '0402aa', false],
['trailing bytes', '0500' + '00', false], ['trailing bytes', '0500' + '00', false],
['BOOLEAN 01', '010101', false], ['BOOLEAN 01', '010101', false],
['BOOLEAN FF', '0101ff', true],
['BOOLEAN 00', '010100', true], ['BOOLEAN 00', '010100', true],
['BOOLEAN FF', '0101ff', true],
['BOOLEAN of two bytes', '0102ffff', false],
['INTEGER with a leading zero', '02020001', false], ['INTEGER with a leading zero', '02020001', false],
['INTEGER 0x80 with its zero', '02020080', true], ['INTEGER 0x80 with its zero', '02020080', true],
['INTEGER with a leading FF', '0202ff80', false], ['INTEGER with a leading FF', '0202ff80', false],
['INTEGER -1 in two bytes', '0202ffff', false],
['INTEGER -129', '0202ff7f', true],
['empty INTEGER', '0200', false], ['empty INTEGER', '0200', false],
['ENUMERATED', '0a0101', true], ['ENUMERATED', '0a0101', true],
['ENUMERATED with a leading zero', '0a020001', false],
['NULL with content', '050100', false], ['NULL with content', '050100', false],
['constructed OCTET STRING', '2404' + '0402aabb', false], ['constructed OCTET STRING', '2404' + '0402aabb', false],
['constructed INTEGER', '2203' + '020101', false],
['BIT STRING with unused bits set', '03020701', false], ['BIT STRING with unused bits set', '03020701', false],
['BIT STRING with unused bits clear', '03020780', true], ['BIT STRING with unused bits clear', '03020780', true],
['BIT STRING of 4 bits', '030204f0', true], ['BIT STRING of 4 bits', '030204f0', true],
['an empty BIT STRING', '0300', false], ['an empty BIT STRING', '030100', true],
['a BIT STRING of only unused bits', '030105', false], ['a BIT STRING without its first octet', '0300', false],
['BIT STRING with more than 7 unused bits', '03020800', false], ['a BIT STRING of eight unused bits', '03020800', false],
['a BIT STRING of no bits with unused bits', '030101', false],
['OID', '06032a0304', true], ['OID', '06032a0304', true],
['OID with a leading 0x80', '0603800102', false], ['OID with 0x80 inside a subidentifier', '0604' + '2a818001', true],
['OID with a leading 0x80', '06038001' + '02', false],
['OID that does not end', '06022a83', false], ['OID that does not end', '06022a83', false],
['an empty OID', '0600', false], ['an empty OID', '0600', false],
['context tag, constructed', 'a003020101', true], ['context tag, constructed', 'a003020101', true],
['context tag, primitive, any content', '8003000000', true],
['SET in primitive form', '1100', false], ['SET in primitive form', '1100', false],
['SEQUENCE in primitive form', '1000', false], ['SEQUENCE in primitive form', '1000', false],
['the end of contents', '0000', false], ['the end of contents', '0000', false],
['a reserved universal tag', '0e0141', false], ['a reserved universal tag', '0e0141', false],
['a SEQUENCE of the end of contents', '30020000', false], ['a SEQUENCE of the end of contents', '30020000', false],
['a SEQUENCE with a bad child', '3003' + '020000', false],
['a SEQUENCE whose child does not fit', '3003' + '040500', false],
['UTF8String', '0c026162', true], ['UTF8String', '0c026162', true],
['UTF8String that is not UTF-8', '0c01ff', true],
['PrintableString with an underscore', '13015f', true],
['TeletexString', '1401e9', true],
['IA5String', '160161', true],
['VisibleString', '1a0161', true],
['UniversalString', '1c0400000061', true],
['BMPString', '1e020061', true],
// The other string types are DER too, whatever their content: a name may
// hold a NumericString, as the INN of a Russian certificate.
['NumericString', '1204' + hexOf('1234'), true],
['NumericString with a letter', '1201' + hexOf('A'), true],
['VideotexString', '150141', true],
['GraphicString', '190141', true],
['GeneralString', '1b0141', true],
['ObjectDescriptor', '070141', true],
['NumericString in constructed form', '3203' + '120131', false],
['REAL', '0900', false],
['RELATIVE-OID', '0d0101', false],
// Times in the forms of DER (X.690 11.7, 11.8).
['UTCTime', '170d' + hexOf('250101120000Z'), true],
['an empty UTCTime', '1700', false],
['UTCTime without seconds', '170b' + hexOf('2501011200Z'), false],
['UTCTime with a digit more', '170e' + hexOf('2501011200001Z'), false],
['UTCTime with an offset', '1711' + hexOf('250101120000+0100'), false],
['UTCTime of 30 February', '170d' + hexOf('250230120000Z'), false],
['UTCTime with second 60', '170d' + hexOf('250101235960Z'), false],
['a UTCTime that is not a time', '170a' + hexOf('not a time'), false],
['UTCTime with a slash in its seconds', '170d' + hexOf('2501011200/0Z'), false],
['UTCTime with a colon in its day', '170d' + hexOf('25010:120000Z'), false],
['GeneralizedTime', '180f' + hexOf('20250101120000Z'), true],
['GeneralizedTime with a fraction', '1812' + hexOf('20250101120000.25Z'), true],
['GeneralizedTime with a trailing zero', '1813' + hexOf('20250101120000.250Z'), false],
['GeneralizedTime with an empty fraction', '1810' + hexOf('20250101120000.Z'), false],
['GeneralizedTime with a letter in its fraction', '1812' + hexOf('20250101120000.2aZ'), false],
['GeneralizedTime without Z', '180e' + hexOf('20250101120000'), false],
['GeneralizedTime with a comma', '1812' + hexOf('20250101120000,25Z'), false],
['GeneralizedTime without seconds', '180d' + hexOf('202501011200Z'), false],
['GeneralizedTime with a slash in its seconds', '180f' + hexOf('202501011200/0Z'), false],
['GeneralizedTime of month 0', '180f' + hexOf('20250001120000Z'), false],
['GeneralizedTime of month 13', '180f' + hexOf('20251301120000Z'), false],
['GeneralizedTime of day 0', '180f' + hexOf('20250100120000Z'), false],
['GeneralizedTime of 31 April', '180f' + hexOf('20250431120000Z'), false],
['GeneralizedTime of hour 24', '180f' + hexOf('20250101240000Z'), false],
['GeneralizedTime of minute 60', '180f' + hexOf('20250101126000Z'), false],
['GeneralizedTime of 29 February 2024', '180f' + hexOf('20240229235959Z'), true],
// What a check that is missing would let through: the last byte read as Z,
// a colon read as the digit 10, a slash read as a year, a tag of a high
// number read as one byte, and an indefinite length read as a long form.
['UTCTime that ends in another letter', '170d' + hexOf('250101120000X'), false],
['GeneralizedTime with a digit in the place of Z', '180f' + hexOf('202501011200000'), false],
['GeneralizedTime with a colon in its day', '180f' + hexOf('2025010:120000Z'), false],
['GeneralizedTime with a slash in its year', '180f' + hexOf('/0250101120000Z'), false],
['a context tag of a high number', '9f0100', false],
['an indefinite length and nothing after it', '3080', false],
// Go reads the bytes of a time: a leading U+FEFF is no digit.
['UTCTime after a byte order mark', '1710' + 'efbbbf' + hexOf('250101120000Z'), false],
['33 SEQUENCEs, each holding the next', nested(33), true], ['33 SEQUENCEs, each holding the next', nested(33), true],
['34 SEQUENCEs, each holding the next', nested(34), false], ['34 SEQUENCEs, each holding the next', nested(34), false],
]; ];
@ -70,15 +144,60 @@ describe('checkDer', () => {
expect(() => checkDer(h(nested(32, '0500')))).not.toThrow(); expect(() => checkDer(h(nested(32, '0500')))).not.toThrow();
expect(() => checkDer(h(nested(33, '0500')))).toThrow('der: nested too deep'); expect(() => checkDer(h(nested(33, '0500')))).toThrow('der: nested too deep');
}); });
it('says what is wrong, with the texts of Go', () => {
expect(() => checkDer(h('170d' + hexOf('250230120000Z')))).toThrow(new DerError('a date or a time that does not exist'));
expect(() => checkDer(h('170b' + hexOf('2501011200Z')))).toThrow(new DerError('a time that is not in the form of DER'));
expect(() => checkDer(h('0900'))).toThrow(new DerError('the universal type 9, which the profile does not use'));
expect(() => checkDer(h('0500' + '00'))).toThrow(new DerError('1 bytes after the element'));
});
});
describe('parseTime', () => {
const at = (y: number, mo: number, d: number, hh: number, mm: number, ss: number, nanos = 0): { seconds: number; nanos: number } => {
const t = new Date(0);
t.setUTCFullYear(y, mo - 1, d);
t.setUTCHours(hh, mm, ss);
return { seconds: t.getTime() / 1000, nanos };
};
// The years 50 to 99 of a UTCTime are of the 20th century (RFC 5280 4.1.2.5.1), and a fraction has up to nine digits that count.
it.each([
['170d' + hexOf('491231235959Z'), at(2049, 12, 31, 23, 59, 59), false],
['170d' + hexOf('500101000000Z'), at(1950, 1, 1, 0, 0, 0), false],
['1813' + hexOf('20240229120000.125Z'), at(2024, 2, 29, 12, 0, 0, 125_000_000), true],
['1819' + hexOf('20240229120000.123456789Z'), at(2024, 2, 29, 12, 0, 0, 123_456_789), true],
['181b' + hexOf('20240229120000.12345678912Z'), at(2024, 2, 29, 12, 0, 0, 123_456_789), true],
['180f' + hexOf('19490101000000Z'), at(1949, 1, 1, 0, 0, 0), false],
['180f' + hexOf('00000229000000Z'), at(0, 2, 29, 0, 0, 0), false],
['180f' + hexOf('99991231235959Z'), at(9999, 12, 31, 23, 59, 59), false],
] as const)('%s', (hex, time, fraction) => {
expect(parseTime(h(hex))).toEqual({ time, fraction });
});
it.each([
['a date that does not exist', '180f' + hexOf('20230229120000Z')],
['an OCTET STRING', '040d' + hexOf('491231235959Z')],
['a GeneralizedTime in an OCTET STRING', '040f' + hexOf('20230228120000Z')],
['a UTCTime in a UTF8String', '0c0d' + hexOf('491231235959Z')],
['a lone identifier', '17'],
['a content that does not fit', '170d' + hexOf('4912')],
['nothing', ''],
])('refuses %s', (_name, hex) => {
expect(() => parseTime(h(hex))).toThrow(DerError);
});
}); });
describe('setOfSorted, splitDer and derContent', () => { describe('setOfSorted, splitDer and derContent', () => {
it('knows the order of the elements of a SET OF', () => { // The elements of a SET OF go in ascending order, and equal ones may repeat (X.690 11.6).
it('knows the order of the elements of a SET OF, which may repeat one', () => {
const a = h('020101'); const a = h('020101');
const b = h('020102'); const b = h('020102');
expect(setOfSorted([a, b])).toBe(true); expect(setOfSorted([a, b])).toBe(true);
expect(setOfSorted([b, a])).toBe(false); expect(setOfSorted([b, a])).toBe(false);
expect(setOfSorted([a, a])).toBe(false); expect(setOfSorted([a, a])).toBe(true);
expect(setOfSorted([a, a, b])).toBe(true);
expect(setOfSorted([a, b, a])).toBe(false);
expect(setOfSorted([h('0201'), a])).toBe(true);
expect(setOfSorted([])).toBe(true); expect(setOfSorted([])).toBe(true);
}); });
@ -87,8 +206,12 @@ describe('setOfSorted, splitDer and derContent', () => {
checkDer(b); checkDer(b);
const { id, children } = splitDer(b); const { id, children } = splitDer(b);
expect([id, children.map((c) => c.length)]).toEqual([0x30, [3, 2]]); expect([id, children.map((c) => c.length)]).toEqual([0x30, [3, 2]]);
expect(derContent(b)).toEqual(b.subarray(2));
expect(derContent(children[0]!)).toEqual(h('01')); expect(derContent(children[0]!)).toEqual(h('01'));
// Nothing, a primitive element, and constructed ones whose content does not fit.
for (const bad of ['', '0400', '3005', 'a005', '3003040500']) expect(() => splitDer(h(bad)), bad).toThrow(DerError);
expect(() => splitDer(h('020101'))).toThrow(/not a constructed/); expect(() => splitDer(h('020101'))).toThrow(/not a constructed/);
expect(() => splitDer(new Uint8Array(0))).toThrow(DerError); expect(() => derContent(h('040500'))).toThrow(DerError);
expect(() => derContent(h('04'))).toThrow(DerError);
}); });
}); });

@ -1,14 +1,16 @@
// A strict check that bytes are one element in the Distinguished Encoding // A strict check that bytes are one element in the Distinguished Encoding
// Rules of X.690, as the Go package internal/der does it, which spec v0.11 // Rules of X.690, as the Go package internal/der does it, which spec v0.12
// §29.10 asks of a CMS signature before anyone looks inside it: definite and // §29.10 asks of a CMS signature before anyone looks inside it: definite and
// minimal lengths, no high tag numbers, no constructed form of a type that DER // minimal lengths, no high tag numbers, no constructed form of a type that DER
// only has primitive, canonical BOOLEAN, INTEGER, NULL, OBJECT IDENTIFIER and // only has primitive, canonical BOOLEAN, INTEGER, NULL, OBJECT IDENTIFIER, BIT
// BIT STRING, only the universal types that certificates, signatures and // STRING, UTCTime and GeneralizedTime, only the universal types that
// tokens use, and no bytes after the element. The order of the elements of a // certificates, signatures and tokens use, and no bytes after the element. The
// SET OF cannot be checked without a schema: setOfSorted does it for the // order of the elements of a SET OF cannot be checked without a schema:
// callers that know theirs. Internal: index.ts does not re-export it. // setOfSorted does it for the callers that know theirs. Internal: index.ts
// does not re-export it.
import { compareBytes } from './bytes.ts'; import { compareBytes } from './bytes.ts';
import type { Instant } from './datekey.ts';
/** A byte string that is not DER; the message says what is wrong. */ /** A byte string that is not DER; the message says what is wrong. */
export class DerError extends Error { export class DerError extends Error {
@ -50,17 +52,85 @@ export function derContent(b: Uint8Array): Uint8Array {
} }
/** /**
* Whether the encodings are in ascending order of their bytes, as DER * Whether the encodings are in ascending order of their bytes, as DER requires
* requires of the elements of a SET OF (X.690 11.6), and without repetitions: a * of the elements of a SET OF (X.690 11.6). Equal elements may repeat, side by
* SET OF of this profile has none. * side: X.690 does not forbid it, and a time-stamping authority may send its
* certificate twice. Whoever counts the elements of a SET OF decides what a
* repetition means.
*/ */
export function setOfSorted(elems: readonly Uint8Array[]): boolean { export function setOfSorted(elems: readonly Uint8Array[]): boolean {
for (let i = 1; i < elems.length; i++) { for (let i = 1; i < elems.length; i++) {
if (compareBytes(elems[i - 1]!, elems[i]!) >= 0) return false; if (compareBytes(elems[i - 1]!, elems[i]!) > 0) return false;
} }
return true; return true;
} }
/**
* Reads a UTCTime or a GeneralizedTime element as DER writes them (X.690 11.7
* and 11.8): YYMMDDHHMMSSZ, with the years 50 to 99 in the 20th century (RFC
* 5280 4.1.2.5.1), or YYYYMMDDHHMMSS, an optional fraction of seconds without
* a trailing zero, and Z. A date or a time that does not exist, a second 60
* included, is refused. `fraction` tells whether a GeneralizedTime has one.
*/
export function parseTime(b: Uint8Array): { time: Instant; fraction: boolean } {
if (b.length < 2 || (b[0] !== 0x17 && b[0] !== 0x18)) throw new DerError('not a UTCTime or a GeneralizedTime');
return timeOf(b[0], derContent(b));
}
// The time of the content c of a UTCTime (tag 0x17) or a GeneralizedTime,
// read from its bytes as Go reads them: no byte decodes to another.
function timeOf(tag: number, c: Uint8Array): { time: Instant; fraction: boolean } {
const bad = (): never => {
throw new DerError('a time that is not in the form of DER');
};
if (c.length < 13 || c[c.length - 1] !== 0x5a) bad(); // Z
let body = c.subarray(0, c.length - 1);
let frac = body.subarray(0, 0);
let year: number;
if (tag === 0x17) {
if (body.length !== 12 || !digits(body)) bad();
year = number(body.subarray(0, 2)) + 1900;
if (year < 1950) year += 100;
body = body.subarray(2);
} else {
const dot = body.indexOf(0x2e); // .
if (dot >= 0) {
frac = body.subarray(dot + 1);
body = body.subarray(0, dot);
if (frac.length === 0 || frac[frac.length - 1] === 0x30 || !digits(frac)) bad();
}
if (body.length !== 14 || !digits(body)) bad();
year = number(body.subarray(0, 4));
body = body.subarray(4);
}
const [month, day, hour, minute, second] = [0, 2, 4, 6, 8].map((i) => number(body.subarray(i, i + 2))) as [number, number, number, number, number];
if (month < 1 || month > 12 || day < 1 || hour > 23 || minute > 59 || second > 59 || day > daysIn(year, month)) {
throw new DerError('a date or a time that does not exist');
}
let nanos = 0;
for (let i = 0; i < 9; i++) nanos = nanos * 10 + (i < frac.length ? frac[i]! - 0x30 : 0);
// setUTCFullYear, unlike Date.UTC, keeps the years 0 to 99 as they are.
const d = new Date(0);
d.setUTCFullYear(year, month - 1, day);
return { time: { seconds: d.getTime() / 1000 + hour * 3600 + minute * 60 + second, nanos }, fraction: frac.length > 0 };
}
function digits(b: Uint8Array): boolean {
return b.length > 0 && b.every((x) => x >= 0x30 && x <= 0x39);
}
function number(b: Uint8Array): number {
let n = 0;
for (const x of b) n = n * 10 + x - 0x30;
return n;
}
// The days of a month of the proleptic Gregorian calendar, as Go's time.Date.
function daysIn(year: number, month: number): number {
if (month === 2) return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0) ? 29 : 28;
return month === 4 || month === 6 || month === 9 || month === 11 ? 30 : 31;
}
// The length of the identifier and length octets of the element at the start // The length of the identifier and length octets of the element at the start
// of b, and the length of its content, which must fit in b. // of b, and the length of its content, which must fit in b.
function header(b: Uint8Array): { headerLen: number; contentLen: number } { function header(b: Uint8Array): { headerLen: number; contentLen: number } {
@ -142,23 +212,37 @@ function checkPrimitive(tag: number, c: Uint8Array): void {
} }
return; return;
} }
case 4: // OCTET STRING and the string and time types of X.509 case 23: // UTCTime
case 24: // GeneralizedTime
timeOf(tag === 23 ? 0x17 : 0x18, c);
return;
case 4:
case 7:
case 12: case 12:
case 18:
case 19: case 19:
case 20: case 20:
case 21:
case 22: case 22:
case 23: case 25:
case 24:
case 26: case 26:
case 27:
case 28: case 28:
case 30: case 30:
// OCTET STRING, ObjectDescriptor and the restricted character string
// types: DER writes them primitive (X.690 10.2), with their content as
// it is. A name may hold any of them, a NumericString among them, as the
// certificates of some countries do: what is not text decides at the
// text of the name (spec §29.10), not here.
return; return;
case 16: case 16:
case 17: case 17:
throw new DerError(`the universal type ${tag} in primitive form`); throw new DerError(`the universal type ${tag} in primitive form`);
default: default:
// 0 is the end of contents of BER, and the rest are types that no // 0 is the end of contents of BER, and the rest are types that no
// certificate, signature or token of the profile has. // certificate, signature or token of the profile has: REAL,
// RELATIVE-OID, TIME and the reserved tags, whose DER has rules of its
// own that this module does not check.
throw new DerError(`the universal type ${tag}, which the profile does not use`); throw new DerError(`the universal type ${tag}, which the profile does not use`);
} }
} }

@ -77,6 +77,25 @@ describe('inspect', () => {
expect(Object.keys(inspectView(r))).toEqual(['format', 'capsule_id', 'datekey', 'profile', 'round', 'unlock_at', 'access_policy', 'valid', 'checks']); expect(Object.keys(inspectView(r))).toEqual(['format', 'capsule_id', 'datekey', 'profile', 'round', 'unlock_at', 'access_policy', 'valid', 'checks']);
}); });
// Go's inspectview: the public note when it is usable, and public_note_unusable for one that breaks the rules of
// text, which is not shown (spec §24.1). inspect loads those rules on demand, only for a header with a note.
it('gives the public note in the view, or says that it is unusable', async () => {
const dkc = readBytes('fixtures/format3_note.dkc');
const r = await inspect(dkc);
expect([r.error, r.publicNote, r.unusableNote]).toEqual([undefined, 'Cartas del viaje a Lisboa', false]);
const view = inspectView(r);
expect(view.public_note).toBe('Cartas del viaje a Lisboa');
expect(Object.keys(view)).toEqual(['format', 'capsule_id', 'datekey', 'profile', 'round', 'unlock_at', 'access_policy', 'public_note', 'valid', 'checks']);
// A tab for the first space: steps 1 to 8 do not read the data of a noncritical extension that nobody registers,
// so the capsule passes them, and the note does not show.
const tabbed = await inspect(replaceText(dkc, 'Cartas del', 'Cartas\tdel'));
expect([tabbed.error, tabbed.publicNote, tabbed.unusableNote]).toEqual([undefined, undefined, true]);
expect(Object.keys(inspectView(tabbed))).toEqual(['format', 'capsule_id', 'datekey', 'profile', 'round', 'unlock_at', 'access_policy', 'public_note_unusable', 'valid', 'checks']);
// Without a note there is nothing to load, and inspectWith alone reads none.
expect((await inspect(timeOnly)).unusableNote).toBeUndefined();
expect(inspectWith(dkc, await defaultRegistry()).publicNote).toBeUndefined();
});
it('fails the framing steps as the reference', () => { it('fails the framing steps as the reference', () => {
const cases: [Uint8Array, number, string, RegExp][] = [ const cases: [Uint8Array, number, string, RegExp][] = [
[new Uint8Array(0), 1, 'ERR_INVALID_MAGIC', /^capsule: ERR_INVALID_MAGIC$/], [new Uint8Array(0), 1, 'ERR_INVALID_MAGIC', /^capsule: ERR_INVALID_MAGIC$/],

@ -10,7 +10,7 @@ import { ageStanzas, checkPayloadStanzas, checkTimeStanzas, type Stanza, STANZA_
import { copyBytes, goQuote, toHex } from './bytes.ts'; import { copyBytes, goQuote, toHex } from './bytes.ts';
import { compactDateKey, formatRFC3339, type Instant, roundTime, validateDateKey } from './datekey.ts'; import { compactDateKey, formatRFC3339, type Instant, roundTime, validateDateKey } from './datekey.ts';
import { DateKeysError } from './errors.ts'; import { DateKeysError } from './errors.ts';
import { checkCritical, checkNoncritical, type ExtensionRegistry, type Unusable } from './extension.ts'; import { checkCritical, checkNoncritical, type ExtensionRegistry, NOTE_ID, type Unusable } from './extension.ts';
import { type CapsuleSections, FramingError, payloadOffset, type Prelude, splitCapsule } from './framing.ts'; import { type CapsuleSections, FramingError, payloadOffset, type Prelude, splitCapsule } from './framing.ts';
import { decodeHeader, type Header, policyName } from './header.ts'; import { decodeHeader, type Header, policyName } from './header.ts';
import { chainHashHex, defaultRegistry, type Profile, type ProfileRegistry } from './profile.ts'; import { chainHashHex, defaultRegistry, type Profile, type ProfileRegistry } from './profile.ts';
@ -65,6 +65,13 @@ export interface Inspection {
* registry rejects. The capsule stays valid (spec §54). * registry rejects. The capsule stays valid (spec §54).
*/ */
readonly unusableExtensions: readonly Unusable[]; readonly unusableExtensions: readonly Unusable[];
/**
* The public note of PUBLIC_HEADER (spec §24.1), text of the creator that
* nobody has checked, when it has one that is usable; set by inspect.
*/
readonly publicNote?: string;
/** True when PUBLIC_HEADER holds a public note that breaks the rules of text, which is not shown (§24.1); set by inspect. */
readonly unusableNote?: boolean;
readonly checks: readonly CheckResult[]; readonly checks: readonly CheckResult[];
/** The failure, or undefined when steps 1 to 8 pass. */ /** The failure, or undefined when steps 1 to 8 pass. */
readonly error?: DateKeysError; readonly error?: DateKeysError;
@ -235,7 +242,19 @@ function asDateKeysError(err: unknown): DateKeysError {
*/ */
export async function inspect(dkc: Uint8Array, opts: InspectOptions = {}): Promise<Inspection> { export async function inspect(dkc: Uint8Array, opts: InspectOptions = {}): Promise<Inspection> {
const registry = opts.registry ?? (await defaultRegistry()); const registry = opts.registry ?? (await defaultRegistry());
return inspectWith(dkc, registry, opts.extensions); return withNote(inspectWith(dkc, registry, opts.extensions));
}
// The public note of the header, read with the rules of text (note.ts),
// whose Unicode tables load on demand: only for a header that has one, so
// that a page without notes never loads them.
async function withNote(in_: Inspection): Promise<Inspection> {
const noncritical = in_.header?.noncritical ?? [];
if (!noncritical.some((x) => x.id === NOTE_ID && x.version === 1)) return in_;
const { publicNote } = await import('./note.ts');
const note = publicNote(noncritical);
// There is a note: without its text, it is unusable (Go's UnusableNote).
return note === undefined ? { ...in_, unusableNote: true } : { ...in_, publicNote: note, unusableNote: false };
} }
/** The JSON view of `datekeys inspect -json` (Go cmd/datekeys inspectView). */ /** The JSON view of `datekeys inspect -json` (Go cmd/datekeys inspectView). */
@ -249,6 +268,10 @@ export interface InspectView {
round?: number; round?: number;
unlock_at?: string; unlock_at?: string;
access_policy?: string; access_policy?: string;
/** The public note, text of the creator that nobody has checked (spec §24.1). */
public_note?: string;
/** True for a public note that breaks the rules of text, which is not shown. */
public_note_unusable?: boolean;
valid: boolean; valid: boolean;
error?: string; error?: string;
checks: CheckResult[]; checks: CheckResult[];
@ -272,6 +295,8 @@ export function inspectView(in_: Inspection, file?: string): InspectView {
v.profile = h.dateKey.profileId; v.profile = h.dateKey.profileId;
v.round = h.dateKey.round; v.round = h.dateKey.round;
v.access_policy = policyName(h.policy); v.access_policy = policyName(h.policy);
if (in_.publicNote !== undefined) v.public_note = in_.publicNote;
if (in_.unusableNote === true) v.public_note_unusable = true;
} }
if (in_.unlockAt !== undefined) v.unlock_at = formatRFC3339(in_.unlockAt); if (in_.unlockAt !== undefined) v.unlock_at = formatRFC3339(in_.unlockAt);
return orderView(v); return orderView(v);
@ -279,7 +304,21 @@ export function inspectView(in_: Inspection, file?: string): InspectView {
// The key order of the Go struct, so that JSON.stringify matches the CLI. // The key order of the Go struct, so that JSON.stringify matches the CLI.
function orderView(v: InspectView): InspectView { function orderView(v: InspectView): InspectView {
const keys: (keyof InspectView)[] = ['file', 'format', 'capsule_id', 'datekey', 'profile', 'round', 'unlock_at', 'access_policy', 'valid', 'error', 'checks']; const keys: (keyof InspectView)[] = [
'file',
'format',
'capsule_id',
'datekey',
'profile',
'round',
'unlock_at',
'access_policy',
'public_note',
'public_note_unusable',
'valid',
'error',
'checks',
];
const o: Record<string, unknown> = {}; const o: Record<string, unknown> = {};
for (const k of keys) if (v[k] !== undefined) o[k] = v[k]; for (const k of keys) if (v[k] !== undefined) o[k] = v[k];
return o as unknown as InspectView; return o as unknown as InspectView;

@ -1,15 +1,17 @@
// Faults of the evaluation of the security area that no input reaches today: // 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 // the decoder, the strict verification of alg 1, the evaluators of alg 2 and
// and of the seal are replaced by functions that throw, in a file of its own // of the seal and the reader of a token are replaced by functions that throw,
// because vi.mock replaces a module for the whole file. evaluateSecurity never // in a file of its own because vi.mock replaces a module for the whole file.
// throws, since the security area never decides the opening (spec §29.3): a // evaluateSecurity never throws, since the security area never decides the
// fault while it evaluates the signature gives F1, and one while it evaluates // opening (spec §29.3): a fault while it evaluates the signature gives F1, and
// the seal, S2, each without touching the other verdict; one while it decodes // one while it evaluates the seal, S2, each without touching the other
// the area gives X. The Go reference does the same from v0.12. // 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 { afterEach, describe, expect, it, vi } from 'vitest';
import { controlCommit, headDigest } from './author.ts'; import { controlCommit, headDigest } from './author.ts';
import { peek } from './cbor.ts'; import { peek } from './cbor.ts';
import { parseToken } from './cms.ts';
import { decodeControl } from './control.ts'; import { decodeControl } from './control.ts';
import { parseRFC3339 } from './datekey.ts'; import { parseRFC3339 } from './datekey.ts';
import { verifyStrict } from './ed25519strict.ts'; import { verifyStrict } from './ed25519strict.ts';
@ -30,6 +32,10 @@ vi.mock('./securitycms.ts', async (importOriginal) => {
const m = await importOriginal<typeof import('./securitycms.ts')>(); const m = await importOriginal<typeof import('./securitycms.ts')>();
return { ...m, evaluateCMS: vi.fn(m.evaluateCMS), evaluateSeal: vi.fn(m.evaluateSeal) }; return { ...m, evaluateCMS: vi.fn(m.evaluateCMS), evaluateSeal: vi.fn(m.evaluateSeal) };
}); });
vi.mock('./cms.ts', async (importOriginal) => {
const m = await importOriginal<typeof import('./cms.ts')>();
return { ...m, parseToken: vi.fn(m.parseToken) };
});
interface Record { interface Record {
control_cbor: string; control_cbor: string;
@ -58,6 +64,7 @@ afterEach(() => {
vi.mocked(verifyStrict).mockReset(); vi.mocked(verifyStrict).mockReset();
vi.mocked(evaluateCMS).mockReset(); vi.mocked(evaluateCMS).mockReset();
vi.mocked(evaluateSeal).mockReset(); vi.mocked(evaluateSeal).mockReset();
vi.mocked(parseToken).mockReset();
}); });
describe('evaluateSecurity, when an evaluator throws', () => { describe('evaluateSecurity, when an evaluator throws', () => {
@ -91,6 +98,17 @@ describe('evaluateSecurity, when an evaluator throws', () => {
expect([v.signature, v.seal, v.authorKey === undefined, v.detail]).toEqual(['F4', 'S2', false, undefined]); expect([v.signature, v.seal, v.authorKey === undefined, v.detail]).toEqual(['F4', 'S2', false, undefined]);
}); });
// A fault of the reader of a token, which is not a verdict of its form or of its algorithms, reaches evaluateSecurity:
// the seal of key 3 is S2, and a signature of alg 2 whose signer is sealed F1.
it('gives S2 for a fault of the reader of the token of a seal, and F1 for one of the token of a signer', () => {
const sealed = fixture('format3_sealed');
vi.mocked(parseToken).mockImplementationOnce(fault);
expect(evaluateSecurity(sealed.area, sealed.context)).toMatchObject({ signature: 'F4', seal: 'S2' });
const cms = fixture('format3_signed_cms');
vi.mocked(parseToken).mockImplementationOnce(fault);
expect(evaluateSecurity(cms.area, cms.context)).toEqual({ signature: 'F1', seal: 'S0' });
});
it('gives F1 and S2 for faults in both, and X for a fault while it decodes the area', () => { 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'); const { area, context } = fixture('format3_sealed');
vi.mocked(verifyStrict).mockImplementationOnce(fault); vi.mocked(verifyStrict).mockImplementationOnce(fault);

@ -1,7 +1,7 @@
// Tests of security.ts: the empty area that writers write, the verdicts of // Tests of security.ts: the empty area that writers write, the verdicts of
// the signature and of the seal without the context of a capsule, on the // the signature and of the seal without the context of a capsule, on the
// cases of TestSecurityVerdicts of the Go reference and a few more, and the // cases of TestSecurityVerdicts of the Go reference and a few more, and the
// Spanish lines of spec §29.7, as the reference gives them at spec-v0.11. // Spanish lines of spec §29.7, as the reference gives them at the draft v0.12.
// The verdicts in the context of a capsule are tested by author.test.ts, // The verdicts in the context of a capsule are tested by author.test.ts,
// securitycms.test.ts, security.failure.test.ts, fixtures.test.ts and // securitycms.test.ts, security.failure.test.ts, fixtures.test.ts and
// vectors.test.ts. // vectors.test.ts.
@ -86,24 +86,53 @@ describe('verdictLines', () => {
expect(verdictText('S0')).toBe(''); expect(verdictText('S0')).toBe('');
}); });
// F6 and S4 write the names that the reader found (spec §29.7, §29.10): a signer sealed before the round time or not, a signer who does not count. // F6 and S4 write the names that the reader found (spec v0.12 §29.7, §29.10), each between « and »: a signer sealed
it('writes the lines of F6 and S4 from the signers and the authority that were found', () => { // before the round time or not, with the authority of its seal, the warning that DateKeys does not check who issued
// the seals, and a signer who does not count, with its result in Spanish.
it('writes the lines of F6 and S4 from the signers and the authorities that were found', () => {
const t = { seconds: 1_790_000_000, nanos: 0 }; const t = { seconds: 1_790_000_000, nanos: 0 };
const line = (holder: string, before: boolean, result = 'valid') => ({ holder, issuer: `emisor de ${holder}`, result, sealTime: t, before }); const line = (holder: string, before: boolean, result = 'valid') => ({ holder, issuer: `emisor de ${holder}`, result, sealHolder: `TSA de ${holder}`, sealTime: t, before });
const lines = verdictLines({ const lines = verdictLines({
signature: 'F6', signature: 'F6',
seal: 'S4', seal: 'S4',
detail: { signers: [line('Ana', true), line('Luis', false)], foreign: [line('Otro', true, 'invalid')], sealHolder: 'TSA', sealTime: t }, detail: { signers: [line('Ana', true), line('Luis', false)], foreign: [line('Otro', true, 'invalid')], sealHolder: 'TSA', sealTime: t },
}); });
expect(lines).toEqual([ expect(lines).toEqual([
'Firmado con un certificado a nombre de Ana, Luis. DateKeys no comprueba quién lo emitió: para eso, exporta la firma a un validador oficial.', 'Firmado con un certificado a nombre de «Ana», «Luis». DateKeys no comprueba quién lo emitió: para eso, exporta la firma a un validador oficial.',
' Ana (emisor según su certificado: emisor de Ana), sellado el 2026-09-21T14:13:20Z, antes de la fecha de apertura.', ' «Ana» (emisor según su certificado: «emisor de Ana»), sellado por «TSA de Ana» el 2026-09-21T14:13:20Z, antes de la fecha de apertura.',
' Luis (emisor según su certificado: emisor de Luis), sellado el 2026-09-21T14:13:20Z, no antes de la fecha de apertura.', ' «Luis» (emisor según su certificado: «emisor de Luis»), sellado por «TSA de Luis» el 2026-09-21T14:13:20Z, no antes de la fecha de apertura.',
' Otro firmante, Otro: invalid. No cuenta.', ' DateKeys no comprueba quién emitió los sellos.',
'Según un sello a nombre de TSA, existía el 2026-09-21T14:13:20Z, antes de que la cápsula pudiera abrirse. DateKeys no comprueba quién emitió el sello.', ' Otro firmante, «Otro»: inválida. No cuenta.',
'Según un sello a nombre de «TSA», existía el 2026-09-21T14:13:20Z, antes de que la cápsula pudiera abrirse. DateKeys no comprueba quién emitió el sello.',
]); ]);
}); });
// No line says "antes de la fecha de apertura": no warning. A time keeps the fraction of its seal (RFC 3339).
it('warns of who issued the seals only when a signer was sealed before the date, and writes the fraction of a time', () => {
const t = { seconds: 1_790_000_000, nanos: 250_000_000 };
const late = { holder: 'Ana', issuer: 'CA', result: 'valid', sealHolder: 'TSA', sealTime: t, before: false };
expect(verdictLines({ signature: 'F6', seal: 'S0', detail: { signers: [late], foreign: [] } })).toEqual([
'Firmado con un certificado a nombre de «Ana». DateKeys no comprueba quién lo emitió: para eso, exporta la firma a un validador oficial.',
' «Ana» (emisor según su certificado: «CA»), sellado por «TSA» el 2026-09-21T14:13:20.25Z, no antes de la fecha de apertura.',
]);
expect(verdictLines({ signature: 'F4', seal: 'S4', authorKey: new Uint8Array(32), detail: { signers: [], foreign: [], sealHolder: 'TSA', sealTime: { seconds: t.seconds, nanos: 1 } } })[1]).toBe(
'Según un sello a nombre de «TSA», existía el 2026-09-21T14:13:20.000000001Z, antes de que la cápsula pudiera abrirse. DateKeys no comprueba quién emitió el sello.',
);
});
// The result of a signer who does not count, in the words of §29.7, whatever the verdict of the signature.
it.each([
['valid', 'válida'],
['invalid', 'inválida'],
['not verifiable', 'no verificable'],
['without seal', 'sin sello'],
['invalid seal', 'con el sello inválido'],
['out of validity', 'con el certificado fuera de validez'],
])('writes the result %s of a signer who does not count as «%s»', (result, text) => {
const foreign = [{ holder: 'Otro', issuer: 'CA', result, before: false }];
expect(verdictLines({ signature: 'F5', seal: 'S0', detail: { signers: [], foreign } })).toEqual([verdictText('F5'), ` Otro firmante, «Otro»: ${text}. No cuenta.`]);
});
// The verdicts of spec v0.11 (§29.7) whose text is fixed, and those whose text names a key, a holder or a time, F3, F4, F6 // The verdicts of spec v0.11 (§29.7) whose text is fixed, and those whose text names a key, a holder or a time, F3, F4, F6
// and S4, which verdictLines writes from what the reader found and verdictText leaves empty. // and S4, which verdictLines writes from what the reader found and verdictText leaves empty.
it('has the text of the verdicts of v0.11 that do not name anything, and none for those that do', () => { it('has the text of the verdicts of v0.11 that do not name anything, and none for those that do', () => {

@ -1,6 +1,7 @@
// The security area of a format 3 capsule (spec §29.3, §29.7), as // The security area of a format 3 capsule (spec §29.3, §29.7), as
// EncodeSecurity, EvaluateSecurityIn and the verdicts of the Go package // EncodeSecurity, EvaluateSecurityIn and the verdicts of the Go package
// capsule at spec-v0.11 (format3.go, signature.go). SECURITY_CBOR is the map // capsule give them at the draft v0.12 (format3.go, signature.go), the texts
// of the verdicts of a certificate included. SECURITY_CBOR is the map
// {0: "datekeys-security", 1: 1, ? 2: author-signature, ? 3: seal}, whose // {0: "datekeys-security", 1: 1, ? 2: author-signature, ? 3: seal}, whose
// keys 2 and 3 hold CBOR encoded apart. In the context of a capsule it checks // 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 // the signature of alg 1 with the strict profile of ed25519strict.ts, and the
@ -15,7 +16,7 @@ import { utf8Length } from './bytes.ts';
import { type Decoder, Encoder, peek, unmarshal } from './cbor.ts'; import { type Decoder, Encoder, peek, unmarshal } from './cbor.ts';
import { verifyStrict } from './ed25519strict.ts'; import { verifyStrict } from './ed25519strict.ts';
import { DateKeysError } from './errors.ts'; import { DateKeysError } from './errors.ts';
import { formatRFC3339, type Instant } from './datekey.ts'; import { formatRFC3339Nano, type Instant } from './datekey.ts';
import { fieldOf, requireKeys } from './schema.ts'; import { fieldOf, requireKeys } from './schema.ts';
import { type Detail, evaluateCMS, evaluateSeal } from './securitycms.ts'; import { type Detail, evaluateCMS, evaluateSeal } from './securitycms.ts';
@ -108,7 +109,30 @@ export function verdictText(v: Verdict): string {
} }
} }
/** The verdicts as the official SDK shows them, in order: X alone, or the signature and then the seal, when it shows something. */ // The result of a signer in the texts of §29.7.
const RESULT_TEXT: Readonly<Record<string, string>> = {
valid: 'válida',
invalid: 'inválida',
absent: 'ausente',
'not verifiable': 'no verificable',
'without seal': 'sin sello',
'invalid seal': 'con el sello inválido',
'out of validity': 'con el certificado fuera de validez',
};
// A name of a certificate between « and », as the texts of §29.7 write it, so
// that where it starts and where it ends is in view.
const quoted = (name: string): string => `«${name}»`;
/**
* The verdicts as the official SDK shows them, in order: X alone, or the
* signature and then the seal, when it shows something (spec §29.7). F6 is
* followed by a line for each required signer, which names the authority of
* its seal, by the warning that DateKeys does not check who issued the seals
* when one of them says that it is before the opening date, and by the
* signers who do not count. A time is in RFC 3339 with the fraction of the
* seal.
*/
export function verdictLines(v: Verdicts): string[] { export function verdictLines(v: Verdicts): string[] {
if (v.signature === 'X') return [verdictText('X')]; if (v.signature === 'X') return [verdictText('X')];
let signature = verdictText(v.signature); let signature = verdictText(v.signature);
@ -118,15 +142,17 @@ export function verdictLines(v: Verdicts): string[] {
const lines = [signature]; const lines = [signature];
const d = v.detail; const d = v.detail;
if (v.signature === 'F6' && d !== undefined) { if (v.signature === 'F6' && d !== undefined) {
lines[0] = `Firmado con un certificado a nombre de ${d.signers.map((s) => s.holder).join(', ')}. DateKeys no comprueba quién lo emitió: para eso, exporta la firma a un validador oficial.`; lines[0] = `Firmado con un certificado a nombre de ${d.signers.map((s) => quoted(s.holder)).join(', ')}. DateKeys no comprueba quién lo emitió: para eso, exporta la firma a un validador oficial.`;
for (const s of d.signers) { for (const s of d.signers) {
const when = s.before ? 'antes de la fecha de apertura' : 'no antes de la fecha de apertura'; const when = s.before ? 'antes de la fecha de apertura' : 'no antes de la fecha de apertura';
lines.push(` ${s.holder} (emisor según su certificado: ${s.issuer}), sellado el ${formatRFC3339(s.sealTime!)}, ${when}.`); lines.push(` ${quoted(s.holder)} (emisor según su certificado: ${quoted(s.issuer)}), sellado por ${quoted(s.sealHolder!)} el ${formatRFC3339Nano(s.sealTime!)}, ${when}.`);
} }
// §29.7: whoever says that a capsule was signed before the date says that it does not check who issued the seal.
if (d.signers.some((s) => s.before)) lines.push(' DateKeys no comprueba quién emitió los sellos.');
} }
for (const s of d?.foreign ?? []) lines.push(` Otro firmante, ${s.holder}: ${s.result}. No cuenta.`); for (const s of d?.foreign ?? []) lines.push(` Otro firmante, ${quoted(s.holder)}: ${RESULT_TEXT[s.result]!}. No cuenta.`);
if (v.seal === 'S4' && d?.sealHolder !== undefined) { if (v.seal === 'S4' && d?.sealHolder !== undefined) {
lines.push(`Según un sello a nombre de ${d.sealHolder}, existía el ${formatRFC3339(d.sealTime!)}, antes de que la cápsula pudiera abrirse. DateKeys no comprueba quién emitió el sello.`); lines.push(`Según un sello a nombre de ${quoted(d.sealHolder)}, existía el ${formatRFC3339Nano(d.sealTime!)}, antes de que la cápsula pudiera abrirse. DateKeys no comprueba quién emitió el sello.`);
} else if (verdictText(v.seal) !== '') { } else if (verdictText(v.seal) !== '') {
lines.push(verdictText(v.seal)); lines.push(verdictText(v.seal));
} }

@ -1,31 +1,43 @@
// Tests of securitycms.ts, the verdicts of a signature of alg 2 and of a seal // 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 // of seal_type 2 (spec v0.12 §29.7, §29.10, §29.11), through evaluateSecurity
// in the context of a capsule, on signatures and tokens that // 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 // testing/cmsbuild.ts makes: the cases of signature2_test.go of the Go
// order mark at the start of a name or a time, and keys whose point is // reference at the draft v0.12, what a signer line shows of a certificate, a
// compressed. capsule.EvaluateSecurityIn of the Go reference at spec-v0.11 // byte order mark at the start of a name or a time, and keys whose point is
// gives the same verdicts, signer lines and Spanish lines on areas made the // compressed. The hashes of the issuers below are those of the DER of their
// same way: an oracle compared them, and the hashes of the issuers below are // Names, which the reference shows in their place.
// the ones it gave for these Names.
import { sha256 } from '@noble/hashes/sha2.js'; import { sha256 } from '@noble/hashes/sha2.js';
import { describe, expect, it } from 'vitest'; import { describe, expect, it } from 'vitest';
import { ALG_CMS, authorMessage, sealSubject, signersDigest } from './author.ts'; import { ALG_CMS, ALG_ED25519, authorMessage, sealSubject, signersDigest } from './author.ts';
import { compareBytes, toHex } from './bytes.ts'; import { compareBytes, toHex } from './bytes.ts';
import { Encoder } from './cbor.ts'; import { Encoder } from './cbor.ts';
import { evaluateSecurity, type SecurityContext, type Verdicts, verdictLines } from './security.ts'; import { ed25519 } from '@noble/curves/ed25519.js';
import { evaluateSecurity, type SecurityContext, type Verdicts, verdictLines, verdictText } from './security.ts';
import * as b from './testing/cmsbuild.ts'; import * as b from './testing/cmsbuild.ts';
const from = new Date(Date.UTC(2025, 0, 1)); const certFrom = new Date(Date.UTC(2025, 0, 1));
const to = new Date(Date.UTC(2030, 0, 1)); const certTo = new Date(Date.UTC(2032, 0, 1));
const now = new Date(Date.UTC(2026, 8, 30, 12)); const roundTime = new Date(Date.UTC(2030, 0, 1));
const signedAt = new Date(Date.UTC(2026, 8, 30, 12));
const at = (d: Date): { seconds: number; nanos: number } => ({ seconds: Math.floor(d.getTime() / 1000), nanos: (d.getTime() % 1000) * 1_000_000 });
const context: SecurityContext = { const context: SecurityContext = {
controlCommit: new Uint8Array(32).fill(1), controlCommit: Uint8Array.from({ length: 32 }, (_, i) => (i === 0 ? 1 : 0)),
headDigest: new Uint8Array(32).fill(2), headDigest: Uint8Array.from({ length: 32 }, (_, i) => (i === 0 ? 2 : 0)),
roundTime: { seconds: Date.UTC(2026, 9, 1) / 1000, nanos: 0 }, roundTime: at(roundTime),
}; };
const te = new TextEncoder(); const te = new TextEncoder();
const BOM = Uint8Array.of(0xef, 0xbb, 0xbf); const BOM = Uint8Array.of(0xef, 0xbb, 0xbf);
const F5 = verdictText('F5');
const ecKey = await b.newKey('P-256');
const rsaKey = await b.newKey('rsa', 2048);
const p384Key = await b.newKey('P-384');
const tsaKey = await b.newKey('P-256');
const ana = b.newCert({ cn: 'Ana López', from: certFrom, to: certTo }, ecKey);
const luis = b.newCert({ cn: 'Luis Gómez', from: certFrom, to: certTo }, rsaKey);
const otro = b.newCert({ cn: 'Otro', from: certFrom, to: certTo }, p384Key);
const tsa = b.newCert({ cn: 'TSA de prueba', from: certFrom, to: certTo }, tsaKey);
// SECURITY_CBOR with the contents of keys 2 and 3 given. // SECURITY_CBOR with the contents of keys 2 and 3 given.
function area(signature: Uint8Array | undefined, seal: Uint8Array | undefined): Uint8Array { function area(signature: Uint8Array | undefined, seal: Uint8Array | undefined): Uint8Array {
@ -46,46 +58,180 @@ function area(signature: Uint8Array | undefined, seal: Uint8Array | undefined):
return e.out(); return e.out();
} }
// The verdicts of a signature of alg 2 by the signers, all of them required, // The content of a key of the security area: a map of 0, 1 and, when given, 2.
// each with a signature-time-stamp of `tsa` when it is given. function item(first: number, key1: Uint8Array, key2?: Uint8Array): Uint8Array {
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(); const e = new Encoder();
e.map(3); e.map(key2 === undefined ? 2 : 3);
e.uint(0); e.uint(0);
e.uint(ALG_CMS); e.uint(first);
e.uint(1); e.uint(1);
e.bstr(key1); e.bstr(key1);
e.uint(2); if (key2 !== undefined) {
e.bstr(cms); e.uint(2);
return evaluateSecurity(area(e.out(), undefined), context); e.bstr(key2);
}
return e.out();
} }
// The verdicts of a seal of seal_type 2 by `tsa`, without a signature. // SIGNERS of the certificates given, sorted.
async function sealed(tsa: b.Signer): Promise<Verdicts> { function signersOf(...required: b.Signer[]): Uint8Array {
const token = await b.token(sealSubject(context.controlCommit, context.headDigest, undefined), now, {}, tsa); const hashes = required.map((s) => sha256(s.cert)).sort(compareBytes);
const e = new Encoder(); const list = new Encoder();
e.map(2); list.array(hashes.length);
e.uint(0); for (const x of hashes) list.bstr(x);
e.uint(2); return list.out();
e.uint(1); }
e.bstr(token);
return evaluateSecurity(area(undefined, e.out()), context); // The security area of a capsule with a signature of alg 2 by the signers, which SIGNERS requires with required, each
// signature sealed by `authority` at `when` with an accuracy of a second, and the seal of key 3 given.
async function cmsArea(required: b.Signer[], signers: b.Signer[], authority: b.Signer | undefined, when: Date, seal?: Uint8Array): Promise<Uint8Array> {
const key1 = signersOf(...required);
const msg = authorMessage(context.controlCommit, context.headDigest, signersDigest(ALG_CMS, key1));
const o: b.Options = authority === undefined ? {} : { token: (sig) => b.token(sig, when, { accuracy: b.accuracyOf(1) }, authority) };
return area(item(ALG_CMS, key1, await b.signature(msg, o, ...signers)), seal);
} }
const cn = (s: string): Uint8Array => b.rdnName(['2.5.4.3', b.utf8(s)], ['2.5.4.10', b.utf8('DateKeys test')]); // The verdicts of a signature of alg 2 by the signers, all of them required, each with a signature-time-stamp of `tsa`.
async function signed(authority: b.Signer | undefined, ...signers: b.Signer[]): Promise<Verdicts> {
return evaluateSecurity(await cmsArea(signers, signers, authority, signedAt), context);
}
// The verdicts of a seal of seal_type 2 by `authority`, without a signature.
async function sealed(authority: b.Signer): Promise<Verdicts> {
const token = await b.token(sealSubject(context.controlCommit, context.headDigest, undefined), signedAt, {}, authority);
return evaluateSecurity(area(undefined, item(2, token)), context);
}
const quoted = (v: Verdicts): string => v.detail!.signers.map((s) => `«${s.holder}»`).join(', ');
describe('a signature of alg 2', () => {
// Spec §29.7, §29.10: alg 2 gives F6 when every required signer is valid and sealed, and the first of F2, F5 and F1
// that applies otherwise.
it('names its signers between « and », with the authority of each seal and the warning of §29.7', async () => {
const v = evaluateSecurity(await cmsArea([ana, luis], [ana, luis], tsa, signedAt), context);
expect([v.signature, v.seal, v.detail!.signers.length]).toEqual(['F6', 'S0', 2]);
const s = v.detail!.signers;
expect(verdictLines(v)).toEqual([
`Firmado con un certificado a nombre de ${quoted(v)}. DateKeys no comprueba quién lo emitió: para eso, exporta la firma a un validador oficial.`,
` «${s[0]!.holder}» (emisor según su certificado: «${s[0]!.issuer}»), sellado por «TSA de prueba» el 2026-09-30T12:00:00Z, antes de la fecha de apertura.`,
` «${s[1]!.holder}» (emisor según su certificado: «${s[1]!.issuer}»), sellado por «TSA de prueba» el 2026-09-30T12:00:00Z, antes de la fecha de apertura.`,
' DateKeys no comprueba quién emitió los sellos.',
]);
expect([...s.map((x) => x.holder)].sort()).toEqual(['Ana López', 'Luis Gómez']);
expect(s.map((x) => [x.result, x.sealHolder, x.sealTime, x.before])).toEqual([
['valid', 'TSA de prueba', at(signedAt), true],
['valid', 'TSA de prueba', at(signedAt), true],
]);
});
describe('the issuer of a signer', () => { it('warns of who issued the seals only when a line says before the opening date', async () => {
// The issuer is text of the certificate, as the holder is: one that breaks the rules of the declared author shows const late = evaluateSecurity(await cmsArea([ana], [ana], tsa, new Date(roundTime.getTime() + 3_600_000)), context);
// the SHA-256 of the DER of its Name, Go's sha256.Sum256(RawIssuer), and never that of the certificate. expect([late.signature, verdictLines(late).length]).toEqual(['F6', 2]);
it('shows the SHA-256 of the Name of an issuer that breaks the rules of the declared author, as Go', async () => { expect(verdictLines(late)[1]).toBe(' «Ana López» (emisor según su certificado: «Ana López»), sellado por «TSA de prueba» el 2030-01-01T01:00:00Z, no antes de la fecha de apertura.');
const tsa = await b.newECDSA('TSA de prueba', 'P-256', from, to); // t plus the accuracy of a second equal to the round time is not before it.
const edge = evaluateSecurity(await cmsArea([ana], [ana], tsa, new Date(roundTime.getTime() - 1000)), context);
expect([edge.signature, edge.detail!.signers[0]!.before, verdictLines(edge).length]).toEqual(['F6', false, 2]);
// A seal with a fraction of a second shows it.
const fraction = evaluateSecurity(await cmsArea([ana], [ana], tsa, new Date(signedAt.getTime() + 250)), context);
expect(verdictLines(fraction)[1]).toBe(' «Ana López» (emisor según su certificado: «Ana López»), sellado por «TSA de prueba» el 2026-09-30T12:00:00.25Z, antes de la fecha de apertura.');
});
// A signer who is not required shows apart, with its result in Spanish, and does not count.
it('shows a signer who is not required apart, with its result in Spanish', async () => {
const f = evaluateSecurity(await cmsArea([ana], [ana, otro], tsa, signedAt), context);
expect([f.signature, f.detail!.foreign.map((s) => s.holder), verdictLines(f).at(-1)]).toEqual(['F6', ['Otro'], ' Otro firmante, «Otro»: válida. No cuenta.']);
const unsealed = evaluateSecurity(await cmsArea([ana], [ana, otro], undefined, signedAt), context);
expect([unsealed.signature, verdictLines(unsealed)]).toEqual(['F5', [F5, ' Otro firmante, «Otro»: sin sello. No cuenta.']]);
});
// F5, and the result of the first required signer (spec §29.10, steps 1 to 7): the certificate of a signer that
// expired before the time of a valid seal is out of validity; a seal whose authority was not valid at its time is an
// invalid seal.
it('gives F5 and the result of each required signer', async () => {
const expired = b.newCert({ cn: 'Ana caducada', from: certFrom, to: new Date(Date.UTC(2026, 7, 30, 12)) }, ecKey);
const small = b.newCert({ cn: 'Clave corta', from: certFrom, to: certTo }, await b.newKey('rsa', 1024));
const key3 = item(4294967295, Uint8Array.of(1));
const cases: [string, Promise<Uint8Array>, string][] = [
['a required signer is absent', cmsArea([luis], [ana], tsa, signedAt), 'absent'],
['no seal', cmsArea([ana], [ana], undefined, signedAt), 'without seal'],
['a certificate out of validity at the time of a valid seal', cmsArea([expired], [expired], tsa, signedAt), 'out of validity'],
['a seal whose authority was not valid at its time', cmsArea([ana], [ana], tsa, new Date(Date.UTC(2024, 0, 1))), 'invalid seal'],
['a key outside the table', cmsArea([small], [small], tsa, signedAt), 'not verifiable'],
['a key 3 beside it', cmsArea([ana], [ana], tsa, signedAt, key3), 'valid'],
];
for (const [name, der, result] of cases) {
const v = evaluateSecurity(await der, context);
expect([v.signature, v.detail!.signers[0]!.result, verdictLines(v)[0]], name).toEqual(['F5', result, F5]);
}
// The absent signer is named by the hash that SIGNERS gives, without an issuer.
const absent = evaluateSecurity(await cmsArea([luis], [ana], tsa, signedAt), context);
expect(absent.detail!.signers).toEqual([{ holder: toHex(sha256(luis.cert)), issuer: '', result: 'absent', before: false }]);
expect(absent.detail!.foreign.map((s) => [s.holder, s.result])).toEqual([['Ana López', 'valid']]);
});
it('gives F2 in another capsule, and F1 without a context or for SIGNERS that break its rule', async () => {
const der = await cmsArea([ana], [ana], tsa, signedAt);
const other: SecurityContext = { ...context, headDigest: context.headDigest.map((x, i) => (i === 5 ? 9 : x)) };
const v = evaluateSecurity(der, other);
expect([v.signature, v.detail!.signers[0]!.result, verdictLines(v)]).toEqual(['F2', 'invalid', [verdictText('F2')]]);
expect(evaluateSecurity(der)).toEqual({ signature: 'F1', seal: 'S0' });
// SIGNERS out of order, empty, too long, with an element of 31 bytes or one twice, each beside a CMS signature that
// is valid for the AUTHOR_MESSAGE of those very SIGNERS: the rule decides, not the CMS.
const [x, y] = [sha256(ana.cert), sha256(luis.cert)].sort(compareBytes) as [Uint8Array, Uint8Array];
const bstr = (h: Uint8Array): Uint8Array => Uint8Array.of(0x58, h.length, ...h);
const cat = (...p: Uint8Array[]): Uint8Array => Uint8Array.from(p.flatMap((q) => [...q]));
const lists: [string, Uint8Array][] = [
['SIGNERS out of order', cat(Uint8Array.of(0x82), bstr(y), bstr(x))],
['an empty SIGNERS', Uint8Array.of(0x80)],
['SIGNERS of 17 entries', cat(Uint8Array.of(0x91), ...Array.from({ length: 17 }, () => bstr(x)))],
['SIGNERS with 31 bytes', cat(Uint8Array.of(0x81), bstr(x.subarray(0, 31)))],
['SIGNERS with one entry twice', cat(Uint8Array.of(0x82), bstr(x), bstr(x))],
['SIGNERS of indefinite length', cat(Uint8Array.of(0x9f), bstr(x), Uint8Array.of(0xff))],
['SIGNERS with a byte after them', cat(Uint8Array.of(0x81), bstr(x), Uint8Array.of(0))],
];
for (const [name, key1] of lists) {
const msg = authorMessage(context.controlCommit, context.headDigest, signersDigest(ALG_CMS, key1));
const cms = await b.signature(msg, { token: (sig) => b.token(sig, signedAt, {}, tsa) }, ana, luis);
expect(evaluateSecurity(area(item(ALG_CMS, key1, cms), undefined), context), name).toEqual({ signature: 'F1', seal: 'S0' });
}
expect(evaluateSecurity(area(item(ALG_CMS, signersOf(ana), te.encode('not DER')), undefined), context)).toEqual({ signature: 'F1', seal: 'S0' });
});
});
describe('the names of a certificate', () => {
// Spec v0.12 §29.7: a name of a certificate shows when it meets the rules of the declared author, has at most 64 code
// points and no two spaces in a row; otherwise the SHA-256 of the certificate shows, or that of the name of the issuer
// for the issuer.
it.each([
['a name', 'Ana López', true],
['64 code points', 'ñ'.repeat(64), true],
['65 code points', `${'ñ'.repeat(64)}a`, false],
['64 code points outside the BMP, 128 units of UTF-16', '\u{1f600}'.repeat(64), true],
['65 code points outside the BMP', '\u{1f600}'.repeat(65), false],
['two spaces in a row', 'Ana López', false],
['an escape', 'Ana\x1b[31mLópez', false],
['U+202E', 'Ana \u{202e}zepóL', false],
['a byte order mark', '\u{feff}Ana López', false],
['a space at the start', ' Ana López', false],
['a line feed', 'Ana\nLópez', false],
['a zero width space', 'Ana\u{200b}López', false],
['a tag that spells a text', 'Ana\u{e0041}', false],
['an emoji with its selector', 'Ana \u{2764}\u{fe0f}', true],
['a combining mark, 64 points', 'n\u{303}'.repeat(32), true],
['a combining mark, 66 points', 'n\u{303}'.repeat(33), false],
['an empty name', '', false],
] as const)('%s', async (_name, cn, shown) => {
const s = b.newCert({ cn, ski: Uint8Array.of(1), issuer: b.name(b.atv(b.OID.commonName, b.utf8(cn))), from: certFrom, to: certTo }, ecKey);
const v = await signed(tsa, s);
expect(v.signature).toBe('F6');
const want = shown ? [cn, cn] : [toHex(sha256(s.cert)), toHex(sha256(s.issuer))];
expect([v.detail!.signers[0]!.holder, v.detail!.signers[0]!.issuer]).toEqual(want);
});
// The issuer is text of the certificate, as the holder is: one that breaks the rules of a name 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 a name', async () => {
const cn = (s: string): Uint8Array => b.rdnName(['2.5.4.3', b.utf8(s)], ['2.5.4.10', b.utf8('DateKeys test')]);
const issuers: [string, Uint8Array, string][] = [ const issuers: [string, Uint8Array, string][] = [
['an issuer with ESC', cn('Ana\x1b[2J'), '53213e495daf7cc473c94da46283bae22d5d54b58681a0635cd22b96abde7c3f'], ['an issuer with ESC', cn('Ana\x1b[2J'), '53213e495daf7cc473c94da46283bae22d5d54b58681a0635cd22b96abde7c3f'],
['an issuer with U+202E', cn('Ana\u{202e}gpj.exe'), 'b1772664c1dbb3470b8d419f2275839241987889333ce17f223414a939634559'], ['an issuer with U+202E', cn('Ana\u{202e}gpj.exe'), 'b1772664c1dbb3470b8d419f2275839241987889333ce17f223414a939634559'],
@ -94,59 +240,101 @@ describe('the issuer of a signer', () => {
['an issuer that starts with U+FEFF', b.rdnName(['2.5.4.3', b.tlv(0x0c, BOM, te.encode('Autoridad'))]), '2bcf35767b63b7b0370d61cf63620623adac5a09662763ebd331316d4d4c6097'], ['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) { for (const [name, issuer, hash] of issuers) {
const s = await b.newECDSA('Ana', 'P-256', from, to, 'cn', { issuer }); const v = await signed(tsa, b.newCert({ cn: 'Ana', issuer, from: certFrom, to: certTo }, ecKey));
const v = await signed(tsa, s);
expect([v.signature, v.seal], name).toEqual(['F6', 'S0']); expect([v.signature, v.seal], name).toEqual(['F6', 'S0']);
const line = v.detail!.signers[0]!; const line = v.detail!.signers[0]!;
expect([line.holder, line.issuer, line.result], name).toEqual(['Ana', hash, 'valid']); expect([line.holder, line.issuer, line.result], name).toEqual(['Ana', hash, 'valid']);
expect(toHex(sha256(issuer)), name).toBe(hash); 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.`); expect(verdictLines(v)[1], name).toBe(` «Ana» (emisor según su certificado: «${hash}»), sellado por «TSA de prueba» el 2026-09-30T12:00:00Z, antes de la fecha de apertura.`);
} }
// An issuer that meets the rules shows its name. // An issuer without a commonName shows its organizationName, which meets the rules.
const good = await signed(tsa, await b.newECDSA('Ana', 'P-256', from, to, 'cn', { issuer: cn('Autoridad de prueba') })); const org = await signed(tsa, b.newCert({ cn: 'Ana', issuer: b.rdnName(['2.5.4.10', b.utf8('Banco de Pruebas S.A.')]), from: certFrom, to: certTo }, ecKey));
expect(good.detail!.signers[0]!.issuer).toBe('Autoridad de prueba'); expect(org.detail!.signers[0]!.issuer).toBe('Banco de Pruebas S.A.');
});
// The holder of a certificate of the FNMT is its givenName and its surname: its commonName carries the NIF.
it('names the holder by givenName and surname before the commonName', async () => {
const subject = b.rdnName(['2.5.4.3', b.utf8('ESPAÑOL ESPAÑOL JUAN - 12345678Z')], ['2.5.4.42', b.utf8('JUAN')], ['2.5.4.4', b.utf8('ESPAÑOL ESPAÑOL')]);
const v = await signed(tsa, b.newCert({ subject, issuer: b.rdnName(['2.5.4.3', b.utf8('CA de prueba')]), from: certFrom, to: certTo }, ecKey));
expect(verdictLines(v)[0]).toBe('Firmado con un certificado a nombre de «JUAN ESPAÑOL ESPAÑOL». DateKeys no comprueba quién lo emitió: para eso, exporta la firma a un validador oficial.');
}); });
}); });
describe('a byte order mark at the start of a name or a time', () => { 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 // 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. // that starts with it does not parse, and the signature is not DER.
it('keeps it in a name, which then shows the hash of the certificate, as Go', async () => { it('keeps it in a name, which then shows the hash of the certificate', async () => {
const tsa = await b.newECDSA('TSA de prueba', 'P-256', from, to); const s = b.newCert({ subject: b.rdnName(['2.5.4.3', b.tlv(0x0c, BOM, te.encode('Ana'))]), from: certFrom, to: certTo }, ecKey);
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); const v = await signed(tsa, s);
expect([v.signature, v.detail!.signers[0]!.holder]).toEqual(['F6', toHex(sha256(s.cert))]); 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 authority = b.newCert({ subject: b.rdnName(['2.5.4.3', b.tlv(0x0c, BOM, te.encode('TSA'))]), from: certFrom, to: certTo }, tsaKey);
const seal = await sealed(authority); const seal = await sealed(authority);
expect([seal.seal, seal.detail!.sealHolder]).toEqual(['S4', toHex(sha256(authority.cert))]); 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 () => { it('refuses a time of a certificate that starts with it: F1 for a signer, S2 for the authority of a seal', async () => {
const tsa = await b.newECDSA('TSA de prueba', 'P-256', from, to); const notBefore = b.tlv(0x17, BOM, te.encode('250101000000Z'));
const utc = (s: Uint8Array): Uint8Array => b.tlv(0x17, s); const v = await signed(tsa, b.newCert({ cn: 'Ana', notBefore, to: certTo }, ecKey));
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]); 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 })); const seal = await sealed(b.newCert({ cn: 'TSA', notBefore, to: certTo }, tsaKey));
expect([seal.signature, seal.seal]).toEqual(['F0', 'S2']); expect([seal.signature, seal.seal]).toEqual(['F0', 'S2']);
}); });
}); });
describe('an ECDSA key with its point compressed', () => { 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 // Go's ecdsa.ParseUncompressedPublicKey reads only the uncompressed point, 0x04 and the two coordinates: a key
// otherwise is outside the table, though noble would read it. // 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) => { 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', async (curve) => {
const tsa = await b.newECDSA('TSA de prueba', 'P-256', from, to); const key = await b.newKey(curve);
const compressed = await b.newECDSA('Ana', curve, from, to, 'cn', { compressed: true }); const curveOID = curve === 'P-256' ? b.OID.p256 : curve === 'P-384' ? b.OID.p384 : b.OID.p521;
const spki = b.spkiEC(b.oid(curveOID), b.compressed(key));
const compressed = b.newCert({ cn: 'Ana', spki, from: certFrom, to: certTo }, key);
const v = await signed(tsa, compressed); const v = await signed(tsa, compressed);
expect([v.signature, v.detail!.signers[0]!.result]).toEqual(['F5', 'not verifiable']); expect([v.signature, v.detail!.signers[0]!.result]).toEqual(['F5', 'not verifiable']);
const plain = await b.newECDSA('Ana', curve, from, to); const plain = b.newCert({ cn: 'Ana', from: certFrom, to: certTo }, key);
expect((await signed(tsa, plain)).signature).toBe('F6'); expect((await signed(tsa, plain)).signature).toBe('F6');
const authority = await b.newECDSA('TSA', curve, from, to, 'cn', { compressed: true }); const authority = b.newCert({ cn: 'TSA', spki, from: certFrom, to: certTo }, key);
expect((await sealed(authority)).seal).toBe('S1'); expect((await sealed(authority)).seal).toBe('S1');
const late = await signed(authority, plain); const late = await signed(authority, plain);
expect([late.signature, late.detail!.signers[0]!.result]).toEqual(['F5', 'invalid seal']); 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'); expect((await sealed(b.newCert({ cn: 'TSA', from: certFrom, to: certTo }, key))).seal).toBe('S4');
});
});
describe('a seal of seal_type 2', () => {
// Spec v0.11 §29.11: a seal of seal_type 2 seals SEAL_SUBJECT, and gives S4 before the round time, S5 after it, and
// S3, S2 and S1 for what does not verify, does not decode or uses another hash.
it('seals SEAL_SUBJECT, and gives S1 to S5', async () => {
const authority = b.newCert({ cn: 'Autoridad de Sellado', from: certFrom, to: certTo }, tsaKey);
const seed = new Uint8Array(32).fill(7);
const msg = authorMessage(context.controlCommit, context.headDigest, signersDigest(ALG_ED25519));
const sig = item(ALG_ED25519, ed25519.getPublicKey(seed), ed25519.sign(msg, seed));
const subject = sealSubject(context.controlCommit, context.headDigest, sig);
const withSeal = (token: Uint8Array): Uint8Array => area(sig, item(2, token));
const v = evaluateSecurity(withSeal(await b.token(subject, signedAt, {}, authority)), context);
expect([v.signature, v.seal, v.detail!.sealHolder, v.detail!.sealTime]).toEqual(['F4', 'S4', 'Autoridad de Sellado', at(signedAt)]);
expect(verdictLines(v)[1]).toBe('Según un sello a nombre de «Autoridad de Sellado», existía el 2026-09-30T12:00:00Z, antes de que la cápsula pudiera abrirse. DateKeys no comprueba quién emitió el sello.');
// Sealed with its own signature part: without key 2 the subject differs.
const alone = await sealed(authority);
expect([alone.signature, alone.seal]).toEqual(['F0', 'S4']);
const cases: [string, Promise<Uint8Array> | Uint8Array, string][] = [
['after the round time', b.token(subject, new Date(roundTime.getTime() + 60_000), {}, authority), 'S5'],
['the accuracy reaches it', b.token(subject, new Date(roundTime.getTime() - 1000), { accuracy: b.accuracyOf(2) }, authority), 'S5'],
['another subject', b.token(te.encode('other'), signedAt, {}, authority), 'S3'],
['an imprint of 33 bytes', b.token(subject, signedAt, { imprint: new Uint8Array(33) }, authority), 'S3'],
['a TSTInfo of version 2', b.token(subject, signedAt, { version: 2 }, authority), 'S2'],
['not DER', te.encode('not DER'), 'S2'],
['SHA-384 in the imprint', b.token(subject, signedAt, { hash: 'SHA-384' }, authority), 'S1'],
['the TSA expired at its time', b.token(subject, new Date(Date.UTC(2033, 0, 1)), {}, authority), 'S3'],
];
for (const [name, token, want] of cases) expect(evaluateSecurity(withSeal(await token), context).seal, name).toBe(want);
// A seal at the edge of the round time, a microsecond before it with its accuracy of 999 ms and 999 µs, proves it.
const edge = evaluateSecurity(withSeal(await b.token(subject, new Date(roundTime.getTime() - 1000), { accuracy: b.accuracyOf(0, 999, 999) }, authority)), context);
expect([edge.seal, verdictLines(edge)[1]]).toEqual([
'S4',
'Según un sello a nombre de «Autoridad de Sellado», existía el 2029-12-31T23:59:59Z, antes de que la cápsula pudiera abrirse. DateKeys no comprueba quién emitió el sello.',
]);
// Without a context, as a reader of v0.10: S1.
expect(evaluateSecurity(withSeal(await b.token(subject, signedAt, {}, authority))).seal).toBe('S1');
}); });
}); });

@ -1,11 +1,11 @@
// The verdicts of a signature of alg 2 (CMS with certificates) and of a seal of // 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, // seal_type 2 (RFC 3161), as the Go package capsule gives them (signature2.go,
// spec v0.11 §29.7, §29.10, §29.11): F1, F2, F5 and F6 with the signers named, // spec v0.12 §29.7, §29.10, §29.11): F1, F2, F5 and F6 with the signers named,
// and S1 to S5 with the authority of a valid seal. Internal: index.ts does not // and S1 to S5 with the authority of a valid seal. Internal: index.ts does not
// re-export it. // re-export it.
import { ALG_CMS, authorMessage, sealSubject, signersDigest } from './author.ts'; import { ALG_CMS, authorMessage, sealSubject, signersDigest } from './author.ts';
import { equalBytes, toHex, utf8Length } from './bytes.ts'; import { compareBytes, equalBytes, toHex } from './bytes.ts';
import { type Decoder, Encoder, unmarshal } from './cbor.ts'; import { type Decoder, Encoder, unmarshal } from './cbor.ts';
import { import {
addInstants, addInstants,
@ -20,6 +20,7 @@ import {
parseSignature, parseSignature,
parseToken, parseToken,
type SignerInfo, type SignerInfo,
type Token,
tokenImprintIsSHA256, tokenImprintIsSHA256,
} from './cms.ts'; } from './cms.ts';
import { compareInstants, type Instant } from './datekey.ts'; import { compareInstants, type Instant } from './datekey.ts';
@ -28,17 +29,20 @@ import { checkAuthor } from './pathrule.ts';
/** The most required signers of an alg 2 signature (spec §29.10). */ /** The most required signers of an alg 2 signature (spec §29.10). */
export const MAX_SIGNERS = 16; export const MAX_SIGNERS = 16;
const MAX_AUTHOR_LEN = 256;
/** 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). */ /** A signer of an alg 2 signature as a reader shows it (spec §29.7, §29.10). */
export interface SignerLine { 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. */ /** 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; 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. */ /** 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; readonly issuer: string;
/** 'valid', 'invalid', 'absent', 'not verifiable', 'without seal', 'invalid seal' or 'out of validity'. */ /** 'valid', 'invalid', 'absent', 'not verifiable', 'without seal', 'invalid seal' or 'out of validity'. */
readonly result: string; readonly result: string;
/** t, undefined without a seal that verifies. */ /** 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; readonly sealTime?: Instant;
/** Whether t plus the accuracy of the seal is before round_time. */ /** Whether t plus the accuracy of the seal is before round_time. */
readonly before: boolean; readonly before: boolean;
@ -63,7 +67,7 @@ function decodeSigners(b: Uint8Array): Uint8Array[] {
for (let i = 0; i < n; i++) { for (let i = 0; i < n; i++) {
const h = d.bstr(32, 32); const h = d.bstr(32, 32);
const last = out[out.length - 1]; const last = out[out.length - 1];
if (last !== undefined && compare(last, h) >= 0) throw new DateKeysError('ERR_NON_CANONICAL_CBOR', 'SIGNERS is not in strictly ascending order'); if (last !== undefined && compareBytes(last, h) >= 0) throw new DateKeysError('ERR_NON_CANONICAL_CBOR', 'SIGNERS is not in strictly ascending order');
out.push(h); out.push(h);
} }
}; };
@ -75,45 +79,57 @@ function decodeSigners(b: Uint8Array): Uint8Array[] {
return out; return out;
} }
function compare(a: Uint8Array, b: Uint8Array): number { // The number of code points of a well-formed string.
for (let i = 0; i < Math.min(a.length, b.length); i++) if (a[i] !== b[i]) return a[i]! < b[i]! ? -1 : 1; function codePointCount(s: string): number {
return a.length - b.length; 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: the name, when it meets the rules of the declared author, and the SHA-256 otherwise. /**
* 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 { function holderText(name: string, hash: Uint8Array): string {
if (name !== '' && utf8Length(name) <= MAX_AUTHOR_LEN) { if (name !== '' && name.isWellFormed() && codePointCount(name) <= MAX_NAME_LEN && !name.includes(' ')) {
try { try {
checkAuthor(name); checkAuthor(name);
return name; return name;
} catch (err) { } catch (err) {
/* v8 ignore next -- @preserve: checkAuthor throws only its own error */ /* v8 ignore next -- @preserve: checkAuthor throws only its own error */
if (!(err instanceof DateKeysError || err instanceof Error)) throw err; if (!(err instanceof Error)) throw err;
} }
} }
return toHex(hash); return toHex(hash);
} }
// The time of a seal, plus its accuracy, against round_time: whether it precedes it.
function before(tok: Token, roundTime: Instant | undefined): boolean {
return roundTime !== undefined && compareInstants(addInstants(tok.genTime, tok.accuracy), roundTime) < 0;
}
// One SignerInfo as §29.10 orders: not verifiable, invalid, without seal, with an invalid seal, out of validity, or valid. // 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, // 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
// so that no escape, no control and no bidirectional character reaches a line of the verdicts. // 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 { 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)) }; 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); const r = checkSigner(s, msg);
if (r === 'not verifiable') return { ...base, result: 'not verifiable', before: false }; if (r !== 'valid') return { ...base, result: r };
if (r === 'invalid') return { ...base, result: 'invalid', before: false }; if (s.token === undefined) return { ...base, result: 'without seal' };
if (s.token === undefined) return { ...base, result: 'without seal', before: false }; let tok: Token | undefined;
let ok = false;
let tok;
try { try {
tok = parseToken(s.token); tok = parseToken(s.token);
ok = checkToken(tok, s.signature);
} catch (err) { } catch (err) {
if (!(err instanceof CmsFormError || err instanceof CmsAlgorithmError)) throw err; if (!(err instanceof CmsFormError || err instanceof CmsAlgorithmError)) throw err;
} }
if (!ok || tok === undefined) return { ...base, result: 'invalid seal', before: false }; if (tok === undefined || !checkToken(tok, s.signature)) return { ...base, result: 'invalid seal' };
if (!certValidAt(s.cert, tok.genTime)) return { ...base, result: 'out of validity', before: false }; if (!certValidAt(s.cert, tok.genTime)) return { ...base, result: 'out of validity' };
return { ...base, result: 'valid', sealTime: tok.genTime, before: roundTime !== undefined && compareInstants(addInstants(tok.genTime, tok.accuracy), roundTime) < 0 }; return { ...base, result: 'valid', sealHolder: holderText(certHolder(tok.tsa), tok.tsa.hash), sealTime: tok.genTime, before: before(tok, roundTime) };
} }
/** /**
@ -184,7 +200,5 @@ export function evaluateSeal(
} }
if (!tokenImprintIsSHA256(tok)) return { seal: 'S1' }; if (!tokenImprintIsSHA256(tok)) return { seal: 'S1' };
if (!checkToken(tok, sealSubject(controlCommit, headDigest, signature))) return { seal: 'S3' }; if (!checkToken(tok, sealSubject(controlCommit, headDigest, signature))) return { seal: 'S3' };
const sealHolder = holderText(certHolder(tok.tsa), tok.tsa.hash); return { seal: before(tok, roundTime) ? 'S4' : 'S5', sealHolder: holderText(certHolder(tok.tsa), tok.tsa.hash), sealTime: tok.genTime };
const before = roundTime !== undefined && compareInstants(addInstants(tok.genTime, tok.accuracy), roundTime) < 0;
return { seal: before ? 'S4' : 'S5', sealHolder, sealTime: tok.genTime };
} }

@ -1,7 +1,9 @@
// Builds the CMS signatures and the RFC 3161 tokens that the tests of cms.ts and // Builds the CMS signatures and the RFC 3161 tokens that the tests of cms.ts and
// of the verdicts read: certificates of test keys, a detached signature of a // of the verdicts read: certificates of test keys, made field by field, a
// message with its signedAttrs and, optionally, a time-stamp token of its // detached signature of a message with its signedAttrs and, optionally, a
// signature. It is the encoder that a signing application has, the TypeScript // time-stamp token of its signature, with options to write what the profiles
// of spec v0.12 §29.10 and §29.11 reject, or what verifies to something else.
// It is the encoder that a signing application has, the TypeScript
// counterpart of internal/cms/cmstest of the Go reference; nothing outside // counterpart of internal/cms/cmstest of the Go reference; nothing outside
// tests uses it. Keys and signatures come from WebCrypto, and the DER is built // tests uses it. Keys and signatures come from WebCrypto, and the DER is built
// here. The certificates are not signed by anyone: cms.ts never checks who // here. The certificates are not signed by anyone: cms.ts never checks who
@ -12,6 +14,7 @@ import { derContent, splitDer } from '../der.ts';
// WebCrypto takes a BufferSource over an ArrayBuffer, which a Uint8Array view does not promise. // WebCrypto takes a BufferSource over an ArrayBuffer, which a Uint8Array view does not promise.
const bs = (b: Uint8Array): ArrayBuffer => b.slice().buffer as ArrayBuffer; const bs = (b: Uint8Array): ArrayBuffer => b.slice().buffer as ArrayBuffer;
const te = new TextEncoder();
// ---- DER --------------------------------------------------------------------- // ---- DER ---------------------------------------------------------------------
@ -25,7 +28,28 @@ export const seq = (...c: Uint8Array[]): Uint8Array => tlv(0x30, ...c);
/** A SET OF (or an implicit [n] SET OF) in DER order. */ /** A SET OF (or an implicit [n] SET OF) in DER order. */
export const set = (tag: number, ...elems: Uint8Array[]): Uint8Array => tlv(tag, ...[...elems].sort(compareBytes)); export const set = (tag: number, ...elems: Uint8Array[]): Uint8Array => tlv(tag, ...[...elems].sort(compareBytes));
export const octets = (b: Uint8Array): Uint8Array => tlv(0x04, b); export const octets = (b: Uint8Array): Uint8Array => tlv(0x04, b);
export const utf8 = (s: string): Uint8Array => tlv(0x0c, new TextEncoder().encode(s)); export const NULL = Uint8Array.of(5, 0);
export const bool = (v: boolean): Uint8Array => Uint8Array.of(1, 1, v ? 0xff : 0);
export const bitString = (b: Uint8Array): Uint8Array => tlv(0x03, Uint8Array.of(0), b);
export const utcTime = (s: string): Uint8Array => tlv(0x17, te.encode(s));
export const generalizedTime = (s: string): Uint8Array => tlv(0x18, te.encode(s));
// The string types of a name, with the bytes as given: those that break their type too.
export const utf8 = (s: string | Uint8Array): Uint8Array => tlv(0x0c, typeof s === 'string' ? te.encode(s) : s);
export const printable = (s: string | Uint8Array): Uint8Array => tlv(0x13, typeof s === 'string' ? latin1(s) : s);
export const teletex = (s: string | Uint8Array): Uint8Array => tlv(0x14, typeof s === 'string' ? latin1(s) : s);
export const ia5 = (s: string | Uint8Array): Uint8Array => tlv(0x16, typeof s === 'string' ? latin1(s) : s);
export const visible = (s: string): Uint8Array => tlv(0x1a, latin1(s));
export const numeric = (s: string): Uint8Array => tlv(0x12, latin1(s));
export const bmp = (b: Uint8Array): Uint8Array => tlv(0x1e, b);
/** The BMPString of s in UTF-16BE: a code point outside the BMP becomes a surrogate pair. */
export function bmpText(s: string): Uint8Array {
const out: number[] = [];
for (let i = 0; i < s.length; i++) out.push(s.charCodeAt(i) >> 8, s.charCodeAt(i) & 255);
return bmp(Uint8Array.from(out));
}
// One byte per UTF-16 code unit, which must be under 0x100: the bytes of a test of PrintableString and its kin.
const latin1 = (s: string): Uint8Array => Uint8Array.from(s, (c) => c.charCodeAt(0));
export function oid(s: string): Uint8Array { export function oid(s: string): Uint8Array {
const parts = s.split('.').map(BigInt); const parts = s.split('.').map(BigInt);
@ -55,14 +79,99 @@ export function int(n: bigint | number): Uint8Array {
return tlv(0x02, Uint8Array.from(bytes)); return tlv(0x02, Uint8Array.from(bytes));
} }
const hex = (s: string): Uint8Array => Uint8Array.from(s.match(/../g) ?? [], (b) => parseInt(b, 16)); /** An INTEGER with the content given, minimal or not. */
const NULL = Uint8Array.of(5, 0); export const intBytes = (c: Uint8Array): Uint8Array => tlv(0x02, c);
/** The unsigned big-endian bytes of the INTEGER n, as Go's big.Int.Bytes. */
export function bigBytes(n: bigint): Uint8Array {
const h = n.toString(16);
return Uint8Array.from((h.length % 2 === 0 ? h : `0${h}`).match(/../g)!, (x) => parseInt(x, 16));
}
/** An AlgorithmIdentifier. */
export const algID = (o: string, ...params: Uint8Array[]): Uint8Array => seq(oid(o), ...params);
/** An Attribute of the type with the values, in DER order. */
export const attr = (o: string, ...values: Uint8Array[]): Uint8Array => seq(oid(o), set(0x31, ...values));
export const hex = (s: string): Uint8Array => Uint8Array.from(s.match(/../g) ?? [], (b) => parseInt(b, 16));
/** The children of the constructed element b. */
export const children = (b: Uint8Array): Uint8Array[] => splitDer(b).children;
/** The element at path in b: the index of a child at each level. */
export function at(b: Uint8Array, ...path: number[]): Uint8Array {
for (const i of path) b = children(b)[i]!;
return b;
}
/**
* b with the element at path replaced by what f returns for it, and the
* lengths of its ancestors written again: undefined removes the element.
*/
export function edit(b: Uint8Array, f: (old: Uint8Array) => Uint8Array | undefined, ...path: number[]): Uint8Array {
if (path.length === 0) return f(b) ?? new Uint8Array(0);
const kids = children(b);
kids[path[0]!] = edit(kids[path[0]!]!, f, ...path.slice(1));
return tlv(b[0]!, ...kids);
}
/** An edit that changes the identifier octet of an element. */
export const retag =
(tag: number) =>
(b: Uint8Array): Uint8Array =>
concatBytes(Uint8Array.of(tag), b.subarray(1));
/** An edit that adds elements at the end of the children of a constructed element. */
export const append =
(...elems: Uint8Array[]) =>
(b: Uint8Array): Uint8Array =>
tlv(b[0]!, ...children(b), ...elems);
/** The constructed element b with an indefinite length: BER, which DER forbids (X.690 10.1). */
export const indefinite = (b: Uint8Array): Uint8Array => concatBytes(Uint8Array.of(b[0]!, 0x80), derContent(b), Uint8Array.of(0, 0));
/**
* The signature or the token that joins the signatures sigs of one content, as
* a signing application adds a SignerInfo: the digest algorithms and the
* certificates of all, each once, and their SignerInfo, each in DER order.
*/
export function merge(...sigs: Uint8Array[]): Uint8Array {
const algs: Uint8Array[] = [];
const certs: Uint8Array[] = [];
const infos: Uint8Array[] = [];
let version: Uint8Array = new Uint8Array(0);
let content: Uint8Array = new Uint8Array(0);
for (const s of sigs) {
const f = children(at(s, ...SIGNED_DATA));
version = f[0]!;
content = f[2]!;
algs.push(...children(f[1]!));
for (const x of f.slice(3)) {
if (x[0] === 0xa0) certs.push(...children(x));
if (x[0] === 0x31) infos.push(...children(x));
}
}
const once = (e: Uint8Array[]): Uint8Array[] => e.sort(compareBytes).filter((x, i) => i === 0 || compareBytes(e[i - 1]!, x) !== 0);
const fields = [version, tlv(0x31, ...once(algs)), content, ...(certs.length > 0 ? [tlv(0xa0, ...once(certs))] : []), set(0x31, ...infos)];
return seq(oid(OID.signedData), tlv(0xa0, seq(...fields)));
}
/** The Accuracy of RFC 3161 with its seconds, millis and micros, each only when it is not zero. */
export function accuracyOf(seconds: number, millis = 0, micros = 0): Uint8Array {
const small = (n: number): Uint8Array => derContent(int(n));
return seq(...(seconds !== 0 ? [int(seconds)] : []), ...(millis !== 0 ? [tlv(0x80, small(millis))] : []), ...(micros !== 0 ? [tlv(0x81, small(micros))] : []));
}
/** The path of the SignedData in a signature or a token. */
export const SIGNED_DATA = [1, 0] as const;
export const OID = { export const OID = {
data: '1.2.840.113549.1.7.1', data: '1.2.840.113549.1.7.1',
signedData: '1.2.840.113549.1.7.2', signedData: '1.2.840.113549.1.7.2',
contentType: '1.2.840.113549.1.9.3', contentType: '1.2.840.113549.1.9.3',
messageDigest: '1.2.840.113549.1.9.4', messageDigest: '1.2.840.113549.1.9.4',
signingTime: '1.2.840.113549.1.9.5',
sigCertV1: '1.2.840.113549.1.9.16.2.12', sigCertV1: '1.2.840.113549.1.9.16.2.12',
sigCertV2: '1.2.840.113549.1.9.16.2.47', sigCertV2: '1.2.840.113549.1.9.16.2.47',
timeStamp: '1.2.840.113549.1.9.16.2.14', timeStamp: '1.2.840.113549.1.9.16.2.14',
@ -71,6 +180,23 @@ export const OID = {
rsa: '1.2.840.113549.1.1.1', rsa: '1.2.840.113549.1.1.1',
pss: '1.2.840.113549.1.1.10', pss: '1.2.840.113549.1.1.10',
mgf1: '1.2.840.113549.1.1.8', mgf1: '1.2.840.113549.1.1.8',
sha256RSA: '1.2.840.113549.1.1.11',
sha384RSA: '1.2.840.113549.1.1.12',
sha512RSA: '1.2.840.113549.1.1.13',
ecPublicKey: '1.2.840.10045.2.1',
p256: '1.2.840.10045.3.1.7',
p384: '1.3.132.0.34',
p521: '1.3.132.0.35',
brainpoolP256: '1.3.36.3.3.2.8.1.1.7',
sha1: '1.3.14.3.2.26',
commonName: '2.5.4.3',
surname: '2.5.4.4',
serialNumber: '2.5.4.5',
country: '2.5.4.6',
organization: '2.5.4.10',
givenName: '2.5.4.42',
ski: '2.5.29.14',
keyUsage: '2.5.29.15',
ecdsa: { 'SHA-256': '1.2.840.10045.4.3.2', 'SHA-384': '1.2.840.10045.4.3.3', 'SHA-512': '1.2.840.10045.4.3.4' }, ecdsa: { 'SHA-256': '1.2.840.10045.4.3.2', 'SHA-384': '1.2.840.10045.4.3.3', 'SHA-512': '1.2.840.10045.4.3.4' },
sha: { 'SHA-256': '2.16.840.1.101.3.4.2.1', 'SHA-384': '2.16.840.1.101.3.4.2.2', 'SHA-512': '2.16.840.1.101.3.4.2.3' }, sha: { 'SHA-256': '2.16.840.1.101.3.4.2.1', 'SHA-384': '2.16.840.1.101.3.4.2.2', 'SHA-512': '2.16.840.1.101.3.4.2.3' },
} as const; } as const;
@ -78,87 +204,187 @@ export const OID = {
export type HashName = 'SHA-256' | 'SHA-384' | 'SHA-512'; export type HashName = 'SHA-256' | 'SHA-384' | 'SHA-512';
const HASH_SIZE: Record<HashName, number> = { 'SHA-256': 32, 'SHA-384': 48, 'SHA-512': 64 }; const HASH_SIZE: Record<HashName, number> = { 'SHA-256': 32, 'SHA-384': 48, 'SHA-512': 64 };
export const digest = async (h: HashName | 'SHA-1', b: Uint8Array): Promise<Uint8Array> => new Uint8Array(await crypto.subtle.digest(h, bs(b))); export const digest = async (h: HashName | 'SHA-1', b: Uint8Array): Promise<Uint8Array> => new Uint8Array(await crypto.subtle.digest(h, bs(b)));
const hashAlg = (h: HashName): Uint8Array => seq(oid(OID.sha[h])); /** The AlgorithmIdentifier of a hash, without parameters. */
export const hashAlg = (h: HashName | 'SHA-1'): Uint8Array => seq(oid(h === 'SHA-1' ? OID.sha1 : OID.sha[h]));
// ---- signers: a certificate with its private key ------------------------------ /** An attribute whose type has an arc of 2^31: an identifier that the profile does not name, and decides nothing. */
export const bigArcAttr = (): Uint8Array => seq(tlv(0x06, hex('2a864886f70d01098880808000')), set(0x31, NULL));
export interface Signer { // ---- keys --------------------------------------------------------------------
/** The DER of the certificate, its subjectKeyIdentifier, and what a SignerInfo needs of it. */
readonly cert: Uint8Array; /** A key pair: the private key as PKCS #8, the SubjectPublicKeyInfo of crypto/x509, and its kind. */
readonly ski: Uint8Array; export interface Key {
readonly issuer: Uint8Array;
readonly serial: bigint;
/** The private key as PKCS #8, and its kind. */
readonly pkcs8: Uint8Array; readonly pkcs8: Uint8Array;
readonly spki: Uint8Array;
readonly kind: 'rsa' | 'P-256' | 'P-384' | 'P-521'; readonly kind: 'rsa' | 'P-256' | 'P-384' | 'P-521';
} }
/** How the certificate names itself: by commonName, by givenName and surname, or with neither. */ /** A new RSA key of `bits` bits, or ECDSA key on a NIST curve. */
export type NameKind = 'cn' | 'given-surname' | 'organization'; export async function newKey(kind: Key['kind'], bits = 2048): Promise<Key> {
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 pkcs8 = new Uint8Array(await crypto.subtle.exportKey('pkcs8', pair.privateKey));
return { pkcs8, spki, kind };
}
/** The point of an EC key, uncompressed: 0x04 and the two coordinates. */
export const uncompressed = (k: Key): Uint8Array => derContent(children(k.spki)[1]!).subarray(1);
function name(cn: string, kind: NameKind = 'cn'): Uint8Array { /** The point of an EC key, compressed (SEC 1 2.3.3). */
const org = set(0x31, seq(oid('2.5.4.10'), utf8('DateKeys test'))); export function compressed(k: Key): Uint8Array {
if (kind === 'given-surname') return seq(set(0x31, seq(oid('2.5.4.42'), utf8(cn.split(' ')[0]!))), set(0x31, seq(oid('2.5.4.4'), utf8(cn.split(' ')[1] ?? 'X'))), org); const point = uncompressed(k);
if (kind === 'organization') return seq(org); const n = (point.length - 1) / 2;
return seq(set(0x31, seq(oid('2.5.4.3'), utf8(cn))), org); return concatBytes(Uint8Array.of(2 | (point[point.length - 1]! & 1)), point.subarray(1, 1 + n));
}
/** The modulus and the exponent of an RSA key. */
export function rsaPublic(k: Key): { n: bigint; e: bigint } {
const [n, e] = children(derContent(children(k.spki)[1]!).subarray(1)).map((x) => BigInt(`0x0${[...derContent(x)].map((b) => b.toString(16).padStart(2, '0')).join('')}`));
return { n: n!, e: e! };
}
/** The SubjectPublicKeyInfo of id-ecPublicKey with the parameters and the point given. */
export const spkiEC = (params: Uint8Array, point: Uint8Array): Uint8Array => seq(algID(OID.ecPublicKey, params), bitString(point));
/** The SubjectPublicKeyInfo of rsaEncryption with NULL parameters and the modulus and exponent given. */
export const spkiRSA = (n: bigint, e: bigint): Uint8Array => seq(algID(OID.rsa, NULL), bitString(seq(int(n), int(e))));
// ---- certificates ------------------------------------------------------------
/** Signer is a certificate with the private key of its subject. */
export interface Signer {
/** The DER of the certificate, and what a sid names: the DER of its issuer and of its serialNumber, and its keyIdentifier. */
readonly cert: Uint8Array;
readonly issuer: Uint8Array;
readonly serial: Uint8Array;
readonly ski: Uint8Array;
readonly key: Key;
} }
/** An AttributeTypeAndValue. */
export const atv = (type: string, value: Uint8Array): Uint8Array => seq(oid(type), value);
/** A RelativeDistinguishedName of several attributes, in the order given, for nameOf. */
export const rdn = (...atvs: Uint8Array[]): Uint8Array => tlv(0x31, ...atvs);
/** A Name of the RelativeDistinguishedName elements given. */
export const nameOf = (...rdns: Uint8Array[]): Uint8Array => seq(...rdns);
/** A Name with one RelativeDistinguishedName for each attribute, in the order given. */
export const name = (...atvs: Uint8Array[]): Uint8Array => nameOf(...atvs.map((a) => rdn(a)));
/** A Name of one attribute per RDN, each given as its type and the DER of its value. */ /** 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 { export function rdnName(...attributes: [type: string, value: Uint8Array][]): Uint8Array {
return seq(...attributes.map(([type, value]) => set(0x31, seq(oid(type), value)))); return name(...attributes.map(([type, value]) => atv(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. */ /** An Extension, with critical only when it is true, as DER writes it. */
export interface CertOptions { export const extension = (o: string, critical: boolean, value: Uint8Array): Uint8Array => seq(oid(o), ...(critical ? [bool(true)] : []), octets(value));
readonly subject?: Uint8Array; /** The extension subjectKeyIdentifier with the keyIdentifier given. */
export const extSKI = (id: Uint8Array): Uint8Array => extension(OID.ski, false, octets(id));
/** The extension keyUsage with digitalSignature. */
export const extKeyUsage = (): Uint8Array => extension(OID.keyUsage, true, Uint8Array.of(0x03, 0x02, 0x07, 0x80));
/**
* A certificate written field by field, as Go's cmstest.CertSpec: each field
* holds the DER of its element as it goes in the certificate, and an absent
* one takes the default of a certificate of version 3 of the key.
*/
export interface CertSpec {
/** The commonName of the subject, in a UTF8String, when there is no subject. */
readonly cn?: string;
/** The validity, 2020-01-01 to 2040-01-01 by default, as RFC 5280 writes it when notBefore and notAfter are absent. */
readonly from?: Date;
readonly to?: Date;
/** The field [0] of the version, INTEGER 2 by default; null leaves it out, as a certificate of version 1. */
readonly version?: Uint8Array | null;
readonly serial?: Uint8Array;
readonly sigAlg?: Uint8Array;
/** The names; the issuer is the subject by default, as in a self-signed certificate. */
readonly issuer?: Uint8Array; readonly issuer?: Uint8Array;
readonly validity?: Uint8Array; readonly subject?: Uint8Array;
readonly compressed?: boolean; readonly notBefore?: Uint8Array;
readonly notAfter?: Uint8Array;
readonly spki?: Uint8Array;
/** [1] issuerUniqueID and [2] subjectUniqueID, written after the SPKI. */
readonly uniqueIDs?: Uint8Array[];
/** The keyIdentifier of the default subjectKeyIdentifier, the commonName by default. */
readonly ski?: Uint8Array;
/** The Extension elements of [3], subjectKeyIdentifier and keyUsage by default; null writes no [3]. */
readonly extensions?: Uint8Array[] | null;
/** Elements written at the end of tbsCertificate. */
readonly after?: Uint8Array[];
/** The BIT STRING of the signature, which nobody checks. */
readonly signature?: Uint8Array;
} }
function utcTime(t: Date): Uint8Array { /** A time of validity as RFC 5280 4.1.2.5 writes it: UTCTime until 2049 and GeneralizedTime from 2050. */
export function certTime(t: Date): Uint8Array {
const p = (n: number, w = 2): string => String(n).padStart(w, '0'); const p = (n: number, w = 2): string => String(n).padStart(w, '0');
const y = t.getUTCFullYear(); const y = t.getUTCFullYear();
const body = `${p(t.getUTCMonth() + 1)}${p(t.getUTCDate())}${p(t.getUTCHours())}${p(t.getUTCMinutes())}${p(t.getUTCSeconds())}Z`; const body = `${p(t.getUTCMonth() + 1)}${p(t.getUTCDate())}${p(t.getUTCHours())}${p(t.getUTCMinutes())}${p(t.getUTCSeconds())}Z`;
return y < 2050 ? tlv(0x17, new TextEncoder().encode(`${p(y % 100)}${body}`)) : tlv(0x18, new TextEncoder().encode(`${p(y, 4)}${body}`)); return y < 2050 ? utcTime(`${p(y % 100)}${body}`) : generalizedTime(`${p(y, 4)}${body}`);
} }
// The SubjectPublicKeyInfo of an ECDSA key with its point 04 || x || y written compressed, 02 or 03 || x. let serials = 0x1000;
function compressPoint(spki: Uint8Array): Uint8Array {
const [alg, key] = splitDer(spki).children; /** The signer of key whose certificate spec describes. */
const point = derContent(key!).subarray(1); export function newCert(spec: CertSpec, key: Key): Signer {
const x = point.subarray(1, 1 + (point.length - 1) / 2); const subject = spec.subject ?? name(atv(OID.commonName, utf8(spec.cn ?? 'DateKeys test')));
return seq(alg!, tlv(0x03, Uint8Array.of(0, 2 + (point[point.length - 1]! & 1)), x)); const issuer = spec.issuer ?? subject;
const serial = spec.serial ?? int(serials++);
const sigAlg = spec.sigAlg ?? seq(oid(key.kind === 'rsa' ? OID.sha256RSA : OID.ecdsa['SHA-256']));
const ski = spec.ski ?? te.encode(spec.cn ?? 'DateKeys test');
const validity = seq(spec.notBefore ?? certTime(spec.from ?? new Date(Date.UTC(2020, 0, 1))), spec.notAfter ?? certTime(spec.to ?? new Date(Date.UTC(2040, 0, 1))));
const tbs: Uint8Array[] = [];
if (spec.version !== null) tbs.push(spec.version ?? tlv(0xa0, int(2)));
tbs.push(serial, sigAlg, issuer, validity, subject, spec.spki ?? key.spki, ...(spec.uniqueIDs ?? []));
if (spec.extensions !== null) tbs.push(tlv(0xa3, seq(...(spec.extensions ?? [extSKI(ski), extKeyUsage()]))));
tbs.push(...(spec.after ?? []));
const cert = seq(seq(...tbs), sigAlg, spec.signature ?? bitString(Uint8Array.of(1, 2, 3, 4, 5, 6, 7)));
return { cert, issuer, serial, ski, key };
} }
async function signerOf(kind: Signer['kind'], bits: number, cn: string, notBefore: Date, notAfter: Date, nameKind: NameKind = 'cn', o: CertOptions = {}): Promise<Signer> { /** What a test changes in a certificate made by newRSA or newECDSA. */
const algorithm = kind === 'rsa' ? { name: 'RSASSA-PKCS1-v1_5', modulusLength: bits, publicExponent: Uint8Array.of(1, 0, 1), hash: 'SHA-256' } : { name: 'ECDSA', namedCurve: kind }; export interface CertOptions {
const pair = (await crypto.subtle.generateKey(algorithm, true, ['sign', 'verify'])) as CryptoKeyPair; readonly subject?: Uint8Array;
const exported = new Uint8Array(await crypto.subtle.exportKey('spki', pair.publicKey)); readonly issuer?: Uint8Array;
const spki = o.compressed === true ? compressPoint(exported) : exported; readonly validity?: Uint8Array;
const pkcs8 = new Uint8Array(await crypto.subtle.exportKey('pkcs8', pair.privateKey)); /** The point of an ECDSA key, compressed. */
const ski = new TextEncoder().encode(cn); readonly compressed?: boolean;
const issuer = o.issuer ?? name(cn, nameKind); }
const serial = BigInt(Math.floor(Math.random() * 2 ** 40) + 1);
const tbs = seq( /** How a certificate of newRSA or newECDSA names itself: by commonName, by givenName and surname, or with neither. */
tlv(0xa0, int(2)), export type NameKind = 'cn' | 'given-surname' | 'organization';
int(serial),
seq(oid(OID.ecdsa['SHA-256'])), function nameOfKind(cn: string, kind: NameKind): Uint8Array {
issuer, const org = atv(OID.organization, utf8('DateKeys test'));
o.validity ?? seq(utcTime(notBefore), utcTime(notAfter)), if (kind === 'given-surname') return name(atv(OID.givenName, utf8(cn.split(' ')[0]!)), atv(OID.surname, utf8(cn.split(' ')[1] ?? 'X')), org);
o.subject ?? name(cn, nameKind), if (kind === 'organization') return name(org);
spki, return name(atv(OID.commonName, utf8(cn)), org);
tlv(0xa3, seq(seq(oid('2.5.29.14'), octets(octets(ski))))), }
async function signerOf(kind: Key['kind'], bits: number, cn: string, from: Date, to: Date, nameKind: NameKind = 'cn', o: CertOptions = {}): Promise<Signer> {
const key = await newKey(kind, bits);
const subject = o.subject ?? nameOfKind(cn, nameKind);
return newCert(
{
cn,
from,
to,
subject,
issuer: o.issuer ?? subject,
...(o.validity === undefined ? {} : { notBefore: children(o.validity)[0]!, notAfter: children(o.validity)[1]! }),
...(o.compressed === true ? { spki: spkiEC(oid(kind === 'P-256' ? OID.p256 : kind === 'P-384' ? OID.p384 : OID.p521), compressed(key)) } : {}),
extensions: [extSKI(te.encode(cn))],
},
key,
); );
const cert = seq(tbs, seq(oid(OID.ecdsa['SHA-256'])), tlv(0x03, Uint8Array.of(0, 1, 2, 3, 4, 5, 6, 7)));
return { cert, ski, issuer, serial, pkcs8, kind };
} }
/** A signer with an RSA key of `bits` bits. */ /** 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); export const newRSA = (cn: string, bits: number, from: Date, to: Date): Promise<Signer> => signerOf('rsa', bits, cn, from, to);
/** A signer with an ECDSA key on a NIST curve. */ /** 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', o: CertOptions = {}): Promise<Signer> => export const newECDSA = (cn: string, curve: 'P-256' | 'P-384' | 'P-521', from: Date, to: Date, nameKind: NameKind = 'cn', o: CertOptions = {}): Promise<Signer> =>
signerOf(curve, 0, cn, notBefore, notAfter, nameKind, o); signerOf(curve, 0, cn, from, to, nameKind, o);
// ECDSA from WebCrypto is r || s; a CMS signature is the DER of the two integers. // ECDSA from WebCrypto is r || s; a CMS signature is the DER of the two integers.
function ecdsaDER(raw: Uint8Array): Uint8Array { function ecdsaDER(raw: Uint8Array): Uint8Array {
@ -172,114 +398,145 @@ function ecdsaDER(raw: Uint8Array): Uint8Array {
return seq(unsigned(raw.subarray(0, half)), unsigned(raw.subarray(half))); return seq(unsigned(raw.subarray(0, half)), unsigned(raw.subarray(half)));
} }
async function sign(s: Signer, o: Options, data: Uint8Array): Promise<Uint8Array> { // Signs data with the key: the signatureAlgorithm of the key and the signature value.
async function sign(k: Key, o: Options, data: Uint8Array): Promise<{ sigAlg: Uint8Array; sig: Uint8Array }> {
const hash = o.hash ?? 'SHA-256'; const hash = o.hash ?? 'SHA-256';
if (s.kind === 'rsa') { if (k.kind === 'rsa') {
const name = o.pss ? 'RSA-PSS' : 'RSASSA-PKCS1-v1_5'; const algorithm = o.pss ? 'RSA-PSS' : 'RSASSA-PKCS1-v1_5';
const key = await crypto.subtle.importKey('pkcs8', bs(s.pkcs8), { name, hash }, false, ['sign']); const key = await crypto.subtle.importKey('pkcs8', bs(k.pkcs8), { name: algorithm, hash }, false, ['sign']);
return new Uint8Array(await crypto.subtle.sign(o.pss ? { name, saltLength: HASH_SIZE[hash] } : { name }, key, bs(data))); const sig = new Uint8Array(await crypto.subtle.sign(o.pss ? { name: algorithm, saltLength: HASH_SIZE[hash] } : { name: algorithm }, key, bs(data)));
if (!o.pss) return { sigAlg: algID(OID.rsa, NULL), sig };
const params = [tlv(0xa0, hashAlg(hash)), tlv(0xa1, algID(OID.mgf1, hashAlg(hash))), tlv(0xa2, int(HASH_SIZE[hash]))];
return { sigAlg: algID(OID.pss, seq(...params, ...(o.pssTrailer ? [tlv(0xa3, int(1))] : []))), sig };
} }
const key = await crypto.subtle.importKey('pkcs8', bs(s.pkcs8), { name: 'ECDSA', namedCurve: s.kind }, false, ['sign']); const key = await crypto.subtle.importKey('pkcs8', bs(k.pkcs8), { name: 'ECDSA', namedCurve: k.kind }, false, ['sign']);
return ecdsaDER(new Uint8Array(await crypto.subtle.sign({ name: 'ECDSA', hash }, key, bs(data)))); return { sigAlg: algID(OID.ecdsa[hash]), sig: ecdsaDER(new Uint8Array(await crypto.subtle.sign({ name: 'ECDSA', hash }, key, bs(data)))) };
} }
// ---- the signature ---------------------------------------------------------- // ---- the signature ----------------------------------------------------------
/**
* What signature and token write, to make signatures that the profile rejects
* or that verify to something else. The empty options write what AutoFirma
* writes.
*/
export interface Options { export interface Options {
/** The digest of the signature, SHA-256 by default. */ /** The digest of the signature, SHA-256 by default. */
hash?: HashName; readonly hash?: HashName;
/** Signs with RSASSA-PSS instead of PKCS #1 v1.5. */ /** Signs with RSASSA-PSS instead of PKCS #1 v1.5, and writes trailerField [3] 1, its default, which DER does not write. */
pss?: boolean; readonly pss?: boolean;
/** Writes trailerField [3] 1 in the PSS parameters, which is its default and DER does not write it. */ readonly pssTrailer?: boolean;
pssTrailer?: boolean;
/** Names the signer by subjectKeyIdentifier instead of by issuer and serial. */ /** Names the signer by subjectKeyIdentifier instead of by issuer and serial. */
ski?: boolean; readonly ski?: boolean;
/** Gives the time-stamp token of the signature that Build wants as an unsigned attribute. */ /** Another number than the version that RFC 5652 gives a SignerInfo. */
token?: (signature: Uint8Array) => Promise<Uint8Array>; readonly version?: number;
/** Written as the digestAlgorithm and as the signatureAlgorithm instead of those of the hash and of the key. */
readonly digestAlg?: Uint8Array;
readonly sigAlg?: Uint8Array;
/** Flips a bit of the signature value, after signing. */
readonly corruptSignature?: boolean;
/** What the message-digest covers, when not the signed message. */ /** What the message-digest covers, when not the signed message. */
message?: Uint8Array; readonly message?: Uint8Array;
/** Edits the signedAttrs, as a list of the DER of each attribute, before they are signed. */ /** Edits the signedAttrs, as a list of the DER of each attribute, before they are signed. */
mutate?: (attrs: Uint8Array[]) => Uint8Array[]; readonly mutate?: (attrs: Uint8Array[]) => Uint8Array[];
/** Adds a signing-certificate beside the v2, or writes it alone in a signature; writes signing-certificate-v2 in a token. */
readonly sigCertV1?: boolean;
readonly noSigCertV2?: boolean;
readonly sigCertV2?: boolean;
/**
* The hashAlgorithm of the ESSCertIDv2, which DER leaves out for SHA-256,
* the hash of its certHash, SHA-256 by default, and the certificate whose
* hash it gives instead of that of the signer.
*/
readonly essHashAlg?: Uint8Array;
readonly essDigest?: HashName | 'SHA-1';
readonly essCert?: Uint8Array;
/** Leaves the message-digest out. */
readonly noMessageDigest?: boolean;
/** Added to the signed attributes, and to the unsigned attributes, as the DER of each. */
readonly extraAttrs?: Uint8Array[];
readonly extraUnsigned?: Uint8Array[];
/** Leaves the signedAttrs in the order they are given, not in DER order. */
readonly unsorted?: boolean;
/** Gives the time-stamp token of the signature that signature puts as an unsigned attribute. */
readonly token?: (signature: Uint8Array) => Promise<Uint8Array>;
/** /**
* Puts a second signature-time-stamp beside the token, to break the * 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 * 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, * attribute. It is a ContentInfo of id-data, which the token must not be,
* so that the SET OF stays in DER order. * so that the SET OF stays in DER order.
*/ */
token2?: 'attribute' | 'value'; readonly token2?: 'attribute' | 'value';
/** Adds this response in crls. */
ocsp?: Uint8Array;
/** The elements of crls as they are, instead of an OCSP response. */
crls?: Uint8Array[];
/** Leaves the signedAttrs in the order they are given, not in DER order. */
unsorted?: boolean;
/** Leaves the certificate of the signer out of certificates. */
omitCert?: boolean;
/** Adds an unsigned attribute of this many bytes, which decides nothing. */ /** Adds an unsigned attribute of this many bytes, which decides nothing. */
junk?: number; readonly junk?: number;
/** Adds a signing-certificate attribute beside the v2. */ /** Leaves the certificate of each signer out of certificates, and adds these. */
sigCertV1?: boolean; readonly omitCert?: boolean;
/** Added to the signed attributes, as the DER of each. */ readonly extraCerts?: Uint8Array[];
extraAttrs?: Uint8Array[]; /** Adds this response in crls, and the elements of crls as they are. */
/** Another number than the version that RFC 5652 gives a SignerInfo. */ readonly ocsp?: Uint8Array;
version?: number; readonly crls?: Uint8Array[];
/** Writes each SignerInfo twice, and signerInfos in descending order. */
readonly signerInfoTwice?: boolean;
readonly unsortedInfos?: boolean;
/** Edits the fields of each SignerInfo after it is signed. */
readonly editSignerInfo?: (fields: Uint8Array[]) => Uint8Array[];
} }
const attr = (o: string, ...values: Uint8Array[]): Uint8Array => seq(oid(o), set(0x31, ...values));
function encap(content: Uint8Array, token: boolean): Uint8Array { function encap(content: Uint8Array, token: boolean): Uint8Array {
return token ? seq(oid(OID.tstInfo), tlv(0xa0, octets(content))) : seq(oid(OID.data)); return token ? seq(oid(OID.tstInfo), tlv(0xa0, octets(content))) : seq(oid(OID.data));
} }
// The SigningCertificate (v1, SHA-1) or the SigningCertificateV2 of a certificate.
async function essCertID(cert: Uint8Array, o: Options, v2: boolean): Promise<Uint8Array> {
const c = o.essCert ?? cert;
if (!v2) return seq(seq(seq(octets(await digest('SHA-1', c)))));
const h = octets(await digest(o.essDigest ?? 'SHA-256', c));
return seq(seq(o.essHashAlg === undefined ? seq(h) : seq(o.essHashAlg, h)));
}
async function signerInfo(message: Uint8Array, o: Options, token: boolean, s: Signer): Promise<Uint8Array> { async function signerInfo(message: Uint8Array, o: Options, token: boolean, s: Signer): Promise<Uint8Array> {
const hash = o.hash ?? 'SHA-256'; const hash = o.hash ?? 'SHA-256';
const sid = o.ski ? tlv(0x80, s.ski) : seq(s.issuer, int(s.serial)); const sid = o.ski ? tlv(0x80, s.ski) : seq(s.issuer, s.serial);
const attrs: Uint8Array[] = [ const attrs: Uint8Array[] = [attr(OID.contentType, oid(token ? OID.tstInfo : OID.data))];
attr(OID.contentType, oid(token ? OID.tstInfo : OID.data)), if (!o.noMessageDigest) attrs.push(attr(OID.messageDigest, octets(await digest(hash, o.message ?? message))));
attr(OID.messageDigest, octets(await digest(hash, o.message ?? message))), if (token && !o.sigCertV2) attrs.push(attr(OID.sigCertV1, await essCertID(s.cert, o, false)));
]; else if (!o.noSigCertV2) attrs.push(attr(OID.sigCertV2, await essCertID(s.cert, o, true)));
const certSha1 = await digest('SHA-1', s.cert); if (o.sigCertV1) attrs.push(attr(OID.sigCertV1, await essCertID(s.cert, {}, false)));
attrs.push(token ? attr(OID.sigCertV1, seq(seq(seq(octets(certSha1))))) : attr(OID.sigCertV2, seq(seq(seq(octets(await digest('SHA-256', s.cert)))))));
if (o.sigCertV1) attrs.push(attr(OID.sigCertV1, seq(seq(seq(octets(certSha1))))));
attrs.push(...(o.extraAttrs ?? [])); attrs.push(...(o.extraAttrs ?? []));
const list = o.mutate === undefined ? attrs : o.mutate(attrs); const list = o.mutate === undefined ? attrs : o.mutate(attrs);
const signed = o.unsorted ? tlv(0xa0, ...list) : set(0xa0, ...list); const signed = o.unsorted ? tlv(0xa0, ...list) : set(0xa0, ...list);
const forSig = concatBytes(Uint8Array.of(0x31), signed.subarray(1)); // The signature covers the signedAttrs with the tag of a SET.
const signature = await sign(s, o, forSig); const { sigAlg, sig } = await sign(s.key, o, concatBytes(Uint8Array.of(0x31), signed.subarray(1)));
if (o.corruptSignature) {
let sigAlg: Uint8Array; const i = sig.length >> 1;
if (s.kind === 'rsa') { sig[i] = sig[i]! ^ 1;
if (o.pss) {
const base = [tlv(0xa0, hashAlg(hash)), tlv(0xa1, seq(oid(OID.mgf1), hashAlg(hash))), tlv(0xa2, int(HASH_SIZE[hash]))];
sigAlg = seq(oid(OID.pss), seq(...base, ...(o.pssTrailer ? [tlv(0xa3, int(1))] : [])));
} else {
sigAlg = seq(oid(OID.rsa), NULL);
}
} else {
sigAlg = seq(oid(OID.ecdsa[hash]));
} }
const f = [int(o.version ?? (o.ski ? 3 : 1)), sid, hashAlg(hash), signed, sigAlg, octets(signature)]; const f = [int(o.version ?? (o.ski ? 3 : 1)), sid, o.digestAlg ?? hashAlg(hash), signed, o.sigAlg ?? sigAlg, octets(sig)];
const unsigned: Uint8Array[] = []; const unsigned: Uint8Array[] = [];
if (o.junk !== undefined && o.junk > 0) unsigned.push(attr('1.2.3.4.5', octets(new Uint8Array(o.junk)))); if (o.junk !== undefined && o.junk > 0) unsigned.push(attr('1.2.3.4.5', octets(new Uint8Array(o.junk))));
if (o.token !== undefined) { if (o.token !== undefined) {
const t = await o.token(signature); const t = await o.token(sig);
const second = seq(oid(OID.data)); const second = seq(oid(OID.data));
if (o.token2 === 'attribute') unsigned.push(attr(OID.timeStamp, t), attr(OID.timeStamp, second)); 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)); else unsigned.push(o.token2 === 'value' ? attr(OID.timeStamp, t, second) : attr(OID.timeStamp, t));
} }
unsigned.push(...(o.extraUnsigned ?? []));
if (unsigned.length > 0) f.push(set(0xa1, ...unsigned)); if (unsigned.length > 0) f.push(set(0xa1, ...unsigned));
return seq(...f); return seq(...(o.editSignerInfo === undefined ? f : o.editSignerInfo(f)));
} }
async function build(message: Uint8Array, o: Options, token: boolean, signers: Signer[]): Promise<Uint8Array> { async function build(content: Uint8Array, o: Options, token: boolean, signers: Signer[]): Promise<Uint8Array> {
const hash = o.hash ?? 'SHA-256'; const certs = [...(o.omitCert ? [] : signers.map((s) => s.cert)), ...(o.extraCerts ?? [])];
const certs = o.omitCert ? [] : signers.map((s) => s.cert); const infos: Uint8Array[] = [];
const infos = await Promise.all(signers.map((s) => signerInfo(message, o, token, s))); for (const s of signers) {
const body: Uint8Array[] = [int(1), set(0x31, hashAlg(hash)), encap(message, token)]; const info = await signerInfo(content, o, token, s);
infos.push(info, ...(o.signerInfoTwice ? [info] : []));
}
const body: Uint8Array[] = [int(1), set(0x31, hashAlg(o.hash ?? 'SHA-256')), encap(content, token)];
if (certs.length > 0) body.push(set(0xa0, ...certs)); if (certs.length > 0) body.push(set(0xa0, ...certs));
if (o.crls !== undefined) body.push(set(0xa1, ...o.crls)); const crls = [...(o.ocsp === undefined ? [] : [tlv(0xa1, oid(OID.ocsp), o.ocsp)]), ...(o.crls ?? [])];
else if (o.ocsp !== undefined) body.push(set(0xa1, tlv(0xa1, oid(OID.ocsp), o.ocsp))); if (crls.length > 0) body.push(set(0xa1, ...crls));
body.push(set(0x31, ...infos)); body.push(o.unsortedInfos ? tlv(0x31, ...[...infos].sort((a, b) => compareBytes(b, a))) : set(0x31, ...infos));
return seq(oid(OID.signedData), tlv(0xa0, seq(...body))); return seq(oid(OID.signedData), tlv(0xa0, seq(...body)));
} }
@ -291,39 +548,43 @@ export function signature(message: Uint8Array, o: Options, ...signers: Signer[])
// ---- the token -------------------------------------------------------------- // ---- the token --------------------------------------------------------------
export interface TokenOptions { export interface TokenOptions {
hash?: HashName; /** The hash of the messageImprint, SHA-256 by default. */
/** Whole seconds; 0 for none. */ readonly hash?: HashName | 'SHA-1';
accuracySeconds?: number; /** The Accuracy element as written: none by default. */
readonly accuracy?: Uint8Array;
/** The version of the TSTInfo, 1 by default. */ /** The version of the TSTInfo, 1 by default. */
version?: number; readonly version?: number;
/** Written as the hashed message instead of the hash of the subject. */ /** Written as the hashed message instead of the hash of the subject, of any length. */
imprint?: Uint8Array; readonly imprint?: Uint8Array;
/** Written as genTime instead of the GeneralizedTime of the time given. */
readonly genTimeRaw?: Uint8Array;
/** Elements written after the accuracy: ordering, nonce, tsa and extensions, or anything after the last field. */
readonly after?: Uint8Array[];
/** The options of the SignedData of the token; its hash is the digest of the signature of the authority. */
readonly cms?: Options;
} }
export function generalizedTime(s: string): Uint8Array { /** The GeneralizedTime of t as DER writes it: the fraction of a second only when there is one. */
return tlv(0x18, new TextEncoder().encode(s)); export function genTimeOf(t: Date): Uint8Array {
const p = (n: number, w = 2): string => String(n).padStart(w, '0');
const ms = t.getUTCMilliseconds();
const frac = ms === 0 ? '' : `.${p(ms, 3).replace(/0+$/, '')}`;
return generalizedTime(`${p(t.getUTCFullYear(), 4)}${p(t.getUTCMonth() + 1)}${p(t.getUTCDate())}${p(t.getUTCHours())}${p(t.getUTCMinutes())}${p(t.getUTCSeconds())}${frac}Z`);
} }
/** The TSTInfo of a token over `subject` at `genTime`, with the fields after genTime that the test gives. */ /** The TSTInfo of a token over `subject` at `genTime`. */
export async function tstInfo(subject: Uint8Array, genTime: Uint8Array, o: TokenOptions = {}, ...after: Uint8Array[]): Promise<Uint8Array> { export async function tstInfo(subject: Uint8Array, genTime: Date, o: TokenOptions = {}): Promise<Uint8Array> {
const hash = o.hash ?? 'SHA-256'; const hash = o.hash ?? 'SHA-256';
return seq(int(o.version ?? 1), oid('1.2.3.4'), seq(hashAlg(hash), octets(o.imprint ?? (await digest(hash, subject)))), int(42), genTime, ...after); const fields = [int(o.version ?? 1), oid('1.2.3.4'), seq(hashAlg(hash), octets(o.imprint ?? (await digest(hash, subject)))), int(42), o.genTimeRaw ?? genTimeOf(genTime)];
} return seq(...fields, ...(o.accuracy === undefined ? [] : [o.accuracy]), ...(o.after ?? []));
export function genTimeOf(t: Date): Uint8Array {
const p = (n: number, w = 2): string => String(n).padStart(w, '0');
return generalizedTime(`${p(t.getUTCFullYear(), 4)}${p(t.getUTCMonth() + 1)}${p(t.getUTCDate())}${p(t.getUTCHours())}${p(t.getUTCMinutes())}${p(t.getUTCSeconds())}Z`);
} }
/** The RFC 3161 token that `tsa` issues over `subject` at `genTime`. */ /** The RFC 3161 token that `tsa` issues over `subject` at `genTime`. */
export async function token(subject: Uint8Array, genTime: Date, o: TokenOptions, tsa: Signer): Promise<Uint8Array> { export async function token(subject: Uint8Array, genTime: Date, o: TokenOptions, tsa: Signer): Promise<Uint8Array> {
const after = o.accuracySeconds === undefined || o.accuracySeconds === 0 ? [] : [seq(int(o.accuracySeconds))]; return build(await tstInfo(subject, genTime, o), o.cms ?? {}, true, [tsa]);
return build(await tstInfo(subject, genTimeOf(genTime), o, ...after), {}, true, [tsa]);
} }
/** A token that `tsa` signs over the given TSTInfo, as it is. */ /** A token that `tsa` signs over the given TSTInfo, as it is. */
export function tokenRaw(info: Uint8Array, tsa: Signer, o: Options = {}): Promise<Uint8Array> { export function tokenRaw(info: Uint8Array, tsa: Signer, o: Options = {}): Promise<Uint8Array> {
return build(info, o, true, [tsa]); return build(info, o, true, [tsa]);
} }
export { hex };

@ -204,15 +204,25 @@
"r": "5ab044fad730f0470dfe574e1edd192065ff12ee04909e8cfe90ebc762cf5df7", "r": "5ab044fad730f0470dfe574e1edd192065ff12ee04909e8cfe90ebc762cf5df7",
"file_key": "b6a845d405e6a47733b42802de903b6c" "file_key": "b6a845d405e6a47733b42802de903b6c"
}, },
{
"name": "format3_note",
"round": 1000,
"signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39",
"body": "87b08e7e96c14d8d9b2583e2c4dfb97f3be23dc398688fffae6a6ba732b0c837fe6d938d6aca444f8d906362087fbc6710eeaf38d7b1eb6bc5f9763f95e521c81b83205ffbbfcf20662ffeb3885d18861623025aee72ae56fded5fce5fdc2fed4433820be2af9f7915fc7ebc10cea1413f223d0a8e7b1821e01633e294389fdb",
"gt": "0a8cad7119881e899628bc141a39032df014815233e97976c3c35166b48e3ed2defd750a4356197e1e8a3b182e6b02d20574e6b1ade3219512b8999ead6a99407f1dabc35af7403e2038bdcb8db8f2cb6216f58643b5c20efe9b6c93d3d36ba50ac889ec48232f54b01709b5aa4118e41925de0791d148641db15399a3dc17d11f320cba4930f73abbe2cec7968324c004e544380de283b6af615402a572b9968a476daa608837465e3255cb48fb5f164f1d87bbbeb4f2f6c56840e6b7d3a5610637ca6f7227b6f28ea67554f969c6f625a2c59d6b8c094076fe881afc5ec89ef5a49ec4bd837d609e52715bf972eef4130620128d79e881c547cd3e0cd13981b225cf7dc5c997093099776688d2dd0cdb7ee824bd2d64a446d7d473413de02b0432bb1ce86063a1303160bc0c96c6c349a023713cb71b84464dd3bfe65e3ef90ce38c8e979d9d6175ca4e634969984a0dc4e49baf78013e72afce3261f905bb48eb02cd2088d8d2a003a0a3e52513c5cff88c46a17b74d8174b6b0617a716e1121d8af63061050385b513b489da2f006b0f696f458ac4c7eca7ac5fd17a80cd7b15523b3a2d5c3f8cf606f8748975e21522bd3986f51d6b92c2890ef22ebd0cb21cb624307a32f5324836589e818ae03c8bcfa5c5df23175af0b2cddfd33f881594eacd8ea222c17782ed13ebffc000391dea5ce30224acff03fc2625d3d9081f81e5ce0c8be1d8c6fa4a472cbe391a11b3a9e99060d11cfcce19276e1551d4fb88b945edd2e7373e023e882e69b71f08af192c027a7d9b63be7bfecea7e667",
"sigma": "b2c582bb1c4e8836a2f48928b57b9171",
"r": "20d56c4f75f533b82c98fe93f6be49ca4da839c0a0a06f14cd9a2a05cf9f18b1",
"file_key": "379701714ecce98406fb439ce0ca0453"
},
{ {
"name": "format3_seal_unsupported", "name": "format3_seal_unsupported",
"round": 2000, "round": 2000,
"signature": "b6cb8f482a0b15d45936a4c4ea08e98a087e71787caee3f4d07a8a9843b1bc5423c6b3c22f446488b3137eaca799c77e", "signature": "b6cb8f482a0b15d45936a4c4ea08e98a087e71787caee3f4d07a8a9843b1bc5423c6b3c22f446488b3137eaca799c77e",
"body": "8f2cb7577b036e54ca2019d01c6941d0ea9f18e3caade81e41c8f77f1514c0a5c75583d6372db607f902d5056c942b0c16c48999c5897a1b44953499a2da59eabc7fbd6ec27886882ce0bf2becdb259b8c62b940dcdfa43865e8de37c74dc49bffa90f04530e33134cc4ee5a843aae112b49ea99eaefc5645b5f963866ed94cb", "body": "93a5434ca155ecf9b350f726ce5ec9ea6084df2f304b89e46ae2e20cff92325f8916b2a88102a323d099acc2e201cc700a4aeb2dc2b99b4b2ccd7b21a60368b913ff283b1757fd8f500dc6f7d6520ff473ca226801fed6a5e743e73bc1d22a5214c0f6f22dfc4bb45d7bf307beb9356bac7676714d86adab40d2713732017226",
"gt": "0a6e979e201b2e75f93ab0af0d266ac7cc8a3617b6aff1e69b65b22dc64055057ce04a5b011c9575306692d12c7673c300aa12d22ecfbf09672bb412f563e8fded84b9a21751c88acffe5802d9a7df55a66be413cd3d2212633403270b96ad32142cc55923292008525f833f2d7b747dbb6f9c7653e219bff4e9c9e0516ebef129884263c4121939b278118d94a42d901045dc777ad453d5d0fe2733da0d22659d61391e6bce2b2cf285ccda5ccbd4d55c7b4d6173131e7fc67d9abd4849f67916e95986d61a47008f4c999f4181b6bf252ad1e82a4e8d28b197d76c08d533427159b66b7e1b2f9b07fda931aa2c5055059ec52f03addc43093aa3e4a9128f8853f2f13cafadf09ceb83a6e2a03c4c289fca5a290db1f1ff03037ad33fee894a0ad29276146cbccdf7433b8c24a1be750355e7c4f9c5d8ffc9824596530677d7fbe31da1424572493ab046340201c20708ae6c523eee32e21fc5ecd62de2fd764180372f617b7fb3fc960554cc4594fa31d74041a3fda4c1291a47a853bcbd3714d9735ee0b398de27283f571d68e37dfcf947d135c0f8db36556318a580de154f874271a4ebdde23bb966d3ef9977640e3b0943c1ee44872ddd379b2a307d1c3baae21bf9546b625f6f2a0aa11bd15c63df7d2315cfa16cf1b59629babca09818e8baab411f7943b1d16ca53df07a417eaeb19b1fb48c3451fe53c0b8d00398872b83f6836e7dc37b3005624488cf81123bfc106d79e3f41e8cfc338b9f960153b4f7a011d90cd0eb81ef3bf6264be3fb165f8989621ee8543ca1741e87091d", "gt": "15b84589e7e0f18ceeab63b13c8f603f44a9899d7d9deb81c63be1277f76cb3109bb1ea6eafc42f5991832480e66f1d702eaee475d26b929e8648433229033bf199399554e907f667abea2896f8593e733b8d3211f5fd03f40ee70bda609d39903a52dac9b57d570448feeffd440f45ea74f9648d78675471b8d5d20affba8c75187a54e1f038aae3daa1ad7b805f7db05ea8b3c8abd9f131582fafb2e4028fe31e7afbe882b7482397a37900a5c38896f6795579b7ecef99676273b8053447a0f612b10866a299edf800e95da4a709850ebf11ba39106aaa64265ddc9511568bbd1396ad945d35afdd7b498c52fe0c4021905939a5215f603e2880d7025f95ea97996fdca9c2eafd732b75224de9c96b97cf5d6ccd5e1656332aa8355d1e46906740d627eca450c4d5fb65cad2e8cdacc993307c029bd4fa11e9edc512b3bcfc14642bb2cf485f8a0518f7c8815f9b102707181ba8c41622ebdb7a8d5904e21c2eb291166aa3f67e9ab15838cbb570c675a47fd9dd86e1f4f2f98d81a07943e17d44a25ded09d960f2caabf48ba676b5f8c5958e4a662f1d327b0b6de4eb45c615db711f6f5599ec2958b43853e4b730a17ec3400e4617e8844b4f4f7b5b8dafa9dc3558b9b031fce0455a484ccf8ca28c506feed7901fb4ee606c36a8fb15402cc3e0075c6cee1e761cadd997592039b6b5ea360749c308715a213356bab8a462ad4e1d660bbc79cd45cc9a7554a2a09d94880fa2be9784bd8e6756609a6a57a7e7f37c5f2a0d02a16cf9ee211e310250dc25829ad458a25cfc950dcdf76c0",
"sigma": "25fedf2e4e3d2db30d94a0b468b90e38", "sigma": "7d4e1a34b1efa321d28f3a92610105db",
"r": "51d85fb4996c5c96c33e15def42a07200863bcda50cb31b102ac1e062e361e8a", "r": "56c9a2ac8036740efd7783fe69c82918a0e53f59f46a291221599ab6fb1af1f2",
"file_key": "ad1ef55f85be2eb6be3b14c2e29d3b10" "file_key": "09b989caf1a98ac7ae25a179b6031a16"
}, },
{ {
"name": "format3_sealed", "name": "format3_sealed",
@ -294,6 +304,16 @@
"r": "3713ea67697e81dd87689bee1b1eb4171f4fc528695efdab1fa0b6ae2a874cb1", "r": "3713ea67697e81dd87689bee1b1eb4171f4fc528695efdab1fa0b6ae2a874cb1",
"file_key": "7bfa8b9f99c10342f5c503851d2f4514" "file_key": "7bfa8b9f99c10342f5c503851d2f4514"
}, },
{
"name": "format3_unsigned",
"round": 1000,
"signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39",
"body": "99d7466198d61841a563c20edd92f3f86816bc66013a39b009f9ca4c421d5135f865c27e33e381d28bf1a580578a440612f390ae7794a6a2e1c9c96814b609124072c9fc069d6d56088ea8a80b5780bb3ae004d06a8f4148f09987021cc7b4274aabe6b9b47227c06898be7b0e507616c2c411d82479aa3764d359f8594da62b",
"gt": "0b625ef2dff31c7918429758e9b8c6b4e1e1548af905c81fecfad5b94e7455404e75c8da7e54e44feebdaebbe6afff7115f7d2262b772d909ad5d3bfac175a67dacf014b1fd4700c6cbcbca2349ac46ba666a317dae752e1b943b434ea90452c181a7ea93aaade0912a022450088280b2787fff54074a9f3d8622648f547a1bfe8ba8998049203537ad09c556869a2860ab38a1a381357ada1bd84c739e7c82c43dbf47590afcbbc4710549ff405b23c13af6445d734f649ec2802a7d5d5cb6105c63438a932ffe386738167d1cc1221afb2074238c81d039c9c2b2209f1d7a8bc53c72b22d09cc32387f562d9d9a194080fb9ee228b21f6047669e41c6d8e784dc4862e63f17d940f2a5ad810ea308d042a6d85ea18154a9643329bcfa9573d16edcfbd817aae838bfb6aa131644990c087645d9a292ff7e01fc17299fe3855caf277a199c7d88976d771483845f77f1058333ac3d1edde99d79ec17f6bfeb637d63661a7fce863a825cabd8d2b3f3b32448caad9222918568d4f67547023bb07a4a0d356114eda7a0b26bc818ce30acebb61dcc4c9454820ffcec05255cd77ff646c9163cd291d3ca9df2fe3de0cd01524a7ae80ebb69208a359aafa67e6fa15c290cc5c6288b9a22f1c45625afa1d370cd525e235306610cea286eaf247211884d80615fa04505af7de711de653f1274ecccfe49884d51771b2c4fb305d1c409af98e5f88783c6b02487a5379a2de060f65d6d52da8665a476d8694f10f1db4775bf4c29683f411c3aa2a3a0c0002a0be27527210830741262048ef482f65",
"sigma": "5133dedc94826a782c2cb6422f627f3f",
"r": "3e80dfc23411c8d4bfa164e6a252a304df0785b1c8769e65c5bf4f90fbd3c951",
"file_key": "906b5caf364f0833e74252b4dcca445e"
},
{ {
"name": "time_and_key_portable", "name": "time_and_key_portable",
"round": 1000, "round": 1000,

@ -828,9 +828,41 @@
"text": "ok" "text": "ok"
}, },
{ {
"name": "a seal of seal_type 1 opens with the verdict S1", "name": "a seal of seal_type 4294967295 opens with the verdict S1",
"text": "ok" "text": "ok"
}, },
{
"name": "the signature of alg 1 altered opens with the verdict F2",
"text": "ok"
},
{
"name": "the signature of alg 1 removed opens with the verdict F0",
"text": "ok"
},
{
"name": "the signature of alg 1 made again with another key opens with the verdict F4 of that key",
"text": "ok"
},
{
"name": "the signature of alg 1 transplanted to another capsule opens with the verdict F2",
"text": "ok"
},
{
"name": "a key of 31 bytes in a signature of alg 1 opens with the verdict F1",
"text": "ok"
},
{
"name": "a signature of 65 bytes of alg 1 opens with the verdict F1",
"text": "ok"
},
{
"name": "the area widened to 64 KiB after signing opens with the verdict F4 and the same AUTHOR_MESSAGE",
"text": "ok"
},
{
"name": "the public note changed in PUBLIC_HEADER",
"text": "capsule: header_binding does not match PRELUDE || PUBLIC_HEADER: ERR_HEADER_BINDING"
},
{ {
"name": "format 3 time_only relabeled format 1", "name": "format 3 time_only relabeled format 1",
"text": "capsule: CONTROL_CBOR: codec: datekeys-control schema version 3, want 1: ERR_UNSUPPORTED_VERSION" "text": "capsule: CONTROL_CBOR: codec: datekeys-control schema version 3, want 1: ERR_UNSUPPORTED_VERSION"

@ -34,7 +34,7 @@ import { readFileSync } from 'node:fs';
import { describe, expect, it } from 'vitest'; import { describe, expect, it } from 'vitest';
import { decodeAccessKey, decodeAccessKeyBody, marshalAccessKeyBody, wipeAccessKey } from './accesskey.ts'; import { decodeAccessKey, decodeAccessKeyBody, marshalAccessKeyBody, wipeAccessKey } from './accesskey.ts';
import { checkCompressedPoint } from './bls12381.ts'; import { checkCompressedPoint } from './bls12381.ts';
import { concatBytes, equalBytes, sha256, toHex } from './bytes.ts'; import { concatBytes, decodeUtf8, equalBytes, sha256, toHex } from './bytes.ts';
import { type Item, MAX_SAFE_UINT, walk } from './cbor.ts'; import { type Item, MAX_SAFE_UINT, walk } from './cbor.ts';
import { decodeControl, encodeControl } from './control.ts'; import { decodeControl, encodeControl } from './control.ts';
import { type Format, FORMAT_1, FORMAT_2, FORMAT_3, isFormat } from './framing.ts'; import { type Format, FORMAT_1, FORMAT_2, FORMAT_3, isFormat } from './framing.ts';
@ -45,6 +45,7 @@ import type { Extension, ExtensionRegistry } from './extension.ts';
import { decodeHead, encodeHead, type HeadFile } from './head.ts'; import { decodeHead, encodeHead, type HeadFile } from './head.ts';
import { decodeHeader, encodeHeader } from './header.ts'; import { decodeHeader, encodeHeader } from './header.ts';
import { inspect, inspectJSON, inspectView } from './inspect.ts'; import { inspect, inspectJSON, inspectView } from './inspect.ts';
import { checkNoteData, newNote, NOTE_ID, publicNote, unusableNote } from './note.ts';
import { open } from './open.ts'; import { open } from './open.ts';
import { checkPath, nfd, pathKey } from './pathrule.ts'; import { checkPath, nfd, pathKey } from './pathrule.ts';
import { TABLES_DIGEST, UNICODE_VERSION } from './pathrule-tables.ts'; import { TABLES_DIGEST, UNICODE_VERSION } from './pathrule-tables.ts';
@ -585,26 +586,26 @@ describe('vectors/mutations.json', () => {
const inspected = f.filter((c) => c.error !== 'ok' && c.step <= LAST_INSPECT_STEP); const inspected = f.filter((c) => c.error !== 'ok' && c.step <= LAST_INSPECT_STEP);
const opened = f.filter((c) => c.step > LAST_INSPECT_STEP); const opened = f.filter((c) => c.step > LAST_INSPECT_STEP);
it('has the cases README counts: 210, 169 of §64 in their runs, 57 in steps 1 to 8 (39 of them from §64), 4 that open', () => { it('has the cases README counts: 218, 178 of §64 in their runs, 57 in steps 1 to 8 (39 of them from §64), 11 that open', () => {
// v0.8.2 had 65 cases, which stay first; v0.9 ran the 33 mutations of the // v0.8.2 had 65 cases, which stay first; v0.9 ran the 33 mutations of the
// first two lists of §64 on the format 2 fixtures, the 22 of its third // first two lists of §64 on the format 2 fixtures, the 22 of its third
// list and 5 companions with a .dkk; v0.10 adds a case to that list, the // list and 5 companions with a .dkk; v0.10 adds a case to that list, the
// same 33 on the format 3 fixtures, the 47 of the list of format 3 and 3 // same 33 on the format 3 fixtures, the 47 of the list of format 3 and 3
// further cases. v0.11 adds one outside §64, a signature of alg 1 that // further cases. v0.11 and the draft v0.12 add to the list of format 3 a
// does not verify, before the seal of seal_type 1: 205 of the 210 cases. // signature of alg 1 that does not verify, and then the 8 cases of the
expect(f).toHaveLength(210); // list of v0.11 that a capsule holds, before those 3 further cases: 215
// of the 218 cases.
expect(f).toHaveLength(218);
const run = (from: number, to: number, spec: boolean): boolean => f.slice(from, to).every((c) => c.spec === spec); const run = (from: number, to: number, spec: boolean): boolean => f.slice(from, to).every((c) => c.spec === spec);
expect([ expect([
run(0, 33, true), run(0, 33, true),
run(33, 65, false), run(33, 65, false),
run(65, 121, true), run(65, 121, true),
run(121, 126, false), run(121, 126, false),
run(126, 205, true), run(126, 215, true),
run(205, 206, false), run(215, 218, false),
run(206, 207, true), ]).toEqual([true, true, true, true, true, true]);
run(207, 210, false), expect(f.filter((c) => c.spec).length).toBe(178);
]).toEqual([true, true, true, true, true, true, true, true]);
expect(f.filter((c) => c.spec).length).toBe(169);
// README: the same 33 on the fixtures of formats 2 and 3 are named // README: the same 33 on the fixtures of formats 2 and 3 are named
// "format 2: …" and "format 3: …". // "format 2: …" and "format 3: …".
expect(f.slice(65, 98).map((c) => c.name)).toEqual(f.slice(0, 33).map((c) => `format 2: ${c.name}`)); expect(f.slice(65, 98).map((c) => c.name)).toEqual(f.slice(0, 33).map((c) => `format 2: ${c.name}`));
@ -614,8 +615,15 @@ describe('vectors/mutations.json', () => {
expect(opens.map((c) => [c.spec, c.verdicts!.signature, c.verdicts!.seal])).toEqual([ expect(opens.map((c) => [c.spec, c.verdicts!.signature, c.verdicts!.seal])).toEqual([
[true, 'X', 'X'], [true, 'X', 'X'],
[true, 'F1', 'S0'], [true, 'F1', 'S0'],
[false, 'F2', 'S0'], [true, 'F2', 'S0'],
[true, 'F0', 'S1'], [true, 'F0', 'S1'],
[true, 'F2', 'S0'],
[true, 'F0', 'S0'],
[true, 'F4', 'S0'],
[true, 'F2', 'S0'],
[true, 'F1', 'S0'],
[true, 'F1', 'S0'],
[true, 'F4', 'S0'],
]); ]);
expect(inspected.length + opened.length + opens.length).toBe(f.length); expect(inspected.length + opened.length + opens.length).toBe(f.length);
expect(new Set(f.map((c) => c.name)).size, 'unique names').toBe(f.length); expect(new Set(f.map((c) => c.name)).size, 'unique names').toBe(f.length);
@ -895,6 +903,65 @@ describe('vectors/padding.json', () => {
}); });
}); });
// ---------------------------------------------------------------------------
// vectors/note.json
//
// The data of the public note, datekeys.note version 1 in the noncritical
// array of PUBLIC_HEADER (spec §24.1): ok, or ERR_EXTENSION_DATA_INVALID with
// the text of the rule that the note breaks, without the code, as Go words it.
interface NoteVector {
name: string;
data: Uint8Array;
result: string;
detail?: string;
}
describe('vectors/note.json', () => {
const f = load('vectors/note.json', (json) => {
const o = object(json, 'note.json');
keys(o, 'note.json', ['spec', 'description', 'notes']);
checkSpec(o, 'note.json');
return array(o.notes, 'notes').map((v, i): NoteVector => {
const at = `notes[${i}]`;
const c = object(v, at);
const res = result(c.result, `${at}.result`);
// README: detail only for a note that fails.
keys(c, at, ['name', 'data', 'result', ...(res === 'ok' ? [] : ['detail'])]);
const n: NoteVector = { name: str(c.name, `${at}.name`), data: hexBytes(c.data, `${at}.data`), result: res };
if (res !== 'ok') n.detail = str(c.detail, `${at}.detail`);
return n;
});
});
if (f === undefined) return;
it('has notes that pass and notes that fail, each failure with ERR_EXTENSION_DATA_INVALID', () => {
expect(f.some((n) => n.result === 'ok')).toBe(true);
expect(new Set(f.filter((n) => n.result !== 'ok').map((n) => n.result))).toEqual(new Set(['ERR_EXTENSION_DATA_INVALID']));
});
it.each(f.map((n) => [n.name, n] as const))('%s', (_name, n) => {
let text: string | undefined;
let got: { result: string; detail?: string };
try {
text = checkNoteData(n.data);
got = { result: 'ok' };
} catch (err) {
if (!(err instanceof DateKeysError)) throw err;
expect(err.message.endsWith(`: ${err.code}`), err.message).toBe(true);
got = { result: err.code, detail: err.message.slice(0, -`: ${err.code}`.length) };
}
expect(got).toEqual({ result: n.result, detail: n.detail });
// A reader shows the note only when it passes, and says when it does
// not; a writer writes it only then.
const noncritical: Extension[] = [{ id: NOTE_ID, version: 1, data: n.data }];
expect([publicNote(noncritical), unusableNote(noncritical)]).toEqual([text, n.result !== 'ok']);
const decoded = decodeUtf8(n.data);
if (decoded !== undefined && n.result === 'ok') expect(newNote(decoded).data).toEqual(n.data);
else if (decoded !== undefined) expect(() => newNote(decoded)).toThrow(`${n.detail}: ${n.result}`);
});
});
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// vectors/tlock_ibe.json // vectors/tlock_ibe.json
// //
@ -1120,27 +1187,51 @@ describe('vectors/head_schema.json', () => {
}); });
}); });
/**
* The context of security.json and of a case of security_cms.json, what a
* verdict needs besides the area: control_commit and head_digest in
* hexadecimal, and round_time in RFC 3339, as Go's time.RFC3339 writes it.
*/
function securityContext(v: unknown, at: string): { controlCommit: Uint8Array; headDigest: Uint8Array; roundTime: Instant } {
const o = object(v, at);
keys(o, at, ['control_commit', 'head_digest', 'round_time']);
const controlCommit = hexBytes(o.control_commit, `${at}.control_commit`);
const headDigest = hexBytes(o.head_digest, `${at}.head_digest`);
const roundTime = parseRFC3339(str(o.round_time, `${at}.round_time`));
if (controlCommit.length !== 32 || headDigest.length !== 32 || formatRFC3339(roundTime) !== o.round_time) throw new FormatError(at, 'not two hashes of 32 bytes and a time in RFC 3339');
return { controlCommit, headDigest, roundTime };
}
/** The lines of a vector: the verdicts as the official SDK shows them (spec §29.7). */
function linesOf(v: unknown, at: string): string[] {
return array(v, at).map((l, i) => str(l, `${at}[${i}]`));
}
describe('vectors/security.json', () => { describe('vectors/security.json', () => {
const f = load('vectors/security.json', (json) => { const f = load('vectors/security.json', (json) => {
const o = object(json, 'security.json'); const o = object(json, 'security.json');
keys(o, 'security.json', ['spec', 'description', 'vectors']); keys(o, 'security.json', ['spec', 'description', 'context', 'vectors']);
checkSpec(o, 'security.json'); checkSpec(o, 'security.json');
return array(o.vectors, 'vectors').map((v, i) => { const context = securityContext(o.context, 'context');
const vectors = array(o.vectors, 'vectors').map((v, i) => {
const at = `vectors[${i}]`; const at = `vectors[${i}]`;
const c = object(v, at); const c = object(v, at);
keys(c, at, ['name', 'hex', 'signature', 'seal']); keys(c, at, ['name', 'hex', 'signature', 'seal', 'lines']);
return { name: str(c.name, at), input: hexBytes(c.hex, at), signature: verdict(c.signature, at), seal: verdict(c.seal, at) }; return { name: str(c.name, at), input: hexBytes(c.hex, at), signature: verdict(c.signature, at), seal: verdict(c.seal, at), lines: linesOf(c.lines, `${at}.lines`) };
}); });
return { context, vectors };
}); });
if (f === undefined) return; if (f === undefined) return;
it('reaches every verdict of this version: X, F0, F1, S0, S1 and S2', () => { // The areas of security.json have no valid signature of alg 2 or seal of seal_type 2: security_cms.json has them.
const reached = new Set(f.flatMap((c) => [c.signature, c.seal])); it('reaches X, F0, F1, F2, F4, S0, S1 and S2, in the context of the file', () => {
expect([...reached].sort()).toEqual(['F0', 'F1', 'S0', 'S1', 'S2', 'X']); const reached = new Set(f.vectors.flatMap((c) => [c.signature, c.seal]));
expect([...reached].sort()).toEqual(['F0', 'F1', 'F2', 'F4', 'S0', 'S1', 'S2', 'X']);
}); });
it.each(f.map((c) => [c.name, c] as const))('%s', (_n, c) => { it.each(f.vectors.map((c) => [c.name, c] as const))('%s', (_n, c) => {
expect(evaluateSecurity(c.input)).toEqual({ signature: c.signature, seal: c.seal }); const v = evaluateSecurity(c.input, f.context);
expect({ ...kinds(v), lines: verdictLines(v) }).toEqual({ signature: c.signature, seal: c.seal, lines: c.lines });
}); });
}); });
@ -1156,6 +1247,7 @@ const VECTOR_FILES = [
'vectors/inspect_differential.json', 'vectors/inspect_differential.json',
'vectors/locator.json', 'vectors/locator.json',
'vectors/mutations.json', 'vectors/mutations.json',
'vectors/note.json',
'vectors/padding.json', 'vectors/padding.json',
'vectors/path_fold.json', 'vectors/path_fold.json',
'vectors/paths.json', 'vectors/paths.json',
@ -1195,36 +1287,73 @@ describe('testdata/', () => {
for (const f of VECTOR_FILES) expect(readme, f).toContain(`\`${f}\``); for (const f of VECTOR_FILES) expect(readme, f).toContain(`\`${f}\``);
}); });
// The vectors of spec v0.11 for the verification of the signature with
// certificates and of the seal (§29.7, §29.10, §29.11): every area is read in
// the context of its capsule and must give the verdicts of the reference, the
// result of each signer and the authority of a valid seal.
describe('vectors of v0.11', () => { describe('vectors of v0.11', () => {
it('security_cms.json: every case gives the verdicts, the signers and the seal of the reference', () => { // The vectors of the draft v0.12 for the verification of the signature
const f = object(readJSON('vectors/security_cms.json'), 'security_cms.json'); // with certificates and of the seal (§29.7, §29.10, §29.11): every area is
expect(f.spec).toBe(SPEC_VERSION); // read in the context of its capsule and must give the verdicts of the
const cases = array(f.cases, 'cases').map((c, i) => object(c, `cases[${i}]`)); // reference, the result of each signer, of those who do not count and the
expect(cases.length).toBeGreaterThanOrEqual(20); // authority of a valid seal, each time in RFC 3339 with the fraction of
for (const [i, c] of cases.entries()) { // its token, and the lines of §29.7, byte for byte.
const at = `cases[${i}] ${String(c.name)}`; describe('security_cms.json', () => {
const area = hexOf(c.security_cbor); const RESULTS = ['valid', 'invalid', 'absent', 'without seal', 'invalid seal', 'out of validity', 'not verifiable'];
const ctx = object(c.context, `${at}.context`); // The results of the signers of a case, [] when the key is absent, as Go omits an empty list.
const v = const results = (v: unknown, at: string): unknown[] =>
c.no_context === true v === undefined
? evaluateSecurity(area) ? []
: evaluateSecurity(area, { controlCommit: hexOf(ctx.control_commit), headDigest: hexOf(ctx.head_digest), roundTime: parseRFC3339(str(ctx.round_time, `${at}.round_time`)) }); : array(v, at).map((s, i) => {
expect({ signature: v.signature, seal: v.seal }, at).toEqual({ signature: verdict(c.signature, `${at}.signature`), seal: verdict(c.seal, `${at}.seal`) }); const w = `${at}[${i}]`;
const o = object(s, w);
keys(o, w, ['holder', 'issuer', 'result', 'before_round_time'], ['seal_time']);
if (!RESULTS.includes(str(o.result, `${w}.result`))) throw new FormatError(w, `"${String(o.result)}" is not a result of README`);
const t = o.seal_time === undefined ? {} : { seal_time: str(o.seal_time, `${w}.seal_time`) };
return { holder: str(o.holder, `${w}.holder`), issuer: str(o.issuer, `${w}.issuer`), result: o.result, ...t, before_round_time: bool(o.before_round_time, w) };
});
const f = load('vectors/security_cms.json', (json) => {
const o = object(json, 'security_cms.json');
keys(o, 'security_cms.json', ['description', 'spec', 'cases']);
checkSpec(o, 'security_cms.json');
return array(o.cases, 'cases').map((v, i) => {
const at = `cases[${i}]`;
const c = object(v, at);
keys(c, at, ['name', 'security_cbor', 'context', 'signature', 'seal', 'lines'], ['signers', 'foreign_signers', 'seal_holder', 'seal_time']);
const want = {
signature: verdict(c.signature, `${at}.signature`),
seal: verdict(c.seal, `${at}.seal`),
signers: results(c.signers, `${at}.signers`),
foreign_signers: results(c.foreign_signers, `${at}.foreign_signers`),
seal_holder: c.seal_holder === undefined ? '' : str(c.seal_holder, `${at}.seal_holder`),
seal_time: c.seal_time === undefined ? '' : str(c.seal_time, `${at}.seal_time`),
lines: linesOf(c.lines, `${at}.lines`),
};
return { name: str(c.name, at), area: hexOf(c.security_cbor), context: securityContext(c.context, `${at}.context`), want };
});
});
if (f === undefined) return;
it('has the 135 cases of README, which reach every verdict but X and F3', () => {
expect(f).toHaveLength(135);
const reached = new Set(f.flatMap((c) => [c.want.signature, c.want.seal]));
expect([...reached].sort()).toEqual(['F0', 'F1', 'F2', 'F4', 'F5', 'F6', 'S0', 'S1', 'S2', 'S3', 'S4', 'S5']);
});
it.each(f.map((c) => [c.name, c] as const))('%s', (_n, c) => {
const v = evaluateSecurity(c.area, c.context);
const result = (s: { holder: string; issuer: string; result: string; sealTime?: Instant; before: boolean }): unknown => ({ const result = (s: { holder: string; issuer: string; result: string; sealTime?: Instant; before: boolean }): unknown => ({
holder: s.holder, holder: s.holder,
issuer: s.issuer, issuer: s.issuer,
result: s.result, result: s.result,
...(s.sealTime === undefined ? {} : { seal_time: formatRFC3339(s.sealTime) }), ...(s.sealTime === undefined ? {} : { seal_time: formatRFC3339Nano(s.sealTime) }),
before_round_time: s.before, before_round_time: s.before,
}); });
expect(v.detail?.signers.map(result) ?? [], at).toEqual(c.signers ?? []); expect({
expect(v.detail?.foreign.map(result) ?? [], at).toEqual(c.foreign_signers ?? []); ...kinds(v),
expect([v.detail?.sealHolder ?? '', v.detail?.sealTime === undefined ? '' : formatRFC3339(v.detail.sealTime)], at).toEqual([c.seal_holder ?? '', c.seal_time ?? '']); signers: v.detail?.signers.map(result) ?? [],
} foreign_signers: v.detail?.foreign.map(result) ?? [],
seal_holder: v.detail?.sealHolder ?? '',
seal_time: v.detail?.sealTime === undefined ? '' : formatRFC3339Nano(v.detail.sealTime),
lines: verdictLines(v),
}).toEqual(c.want);
});
}); });
it('locator.json and ed25519_strict.json name this specification (the locator is not ported yet; ed25519strict.test.ts runs the other)', () => { it('locator.json and ed25519_strict.json name this specification (the locator is not ported yet; ed25519strict.test.ts runs the other)', () => {

308
testdata/README.md vendored

@ -1,23 +1,32 @@
# DateKeys test data # DateKeys test data
Official vectors, fixtures and corpora of the DateKeys Protocol Specification Official vectors, fixtures and corpora of the DateKeys Protocol Specification
v0.10, generated by the reference implementation. Another implementation v0.11 and of the draft v0.12, generated by the reference implementation.
consumes them as they are: this file documents every format, so that no Go code Another implementation consumes them as they are: this file documents every
has to be read. The rules that decide each verdict are in the specification; format, so that no Go code has to be read. The rules that decide each verdict
this file points to them, and states only what belongs to the files are in the specification; this file points to them, and states only what
themselves. belongs to the files themselves.
``` ```
go run ./internal/testkit/genfixtures -out testdata go run ./internal/testkit/genfixtures -out testdata
``` ```
regenerates everything except the `.dkc` and `.dkk` fixtures, which are regenerates everything except the `.dkc` and `.dkk` fixtures, which are
generated once and frozen (spec §67). The records of each fixture generated once and frozen (spec §67), and the vectors `security_cms.json` and
(`<name>.json`, `<name>.dkk.json`, `<name>.inspect.json`) are recomputed from `locator.json`, frozen too because they hold randomness: delete one of them
its frozen bytes, so the fixtures of v0.8.2 and v0.9 carry `"spec": "0.10"` and to make it again. The records of each fixture (`<name>.json`,
the fields added since. The local gate (`scripts/check.sh`) and CI run it and `<name>.dkk.json`, `<name>.inspect.json`) are recomputed from its frozen bytes,
fail if any committed file changes: every file below is exactly what the so the fixtures of v0.8.2, v0.9 and v0.10 carry `"spec": "0.11"` and the fields
implementation computes today. added since. The local gate (`scripts/check.sh`) and CI run it and fail if any
committed file changes: every file below is exactly what the implementation
computes today.
The `spec` field of every file is `"0.11"`, the version this module declares,
until the author approves the draft v0.12. What the draft changes, the texts
of the verdicts of a certificate and of a seal, the profile of a certificate
and the rules of the addresses and of the padding of a locator, is already in
the files: the verdicts and the lines of `security.json`, `security_cms.json`
and `mutations.json`, and the cases of `locator.json`, are those of the draft.
Conventions for every file: Conventions for every file:
@ -43,16 +52,19 @@ Conventions for every file:
| `vectors/paths.json` | the paths of a format 3 head: the rules of one entry, and those of the paths of a head | §29.5 | | `vectors/paths.json` | the paths of a format 3 head: the rules of one entry, and those of the paths of a head | §29.5 |
| `vectors/path_fold.json` | the key of R7 of segments, and their NFD | §29.5, §29.5.1 | | `vectors/path_fold.json` | the key of R7 of segments, and their NFD | §29.5, §29.5.1 |
| `vectors/head_schema.json` | heads of format 3 and the result of decoding them | §29.4 to §29.6, §69.1 | | `vectors/head_schema.json` | heads of format 3 and the result of decoding them | §29.4 to §29.6, §69.1 |
| `vectors/security.json` | security areas of format 3 and their verdicts | §29.3, §29.7 | | `vectors/security.json` | security areas of format 3 in the context of a capsule, their verdicts and the lines that show them | §29.3, §29.7, §29.9 |
| `vectors/ed25519_strict.json` | Ed25519 signatures and the result of the strict profile of the author signature | v0.11 §29.9 | | `vectors/security_cms.json` | security areas with a signature of `alg` 2 or a seal of `seal_type` 2, each with its context, verdicts, results and lines | §29.7, §29.10, §29.11 |
| `vectors/mutations.json` | the mutation corpus: the 169 mutations of §64 and further cases | §63, §64 | | `vectors/ed25519_strict.json` | Ed25519 signatures and the result of the strict profile of the author signature | §29.9 |
| `vectors/note.json` | the data of the public note and the result of its rules | §24.1, §29.6 |
| `vectors/locator.json` | the extension `datekeys.capsule` of a `.dkk`, its envelope and its locator, and what a reader rejects and uses of them | §44.1, §64 |
| `vectors/mutations.json` | the mutation corpus: the 178 mutations of §64 and further cases | §63, §64 |
| `vectors/inspect_differential.json` | 5110 mutations of fourteen fixtures with the verdict of steps 1 to 8 | §63 | | `vectors/inspect_differential.json` | 5110 mutations of fourteen fixtures with the verdict of steps 1 to 8 | §63 |
| `fixtures/<name>.dkc`, `<name>.json` | official capsules and every intermediate value | §67 | | `fixtures/<name>.dkc`, `<name>.json` | official capsules and every intermediate value | §67 |
| `fixtures/<name>.dkk`, `<name>.dkk.json` | official access keys | §68 | | `fixtures/<name>.dkk`, `<name>.dkk.json` | official access keys | §68 |
| `fixtures/<name>.plaintext` | the content of each capsule: what the reader delivers in formats 1 and 2, without the padding of format 2, and in format 3 BODY, whose files its record lays out | §67 | | `fixtures/<name>.plaintext` | the content of each capsule: what the reader delivers in formats 1 and 2, without the padding of format 2, and in format 3 BODY, whose files its record lays out | §67 |
| `fixtures/<name>.inspect.json` | the exact output of `datekeys inspect -json` for each `.dkc` | §63 | | `fixtures/<name>.inspect.json` | the exact output of `datekeys inspect -json` for each `.dkc` | §63 |
There are twenty-one official capsules. Five are in format 1, the fixtures of There are twenty-six official capsules. Five are in format 1, the fixtures of
v0.8.2, kept for compatibility: `time_only`, `time_only_extensions`, v0.8.2, kept for compatibility: `time_only`, `time_only_extensions`,
`time_and_key_portable`, `time_and_key_recipients` and `empty_payload`. Seven `time_and_key_portable`, `time_and_key_recipients` and `empty_payload`. Seven
are in format 2, the fixtures of v0.9, kept for compatibility too: are in format 2, the fixtures of v0.9, kept for compatibility too:
@ -73,7 +85,7 @@ extension and a noncritical CONTROL_CBOR extension. The release that opens each
capsule, a published Quicknet signature, is in its `<name>.json`, so they all capsule, a published Quicknet signature, is in its `<name>.json`, so they all
decrypt offline. decrypt offline.
Twelve are in format 3. Their plaintext file is BODY, L bytes: the frame, the Fourteen are in format 3. Their plaintext file is BODY, L bytes: the frame, the
security area, the head and the files (spec §29.2). security area, the head and the files (spec §29.2).
| Fixture | Policy | Files | Comment | L | Padding code | P | Area | Verdicts | | Fixture | Policy | Files | Comment | L | Padding code | P | Area | Verdicts |
@ -87,27 +99,39 @@ security area, the head and the files (spec §29.2).
| `format3_security_v2` | `time_only` | 1 | — | 659 | 2 | 768 | 512 | X | | `format3_security_v2` | `time_only` | 1 | — | 659 | 2 | 768 | 512 | X |
| `format3_signature_unsupported` | `time_only` | 1 | — | 659 | 2 | 768 | 512 | F1, S0 | | `format3_signature_unsupported` | `time_only` | 1 | — | 659 | 2 | 768 | 512 | F1, S0 |
| `format3_seal_unsupported` | `time_only` | 1 | — | 659 | 2 | 768 | 512 | F1, S1 | | `format3_seal_unsupported` | `time_only` | 1 | — | 659 | 2 | 768 | 512 | F1, S1 |
| `format3_unsigned` | `time_only` | 1, `nota.txt`, with mtime | — | 32915 | 2 | 34816 | 32768 | F0, S0 |
| `format3_note` | `time_only`, with a public note | 1, `nota.txt`, with mtime | — | 32915 | 2 | 34816 | 32768 | F0, S0 |
| `format3_signed` | `time_only` | 1, `nota.txt`, with mtime | — | 32915 | 2 | 34816 | 32768 | F4, S0 | | `format3_signed` | `time_only` | 1, `nota.txt`, with mtime | — | 32915 | 2 | 34816 | 32768 | F4, S0 |
| `format3_signed_cms` | `time_only` | 1, `nota.txt`, with mtime | — | 32915 | 2 | 34816 | 32768 | F6, S0 | | `format3_signed_cms` | `time_only` | 1, `nota.txt`, with mtime | — | 32915 | 2 | 34816 | 32768 | F6, S0 |
| `format3_sealed` | `time_only` | 1, `nota.txt`, with mtime | — | 32915 | 2 | 34816 | 32768 | F4, S4 | | `format3_sealed` | `time_only` | 1, `nota.txt`, with mtime | — | 32915 | 2 | 34816 | 32768 | F4, S4 |
The first five were written by a writer of v0.10, with the area of 512 bytes; The first five were written by a writer of v0.10, with the area of 512 bytes.
a writer of v0.11 writes the area of 32768 bytes, as in the last three, which The next four only a generator of test vectors may write (spec §62.1 rule 13):
`capsule.EncryptFiles` writes with a signer and a sealer. The next four only a an area larger than 512 bytes, a security map of version 2, which a reader of
generator of test vectors may write (spec §62.1 rule 13): an area larger than this version cannot read, an author signature of `alg` 4294967295, reserved
512 bytes, a security map of version 2, which a reader of this version cannot for tests, with a random key of 32 bytes and a random signature of 64, and
read, an author signature of `alg` 4294967295, an `alg` that no version that with a seal of `seal_type` 4294967295, reserved for tests too, and a
defines, with a random key of 32 bytes and a random signature of 64, and that random token of 32 bytes. None of them has a verdict that stops the opening.
with a seal of `seal_type` 1 and a random token of 32 bytes. None of them has
a verdict that stops the opening.
The last three are signed and sealed with test keys (spec v0.11, §29.8 to The last five were written by a writer of v0.11, with the area of 32768 bytes:
§29.11), and their record has a `signature` object, and `seal` in the third:
- `format3_unsigned` holds the content of `format3_signed` without a
signature, and its P is the same (spec §64: the same content with a
signature and without one, in the common area).
- `format3_note` carries the public note «Cartas del viaje a Lisboa»,
`datekeys.note` in the noncritical array of PUBLIC_HEADER (spec §24.1),
which its record gives in `header_extensions`.
- The last three are those that `capsule.EncryptFiles` writes with a signer
and a sealer.
Those three are signed and sealed with test keys (spec §29.8 to §29.11), and
their record has a `signature` object, and `seal` in the third:
- `format3_signed`: `alg` 1. The seed of the test key, which is not a secret, - `format3_signed`: `alg` 1. The seed of the test key, which is not a secret,
is in `secret_seed`: Ed25519 is deterministic, so signing `author_message` is in `secret_seed`: Ed25519 is deterministic, so signing `author_message`
with it gives `signature` again. Opening it gives F4 and the key `author_key`, with it gives `signature` again. Opening it gives F4 and the key `author_key`,
`dkauthor1…`; with that key among the saved ones, F3. `dkauthor1…`, which `verdicts` repeats; with that key among the saved ones,
F3.
- `format3_signed_cms`: `alg` 2, two certificates, an ECDSA P-256 one and an - `format3_signed_cms`: `alg` 2, two certificates, an ECDSA P-256 one and an
RSA 2048 one, each with a seal CAdES-T from a test authority, dated before RSA 2048 one, each with a seal CAdES-T from a test authority, dated before
the round time. The record has `signers` (SIGNERS in hexadecimal), the round time. The record has `signers` (SIGNERS in hexadecimal),
@ -152,7 +176,7 @@ edits of a base file, not as its full bytes:
```json ```json
{ {
"spec": "0.10", "spec": "0.11",
"walk": { "max_depth": 3, "max_len": 64 }, "walk": { "max_depth": 3, "max_len": 64 },
"accept": [ { "name": "uint 2^53 eight bytes", "hex": "1b0020000000000000", "value": "9007199254740992" } ], "accept": [ { "name": "uint 2^53 eight bytes", "hex": "1b0020000000000000", "value": "9007199254740992" } ],
"reject": [ { "name": "tag", "hex": "c101", "error": "ERR_NON_CANONICAL_CBOR" } ], "reject": [ { "name": "tag", "hex": "c101", "error": "ERR_NON_CANONICAL_CBOR" } ],
@ -260,7 +284,7 @@ the flow.
```json ```json
{ {
"spec": "0.10", "spec": "0.11",
"description": "…", "description": "…",
"vectors": [ "vectors": [
{ "name": "H2(e(G1, G2)), the generators of G1 and G2", "g1": "97f1…", "g2": "93e0…", "gt": "0f41…", "h2": "cb87319f24560b5231579a09ad79f12e" } { "name": "H2(e(G1, G2)), the generators of G1 and G2", "g1": "97f1…", "g2": "93e0…", "gt": "0f41…", "h2": "cb87319f24560b5231579a09ad79f12e" }
@ -293,7 +317,7 @@ length of PAYLOAD_AGE.
```json ```json
{ {
"spec": "0.10", "spec": "0.11",
"l_max": 8936830510563328, "l_max": 8936830510563328,
"vectors": [ "vectors": [
{ "l": 78000, "bloque256": 78080, "reforzado": 79872, "payload_age_bloque256": 78296, "payload_age_reforzado": 80088, "e": 16, "s": 5, "last_bits": 11 } { "l": 78000, "bloque256": 78080, "reforzado": 79872, "payload_age_bloque256": 78296, "payload_age_reforzado": 80088, "e": 16, "s": 5, "last_bits": 11 }
@ -396,78 +420,174 @@ extension.
## `vectors/security.json` ## `vectors/security.json`
Security areas of format 3, SECURITY_CBOR exactly as its SECURITY_LEN bytes, Security areas of format 3, SECURITY_CBOR exactly as its SECURITY_LEN bytes,
and their verdicts (spec §29.3, §29.7), which never stop the opening. This with their verdicts and the lines that show them (spec §29.3, §29.7, §29.9),
version implements no `alg` and no `seal_type`, so a reader of it reaches X, which never stop the opening. Every area is evaluated in the `context` of the
F0, F1, S0, S1 and S2 only. file, what a verdict needs besides the area: `control_commit` and
`head_digest` in hexadecimal, and `round_time` in RFC 3339.
```json ```json
{ "name": "a signature of alg 0, and the seal intact", "hex": "a4006f…", "signature": "F1", "seal": "S1" } {
"context": { "control_commit": "0101…", "head_digest": "0202…", "round_time": "2030-01-01T00:00:00Z" },
"vectors": [ { "name": "a signature of alg 4294967295, reserved for tests", "hex": "a300…", "signature": "F1", "seal": "S0", "lines": ["No se ha comprobado ninguna firma: trátala como no firmada."] } ]
}
``` ```
X stands for both, when the outer map fails its layer 2 or 3: key 2 that is X stands for both, when the outer map fails its layer 2 or 3: key 2 that is
not a byte string, or an empty one, an unknown key 4, a byte more after the not a byte string, or an empty one, an unknown key 4, a byte more after the
map, version 2, another type tag, keys out of order, an array. Otherwise the map, version 2, another type tag, keys out of order, an array. Otherwise the
signature and the seal are evaluated apart, and the first row of the table of signature and the seal are evaluated apart, and the first row of the table of
§29.7 that holds decides: `alg` 0, an empty key or content that is not CBOR §29.7 that holds decides:
give F1 with the seal intact, and a seal that breaks its schema gives S2 even
with an unknown `seal_type`, which is read only from a seal that meets it. - an empty area, as a writer without a signer writes it: F0 and S0;
- a signature of `alg` 1 that verifies over the context: F4, and one that
does not: F2;
- `alg` 0 or 4294967295, an empty key, or content that is not CBOR or has a
byte more: F1, with the seal intact;
- a seal of `seal_type` 1, 3 or 4294967295: S1; one of `seal_type` 0, one that
breaks its schema, even with an unknown `seal_type`, which is read only from
a seal that meets it, and one of `seal_type` 2 whose token is not DER: S2.
`lines` are the verdicts as §29.7 words them, which an implementation writes
byte for byte. The valid signatures of `alg` 2 and seals of `seal_type` 2 are
in `security_cms.json`.
## `vectors/security_cms.json` ## `vectors/security_cms.json`
Security areas with an author signature of `alg` 2, a CMS signature with Security areas with an author signature of `alg` 2, a CMS signature with
certificates, or a time seal of `seal_type` 2, an RFC 3161 token, each with certificates, or a time seal of `seal_type` 2, an RFC 3161 token: 135 cases,
the context of its capsule and the verdicts of spec v0.11 §29.7, §29.10 and each with the context of its capsule, the verdicts, the result of each signer
§29.11. They complete `security.json`, whose areas have no valid signature or and the lines of the draft v0.12, §29.7, §29.10 and §29.11. They complete
seal. The file is frozen: the certificates and the tokens are made once, with `security.json`, whose areas have no valid signature or seal of these kinds.
test keys, so a second implementation reads them and must reach the same The file is frozen: the certificates and the tokens are made once, with test
verdicts. Delete the file to make it again. keys, so a second implementation reads them and must reach the same verdicts
and write the same lines. Delete the file to make it again.
```json ```json
{ "name": "alg 2: a required signer is absent", "security_cbor": "a4…", { "name": "alg 2: a required signer is absent: F5", "security_cbor": "a3…",
"context": { "control_commit": "…", "head_digest": "…", "round_time": "2030-01-01T00:00:00Z" }, "context": { "control_commit": "…", "head_digest": "…", "round_time": "2030-01-01T00:00:00Z" },
"signature": "F5", "seal": "S0", "signers": [ { "holder": "Ana López", "result": "valid", … }, { "holder": "<sha256>", "result": "absent", … } ] } "signature": "F5", "seal": "S0",
"signers": [ { "holder": "Ana López", "issuer": "Ana López", "result": "valid", "seal_time": "2026-09-30T12:00:00Z", "before_round_time": true }, … ],
"lines": ["…"] }
``` ```
`context` is what a verdict needs besides `SECURITY_CBOR`; with `no_context` - `context`: what a verdict needs besides `SECURITY_CBOR`, as in
the area is read as a reader of v0.10 does, without a capsule, and any `security.json`.
signature is F1 and any seal S1. `signers` are the results of the required - `signers`: the results of the required signers, in the order of SIGNERS;
signers in the order of SIGNERS, and `foreign_signers` those that are not `foreign_signers`: those of the signers that are not required, which never
required and never count. A valid seal gives `seal_holder` and `seal_time`. count. Each has the `holder` and the `issuer` as §29.7 shows them, its
The cases cover F6 with two signers, with a seal after the round time and `result` (`valid`, `invalid`, `absent`, `without seal`, `invalid seal`,
with a signer who is not required; F5 for an absent signer, no seal, a seal `out of validity` or `not verifiable`), the `seal_time` of its CAdES-T when
from before the certificate was valid and a key 3 beside the signature; F2 in it has one, and `before_round_time`, whether that time plus its accuracy
the context of another head; F1 for SIGNERS out of order or empty, a signature precedes the round time.
that is not a CMS, and the lack of a context; and, over an `alg` 1 signature, - `seal_holder` and `seal_time`: the authority and the time of a valid seal
the seals S4, S5 (also when the accuracy reaches the round time), S3 (another of key 3.
subject, an authority expired at its time), S2 (a TSTInfo of version 2, not - `lines`: the verdicts as the official SDK shows them (§29.7), byte for byte:
DER), S1 (a SHA-384 imprint) and a seal over a capsule without a signature. the names between « and », the line of each signer with its authority, the
warning that DateKeys does not check who issued the seals, and the times in
RFC 3339 with the fraction of the token.
A time is in RFC 3339, with the fraction of the token when it has one. The
cases follow each row of §29.7 and each item of the lists of §64 for v0.11
and v0.12: F6 with two signers, after the round time, with a signer who is not
required and with 16 signers; F5 for an absent signer, a withdrawn CAdES-T, no
seal, a certificate out of validity and keys outside the table; F2 in the
context of another head and for a message-digest of another message; F1 for
SIGNERS that break its rule beside a valid CMS, BER, two SignerInfo of one
certificate, the version against the `sid`, two content-type attributes, the
ESSCertIDv2 and the certificate of the signer; every hash and curve of the
table, RSASSA-PSS with and without `trailerField`, an attribute with an arc of
2^31 and a certificate twice; the names of the holder and of the issuer in
each string type and against each rule, `givenName` and `surname` before a
`commonName` with its NIF included; and, over an `alg` 1 signature, the seals
S1 to S5 at the edges of the token: its accuracy, its `genTime`, `ordering`,
a field after the last, the imprint, `crls` and the authority.
## `vectors/locator.json` ## `vectors/locator.json`
The extension `datekeys.capsule` of a `.dkk` and what it points to (spec The extension `datekeys.capsule` of a `.dkk` and what it points to (spec
v0.11, §44.1): a `.dkc` of patterned bytes in an envelope of age whose header §44.1): a `.dkc` of patterned bytes in an envelope of age whose header
(`envelope_header`) goes in the locator and whose `rest`, without a mark, is (`envelope_header`) goes in the locator and whose `rest`, without a mark, is
hidden in a `host` file at `host_offset`; the locator sealed with tlock for hidden in a `host` file at `host_offset`; the locator sealed with tlock for
round 1000 (`locator_sealed`), with its plaintext of 4096 bytes round 1000 (`locator_sealed`), with its plaintext of 4096 bytes
(`locator_plaintext`) and its fields; and the data of the extension (`locator_plaintext`) and its fields; and the data of the extension
(`extension_data`), with the note `note` and the DateKey `datekey`. A reader (`extension_data`), with the note `note` and the DateKey `datekey`. A reader
opens the locator with the release of round 1000 (`quicknet_rounds.json`), opens the locator with the release of round 1000, which `mutations.json` and
finds the rest in the host, checks `rest_size`, `rest_digest` and the records of the fixtures of that round give, finds the rest in the host,
`capsule_digest`, and gets the `.dkc` back. The file is frozen: the envelope checks `rest_size`, `rest_digest` and `capsule_digest`, and gets the `.dkc`
and the locator hold randomness. back. The file is frozen: the envelope and the locators hold randomness.
`padding_cases` give the length of the plaintext of the locator for the length On the same envelope, the cases of §64, each checked against the reference
of its CBOR without the padding of key 6: the least multiple of 4096 that key 6 when the file is made:
can fill exactly, which skips a multiple where the CBOR length of key 6 jumps
(a base of 4070 gives 8192). `uri_cases` give the verdict of the rules of §44.1 - `padding_cases`: the length of the plaintext of a locator for `base`, the
on an address: the scheme `https` or `ipfs`, the raw ASCII authority with no length of its CBOR without key 6: the least multiple of 4096 that holds it,
percent sign or userinfo, a host of letters, digits and hyphens or a public IP or the next one when key 6 cannot complete it, because 1, 2, 26 or 259
literal, and a port from 1 to 65535. bytes are missing (§44.1). Among them the bases 3837, 4070, 4094 and 4095,
which give 8192, those of the next multiple, which give 12288, and their
neighbours.
- `uri_cases`: an address and whether the rules of §44.1 accept it (`ok`):
the scheme, the authority without a percent sign, userinfo or a backslash,
the port, each character outside RFC 3986 and a percent sign without two
hexadecimal digits, the "." and ".." segments, written or with `%2e`, in the
path but not in the query or the fragment, the first and the last address
of each IPv4 block of §44.1 with the public addresses next to them, the
IPv6 blocks and the addresses that hold an IPv4 one, the names that only a
machine or a local network resolves, a last segment that is numeric or
starts with `0x`, and base32 that is not a CID v1. Then what the text of
v0.12 fixes besides: segments of 63 and 64 characters, or with a hyphen at
an end, the scheme, `0X` and the local names in upper case, an IPv4 and a
port with leading zeros, and a CID with the bits left over not zero, with
padding, in upper case, with a varint that is not minimal, with an empty
digest, or of 128 and 136 characters.
- `mixed`: a second locator of the envelope, sealed for round 1000 too. Of its
`addresses`, a reader rejects the first two, an `http` one and one of NAT64
that leads to 127.0.0.1, and uses the third (`usable`), which finds the rest
in `host`: the locator reads all the same. A writer never writes it.
- `rest_cases`: a `resource` as a reader downloads it, the `offset` that an
address gives, and whether the rest read there opens the envelope (`opens`):
the rest alone, the host with bytes after the rest, of which only
`rest_size` bytes from the offset are read, a byte of the rest changed, an
offset that is not its own, and a rest cut short.
- `extension_cases`: data of `datekeys.capsule` and whether a reader can use
it (`ok`): without a locator, and unusable, with only
`ERR_EXTENSION_DATA_INVALID`, for a locator sealed for round 1001 beside a
DateKey of round 1000, a locator that is not an age file or is empty, no
DateKey or one that is not canonical, a note that breaks its rules and a
key 3.
- `plaintext_cases`: plaintexts of a locator of the envelope, each with the
defect that its name says or none, and whether a reader reads them (`ok`).
The bases 4094, 4070 and 3837 completed to 4096 with an empty key 6 or with
its length not in its shortest form, against the 8192 bytes of the base
4094 (§76 of v0.12, change 7); eight addresses and nine, none, an empty one
and one of 1025 bytes, which make the whole locator unreadable, an offset of
0 written, an offset and a `resto_size` of 2^53; two blocks where one
suffices, padding that is not zeros, a byte less and a byte more. An
address that breaks the rules of §44.1 does not make a locator unreadable:
that is `mixed`.
## `vectors/note.json`
The data of the public note, `datekeys.note` version 1 in the noncritical
array of PUBLIC_HEADER (spec §24.1): text in UTF-8, from 1 to 1024 bytes, that
meets the rules of the declared author of §29.6. A writer writes a note only
when its `result` is `ok`; a reader shows it only then, and otherwise treats
the note as unusable, never the capsule (§54), and says so.
```json
{ "name": "a bidi override", "data": "61e280ae62", "result": "ERR_EXTENSION_DATA_INVALID", "detail": "a public note that breaks the rules of text: text: bidirectional control U+202E" }
```
`detail` is the text of the rule that a note breaks, without the code, as the
reference words it. Among the cases: letters that are not ASCII and an emoji,
an emoji with VS16, 1024 bytes, accepted; no byte, 1025 bytes, a tab, a line
feed, a space at either end, U+202E, U+200B, a byte order mark, a
noncharacter, a byte that is not UTF-8 and the UTF-8 of a lone surrogate,
refused.
## `vectors/ed25519_strict.json` ## `vectors/ed25519_strict.json`
Ed25519 signatures, in hexadecimal, and whether the strict profile of the Ed25519 signatures, in hexadecimal, and whether the strict profile of the
author signature accepts them (spec v0.11, §29.9): the equation of RFC 8032 author signature accepts them (spec §29.9): the equation of RFC 8032
without the cofactor, A and R canonical, S below ℓ and A not of small order. without the cofactor, A and R canonical, S below ℓ and A not of small order.
They follow the cases of «Taming the many EdDSAs»: S + ℓ, the top bits of S, a They follow the cases of «Taming the many EdDSAs»: S + ℓ, the top bits of S, a
non-canonical R, the eight points of small order as A, non-canonical non-canonical R, the eight points of small order as A, non-canonical
@ -504,15 +624,16 @@ reading flow (`capsule.Open`, §63) must fail.
``` ```
- `name`: unique, stable. - `name`: unique, stable.
- `spec`: true for the 169 mutations listed in spec §64, false for the further - `spec`: true for the 178 mutations listed in spec §64, false for the further
cases of the reference. The cases come in this order: the 33 mutations of cases of the reference. The cases come in this order: the 33 mutations of
the first two lists of §64 on the format 1 fixtures (cases 1 to 33), 32 the first two lists of §64 on the format 1 fixtures (cases 1 to 33), 32
further cases (34 to 65), the same 33 mutations on the format 2 fixtures, further cases (34 to 65), the same 33 mutations on the format 2 fixtures,
named "format 2: …" (66 to 98), the 23 of the list of format 2 (99 to 121), named "format 2: …" (66 to 98), the 23 of the list of format 2 (99 to 121),
5 further cases (122 to 126), the same 33 on the format 3 fixtures, named 5 further cases (122 to 126), the same 33 on the format 3 fixtures, named
"format 3: …" (127 to 159), the 47 of the list of format 3 (160 to 206), "format 3: …" (127 to 159), the 48 of the list of format 3 (160 to 207), the
and 3 further cases (207 to 209). A line of the lists of §64 with several 8 of the list of v0.11 that a capsule can hold (208 to 215), and 3 further
values, such as "AREA_LEN 0, 511, 513 o 66048", is one case for each. cases (216 to 218). A line of the lists of §64 with several values, such as
"AREA_LEN 0, 511, 513 o 66048", is one case for each.
- `dkc`: the capsule, as edits of a fixture (see above). The reader gets it as a - `dkc`: the capsule, as edits of a fixture (see above). The reader gets it as a
seekable file, so that the `capsule_digest` of an offered `.dkk` is checked seekable file, so that the `capsule_digest` of an offered `.dkk` is checked
before any release request (spec §63 step 9.a). before any release request (spec §63 step 9.a).
@ -551,8 +672,8 @@ reading flow (`capsule.Open`, §63) must fail.
- `error`, `step`: the expected code and the step of §63 that fails. For a - `error`, `step`: the expected code and the step of §63 that fails. For a
capsule that opens, `error` is `ok`, `step` is 0 and `verdicts` holds the capsule that opens, `error` is `ok`, `step` is 0 and `verdicts` holds the
verdicts of its security area, `signature` and `seal`, with the `lines` verdicts of its security area, `signature` and `seal`, with the `lines`
that show them (spec §29.7): the three cases of security of the list of that show them (spec §29.7): the four cases of security of the list of
format 3. format 3, and seven of the list of v0.11.
Every case reproduces offline: the recorded release stands in for the network. Every case reproduces offline: the recorded release stands in for the network.
A reader that implements only steps 1 to 8 can replay every case whose `step` is A reader that implements only steps 1 to 8 can replay every case whose `step` is
@ -631,7 +752,7 @@ that.
### Format 3: its list of §64 ### Format 3: its list of §64
The 47 cases of the list of format 3 of §64 test what format 3 adds, all at The 48 cases of the list of format 3 of §64 test what format 3 adds, all at
step 17 but the first: step 17 but the first:
- `VERSION` 2 on a format 3 capsule, at step 14 (`VERSION` 4 is - `VERSION` 2 on a format 3 capsule, at step 14 (`VERSION` 4 is
@ -649,8 +770,10 @@ step 17 but the first:
is not zero is `ERR_HEAD_INVALID`, but with the next STREAM chunk corrupt, is not zero is `ERR_HEAD_INVALID`, but with the next STREAM chunk corrupt,
or with PAYLOAD_AGE cut right after the chunk that holds the head, it is or with PAYLOAD_AGE cut right after the chunk that holds the head, it is
`ERR_INTEGRITY`; `ERR_INTEGRITY`;
- three cases of security that open, without a code, with the verdicts X, - four cases of security that open, without a code, with the verdicts X,
F1 and S1. F1, F2 and S1: a security map of version 2, a signature of `alg`
4294967295, a signature of `alg` 1 that does not verify and a seal of
`seal_type` 4294967295.
They derive from `format3_single`, and the two that need a head followed by They derive from `format3_single`, and the two that need a head followed by
another chunk from `format3_tree`. What a case changes in BODY is sealed again another chunk from `format3_tree`. What a case changes in BODY is sealed again
@ -663,6 +786,23 @@ file. The three
further cases relabel format 3 as 1, and a `time_and_key` capsule as 2 with further cases relabel format 3 as 1, and a `time_and_key` capsule as 2 with
its identity and with its `.dkk`. its identity and with its `.dkk`.
### Signature, seal and note: the list of v0.11
Eight cases of the list of v0.11 of §64 change a capsule; the rest of that
list is in `ed25519_strict.json`, `security_cms.json`, `note.json` and
`locator.json`, and in the fixtures `format3_unsigned` and `format3_signed`,
which have the same P. Seven open with their verdicts:
- on `format3_signed`, the signature of `alg` 1 altered (F2), removed (F0),
made again with another key (F4, with the key of that signature), with a
key of 31 bytes or a signature of 65 bytes (F1), and the area widened from
32 KiB to 64 KiB after signing, which leaves the signature valid and
`AUTHOR_MESSAGE` unchanged (F4);
- the signature of `format3_signed` transplanted to `format3_unsigned` (F2).
The eighth changes the public note in the PUBLIC_HEADER of `format3_note`:
`ERR_HEADER_BINDING` at step 15.
## The checks of steps 1 to 8 ## The checks of steps 1 to 8
`mutations.json` and `inspect_differential.json` follow the rules of the `mutations.json` and `inspect_differential.json` follow the rules of the
@ -777,8 +917,8 @@ at least `step`, `name`, `ok` and `error`, and every other field.
files in BODY, 12 + `AREA_LEN` + `HEAD_LEN`, `files`, each with its `path`, files in BODY, 12 + `AREA_LEN` + `HEAD_LEN`, `files`, each with its `path`,
`size`, `start`, `end`, `sha256` and `mtime` when it has one, its bytes `size`, `start`, `end`, `sha256` and `mtime` when it has one, its bytes
being those of BODY from `content_offset + start` to `content_offset + being those of BODY from `content_offset + start` to `content_offset +
end`, and `verdicts`, with `signature`, `seal` and the `lines` that show end`, and `verdicts`, with `signature`, `seal`, the `lines` that show
them. them and, with F4, the `author_key`.
- `fixtures/<name>.dkk.json`: for each `.dkk`, its SHA-256, `credential_id`, - `fixtures/<name>.dkk.json`: for each `.dkk`, its SHA-256, `credential_id`,
`capsule_id`, `access_type`, `access_material` (a test secret), `capsule_id`, `access_type`, `access_material` (a test secret),
`capsule_digest`, extensions and the capsule it opens. `capsule_digest`, extensions and the capsule it opens.

@ -1,8 +1,8 @@
{ {
"module": "g.activething.com/go/DateKeys", "module": "g.activething.com/go/DateKeys",
"commit": "ae334343bfa2433bd82b679c8119596dbd4840a4", "commit": "601e6d217804eacadea48df9d1f4d4c9b22dacd2",
"files": { "files": {
"README.md": "bc32fe488a0e4d2298a9048711c235661187ba57dc3f156767f8cb5b2f3e6416", "README.md": "e56b52887b45b36af400a89aae34809f2f6be4366261255c68398761811e0584",
"fixtures/empty_payload.dkc": "871e9bf05b52bbae17f3adfbbf97b46e7f0e53aa8f57bcaa506e43f36f53a9d4", "fixtures/empty_payload.dkc": "871e9bf05b52bbae17f3adfbbf97b46e7f0e53aa8f57bcaa506e43f36f53a9d4",
"fixtures/empty_payload.inspect.json": "373e5d012b023ad58bbb54cbdffe0bed9e50c637438a4083ddb74d5414c59f59", "fixtures/empty_payload.inspect.json": "373e5d012b023ad58bbb54cbdffe0bed9e50c637438a4083ddb74d5414c59f59",
"fixtures/empty_payload.json": "588c2573d99b953d490e3d9caaa392b804398b1f91d4f95492a01dd5e7e1f8ba", "fixtures/empty_payload.json": "588c2573d99b953d490e3d9caaa392b804398b1f91d4f95492a01dd5e7e1f8ba",
@ -51,13 +51,17 @@
"fixtures/format3_comment_only.inspect.json": "fb56eca8bf42c8c47fde4a1d6b2580fcff386f2f58b566820731dce388542196", "fixtures/format3_comment_only.inspect.json": "fb56eca8bf42c8c47fde4a1d6b2580fcff386f2f58b566820731dce388542196",
"fixtures/format3_comment_only.json": "8731bcb3641d7f99f63729e85f1e2960d3f38a6ac64bb3321369bfe3e21fa2b1", "fixtures/format3_comment_only.json": "8731bcb3641d7f99f63729e85f1e2960d3f38a6ac64bb3321369bfe3e21fa2b1",
"fixtures/format3_comment_only.plaintext": "bc5b05885e608f036d8a14fde8738a8c53b395b71c3bcee99c1eab37ea23e80e", "fixtures/format3_comment_only.plaintext": "bc5b05885e608f036d8a14fde8738a8c53b395b71c3bcee99c1eab37ea23e80e",
"fixtures/format3_seal_unsupported.dkc": "cd3f68e430c8d41df92a364d65fe29b4aed8ec50e5129595735ede6a8d7df88b", "fixtures/format3_note.dkc": "da1bee54231252a0fd98439e24588125c5521f6e5a2c6641b499e6b22192c0eb",
"fixtures/format3_seal_unsupported.inspect.json": "3300ec8a6024c4b7d4569e31100f8ee9b7d6ef6483120e79c60c4d7d3fec9d8c", "fixtures/format3_note.inspect.json": "dcebb62407097c757bb62552be5d315155ba8a35dec175e95c3f6fddd6e81869",
"fixtures/format3_seal_unsupported.json": "593310199c21384c393332de177dddf04ade56d8dd9891d4679fc5fd38e139c6", "fixtures/format3_note.json": "b2778f8991c2ad9b464d0f45f495c94aae0da97a646df1ce401cf8847b9ce02c",
"fixtures/format3_seal_unsupported.plaintext": "18e5a8d45af211d036dfe64fc4065c8ada927265200f48a3094aa7ded519b94e", "fixtures/format3_note.plaintext": "0468737c5141be936f59d2823122e87661d4ae155a1df036b4cad1ea6620c17b",
"fixtures/format3_seal_unsupported.dkc": "ae3219fbdbd1de4cef6fade1a3fb3f6e5d5e2e8af54d9516b05f0a48136913ad",
"fixtures/format3_seal_unsupported.inspect.json": "b3a7a1038192394c1f844fed994011646f3e78d1cf311c18cb5c936249b95f14",
"fixtures/format3_seal_unsupported.json": "96e9338350ade00226b56e8461e54fbfdda1266de4e63e4c25eef6f84c96801a",
"fixtures/format3_seal_unsupported.plaintext": "0f865221d26545762712271faf835cb2e9980f8fb15c9d3b94fb6747cf16c1df",
"fixtures/format3_sealed.dkc": "dde5a072d8783227d28279d06d3d226a1fb967c766da626f889d1c6fd76ac9c7", "fixtures/format3_sealed.dkc": "dde5a072d8783227d28279d06d3d226a1fb967c766da626f889d1c6fd76ac9c7",
"fixtures/format3_sealed.inspect.json": "b984a0755332bad838025e47f8e917b9f18b9bb5c068c2d1ef0070db8e42849c", "fixtures/format3_sealed.inspect.json": "b984a0755332bad838025e47f8e917b9f18b9bb5c068c2d1ef0070db8e42849c",
"fixtures/format3_sealed.json": "52cda41e8d665c8c734124f9eeb8d4e81b15c59693dff62b3aba497d4aec910e", "fixtures/format3_sealed.json": "acfec2917e798cd1e3f2b38e1ff4f6cee7474ca6b7684e739c8143b291fe91e2",
"fixtures/format3_sealed.plaintext": "aea0f5feb40acd81ca3b02dd21ea15510234da3ab52b374322f3206e7632d47b", "fixtures/format3_sealed.plaintext": "aea0f5feb40acd81ca3b02dd21ea15510234da3ab52b374322f3206e7632d47b",
"fixtures/format3_security_v2.dkc": "3d02b39ace010d74604554e378d22fe5ce00cecd998c0f797d657b17620b8912", "fixtures/format3_security_v2.dkc": "3d02b39ace010d74604554e378d22fe5ce00cecd998c0f797d657b17620b8912",
"fixtures/format3_security_v2.inspect.json": "dba4d21f1d4e228a17c761bae9a4b8c5a91cd9c0123e3141d3782a43c139321e", "fixtures/format3_security_v2.inspect.json": "dba4d21f1d4e228a17c761bae9a4b8c5a91cd9c0123e3141d3782a43c139321e",
@ -73,7 +77,7 @@
"fixtures/format3_signed.plaintext": "3de3ccab0ac74f95a76aa45c0f85e1749d4b4a051d87e81828eff6bf24372000", "fixtures/format3_signed.plaintext": "3de3ccab0ac74f95a76aa45c0f85e1749d4b4a051d87e81828eff6bf24372000",
"fixtures/format3_signed_cms.dkc": "d658f8d5ac2c5550c07b8f8fd6883b2f6dc02ceafc47d436ea02d8950b2548d2", "fixtures/format3_signed_cms.dkc": "d658f8d5ac2c5550c07b8f8fd6883b2f6dc02ceafc47d436ea02d8950b2548d2",
"fixtures/format3_signed_cms.inspect.json": "afadf530e8146687b25c03f26100ebff18e7f481e0ef09378816bad582270de5", "fixtures/format3_signed_cms.inspect.json": "afadf530e8146687b25c03f26100ebff18e7f481e0ef09378816bad582270de5",
"fixtures/format3_signed_cms.json": "e88e549a0d0351df83c065a11101e3158b48d473aa9f6dfce1b8adbca96b26d7", "fixtures/format3_signed_cms.json": "f05befb5fa9aafa2ee55fc7f3abe4832878b1950ae3b2e20915936ce20f12a9d",
"fixtures/format3_signed_cms.plaintext": "31c35eeeee856277b605fe44203a8f4786bb8f591eda3b6ee58252df5b3cf2f0", "fixtures/format3_signed_cms.plaintext": "31c35eeeee856277b605fe44203a8f4786bb8f591eda3b6ee58252df5b3cf2f0",
"fixtures/format3_single.dkc": "9f68664af8733255084be9036a100b75d27bd16106bf0acff94ce469dd1d1743", "fixtures/format3_single.dkc": "9f68664af8733255084be9036a100b75d27bd16106bf0acff94ce469dd1d1743",
"fixtures/format3_single.inspect.json": "7878da921c17aada50e00d5911ea97e8558633a1684fb96acbd00d6f1b117529", "fixtures/format3_single.inspect.json": "7878da921c17aada50e00d5911ea97e8558633a1684fb96acbd00d6f1b117529",
@ -89,6 +93,10 @@
"fixtures/format3_tree.inspect.json": "643a9dfdc2d0c44b8a1636909c66ed81bfd8c50df2a4cad6566832a8e47ba938", "fixtures/format3_tree.inspect.json": "643a9dfdc2d0c44b8a1636909c66ed81bfd8c50df2a4cad6566832a8e47ba938",
"fixtures/format3_tree.json": "1d11d2f12603542039ac6b396ffec69e92bde2689fd54dfc9ef800b6b9f7746b", "fixtures/format3_tree.json": "1d11d2f12603542039ac6b396ffec69e92bde2689fd54dfc9ef800b6b9f7746b",
"fixtures/format3_tree.plaintext": "f69ac5f450966f7d0e9161aa37451d4260b194a750e3e132c02e8a15ba561cfa", "fixtures/format3_tree.plaintext": "f69ac5f450966f7d0e9161aa37451d4260b194a750e3e132c02e8a15ba561cfa",
"fixtures/format3_unsigned.dkc": "317ab722ae3812a25ddd78b4c98c586363e5587c8d3634c881ce7c421af19168",
"fixtures/format3_unsigned.inspect.json": "2f52f7286d6bd4c846ddd63b10b4989b25d17501a989a2e9deb25e4db0859e1a",
"fixtures/format3_unsigned.json": "8ab91d2505d8c29eb09a5ac6060b16c48f5b5618e82ac8f28ecbe926df4ec009",
"fixtures/format3_unsigned.plaintext": "25527e5e2e1ce02056d4419fb89f7b0ce35e4920f93217f58dcf62a4377f8af5",
"fixtures/time_and_key_portable.dkc": "2e97878078bae6358037a9c264f379a3cbe839f767d69836b0343f35657b2972", "fixtures/time_and_key_portable.dkc": "2e97878078bae6358037a9c264f379a3cbe839f767d69836b0343f35657b2972",
"fixtures/time_and_key_portable.dkk": "e528fa2c832c91119f0684bb9d6fb3c4c2d0d55183482890e7c4fe92f668426a", "fixtures/time_and_key_portable.dkk": "e528fa2c832c91119f0684bb9d6fb3c4c2d0d55183482890e7c4fe92f668426a",
"fixtures/time_and_key_portable.dkk.json": "b057c25c9950533c01122cb907a43242b7538a7d427f8fa3cfa34ab6dfb7e390", "fixtures/time_and_key_portable.dkk.json": "b057c25c9950533c01122cb907a43242b7538a7d427f8fa3cfa34ab6dfb7e390",
@ -116,15 +124,16 @@
"vectors/ed25519_strict.json": "342d866584435b291832d34deb9ce17aef10d99db148b85443e39e9bed321d0a", "vectors/ed25519_strict.json": "342d866584435b291832d34deb9ce17aef10d99db148b85443e39e9bed321d0a",
"vectors/head_schema.json": "3c7bcf57aa22943cd17005ea9ce426c0b3e7c365a0527a790b5e9e1973f9753f", "vectors/head_schema.json": "3c7bcf57aa22943cd17005ea9ce426c0b3e7c365a0527a790b5e9e1973f9753f",
"vectors/inspect_differential.json": "e8c99d025ec761690e71ee0c6adfcfd5b680cdcb89024463d8ebbc24be6b0774", "vectors/inspect_differential.json": "e8c99d025ec761690e71ee0c6adfcfd5b680cdcb89024463d8ebbc24be6b0774",
"vectors/locator.json": "a378453dadbafad4c18b5d9fdf4e49cd69f391e08cf16a04935c446395a9767f", "vectors/locator.json": "01370395ae53b363b5b0d0e2733fcb6fe0e20d7c64a5d20325ed3aa92beec859",
"vectors/mutations.json": "bd2f86fc13d50acd759acf36374df513687566bfdc5b391759c2040a0d796fb3", "vectors/mutations.json": "30719f97cba6e95fba0e077a5418cee50e216efd86080334c7d7e2d8c6dc247a",
"vectors/note.json": "d345861417b5fb8e3b1a56f39fdc58c7539cff85240496de3fd6edf7322a9ce2",
"vectors/padding.json": "53d71fc9679d6eda3ba7b7a15752927a1a5fe026bdbd7f93b0ce3569a6a4b22a", "vectors/padding.json": "53d71fc9679d6eda3ba7b7a15752927a1a5fe026bdbd7f93b0ce3569a6a4b22a",
"vectors/path_fold.json": "94c708bf04379984326f786ba1a954a94dc958fa06013c525e7f7a2c14f856bc", "vectors/path_fold.json": "94c708bf04379984326f786ba1a954a94dc958fa06013c525e7f7a2c14f856bc",
"vectors/paths.json": "3466da7dd82d82c1c43fe956eb91e674065d159c9326e3bc0dcbbe2c6b774251", "vectors/paths.json": "3466da7dd82d82c1c43fe956eb91e674065d159c9326e3bc0dcbbe2c6b774251",
"vectors/profile_quicknet.json": "c15ecb111635ecae2084da8511efbee133e97ff07c1c951658b036ce05b69781", "vectors/profile_quicknet.json": "c15ecb111635ecae2084da8511efbee133e97ff07c1c951658b036ce05b69781",
"vectors/quicknet_rounds.json": "0f11bf5c5da88b1e929bb39439001a3a1f8447a85a4b3a25296d40d71a261e50", "vectors/quicknet_rounds.json": "0f11bf5c5da88b1e929bb39439001a3a1f8447a85a4b3a25296d40d71a261e50",
"vectors/security.json": "c5c7a0a508daf6a56ee0d2a9a3c1982e8b2d5104621fb78e768bb2a69e5c4c85", "vectors/security.json": "158df388fe881b75989e319eaadc88437b5031cd62f6c8cfc9cc421c3eacff89",
"vectors/security_cms.json": "c29458496bf58ad01514d7d0e0c2a3223a2a95a81d8c426ce642827363b43b68", "vectors/security_cms.json": "1ef9c74f1998fc06ab459ab885e3654bf4375f5baa276ea2a1de3f08e64a32b4",
"vectors/tlock_ibe.json": "27e9d9ebac4f07700661d2b4c524b10d065c9698e9acc68baa3d2c01bdbb24f2" "vectors/tlock_ibe.json": "27e9d9ebac4f07700661d2b4c524b10d065c9698e9acc68baa3d2c01bdbb24f2"
} }
} }

Binary file not shown.

@ -0,0 +1,62 @@
{
"file": "format3_note.dkc",
"format": 3,
"capsule_id": "1999a39e1beae60b2fc9b157d11dab5a",
"datekey": "dk1_eyJ2ZXJzaW9uIjoxLCJuZXR3b3JrIjoiZGF0ZWtleXM6cXVpY2tuZXQ6djEiLCJyb3VuZCI6MTAwMH0",
"profile": "datekeys:quicknet:v1",
"round": 1000,
"unlock_at": "2023-08-23T15:59:24Z",
"access_policy": "time_only",
"public_note": "Cartas del viaje a Lisboa",
"valid": true,
"checks": [
{
"step": 1,
"name": "parse DKC1",
"ok": true,
"detail": "magic DKC1"
},
{
"step": 2,
"name": "prelude",
"ok": true,
"detail": "DKC1 v3, PUBLIC_HEADER_LEN=169, SEALED_CONTROL_LEN=458"
},
{
"step": 3,
"name": "public header",
"ok": true,
"detail": "169 bytes"
},
{
"step": 4,
"name": "header validation",
"ok": true,
"detail": "capsule_id=1999a39e1beae60b2fc9b157d11dab5a datekey=dk1_eyJ2ZXJzaW9uIjoxLCJuZXR3b3JrIjoiZGF0ZWtleXM6cXVpY2tuZXQ6djEiLCJyb3VuZCI6MTAwMH0 policy=time_only profile=datekeys:quicknet:v1"
},
{
"step": 5,
"name": "sealed control structure",
"ok": true,
"detail": "one tlock stanza"
},
{
"step": 6,
"name": "payload structure",
"ok": true,
"detail": "one X25519 stanza"
},
{
"step": 7,
"name": "condition",
"ok": true,
"detail": "round 1000, unlock at 2023-08-23T15:59:24Z"
},
{
"step": 8,
"name": "tlock stanza",
"ok": true,
"detail": "round 1000, chain 52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971"
}
]
}

@ -0,0 +1,160 @@
{
"description": "format 3 time_only capsule with a single file, nota.txt, and the public note «Cartas del viaje a Lisboa» in the noncritical array of PUBLIC_HEADER (spec v0.11, §24.1)",
"spec": "0.11",
"format": 3,
"file": "format3_note.dkc",
"sha256": "da1bee54231252a0fd98439e24588125c5521f6e5a2c6641b499e6b22192c0eb",
"release": {
"round": 1000,
"signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39"
},
"prelude": "444b433103000000000000a9000001ca",
"public_header": "a6006a646174656b6579636170010102501999a39e1beae60b2fc9b157d11dab5a037853646b315f65794a325a584a7a61573975496a6f784c434a755a58523362334a72496a6f695a4746305a57746c65584d3663585670593274755a585136646a45694c434a79623356755a4349364d5441774d483004000681a3006d646174656b6579732e6e6f746501010258194361727461732064656c207669616a652061204c6973626f61",
"datekey": "dk1_eyJ2ZXJzaW9uIjoxLCJuZXR3b3JrIjoiZGF0ZWtleXM6cXVpY2tuZXQ6djEiLCJyb3VuZCI6MTAwMH0",
"capsule_id": "1999a39e1beae60b2fc9b157d11dab5a",
"access_policy": "time_only",
"structure": "time_only",
"unlock_at": "2023-08-23T15:59:24Z",
"header_binding": "804780b0f6ccb6fe5b29380750f350d51ce6a28f85602358ac4beae0fd2d57c7",
"outer_stanzas": [
{
"type": "tlock",
"args": [
"1000",
"52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971"
]
}
],
"payload_stanzas": [
{
"type": "X25519",
"args": [
"r26f17/u1yYeroTKE6Q+oAElUwD0C8K1iT6kH+2KNRI"
]
}
],
"control_cbor": "a60070646174656b6579732d636f6e74726f6c0103025820804780b0f6ccb6fe5b29380750f350d51ce6a28f85602358ac4beae0fd2d57c7035820e605364fc937c07d590a7fdaea14a2f0f6be6f8cb327b5a7c4b5f0054798d328064800000000000080930702",
"payload_identity": "e605364fc937c07d590a7fdaea14a2f0f6be6f8cb327b5a7c4b5f0054798d328",
"payload_length": 32915,
"padding": 2,
"padded_length": 34816,
"plaintext_file": "format3_note.plaintext",
"plaintext_sha256": "0468737c5141be936f59d2823122e87661d4ae155a1df036b4cad1ea6620c17b",
"header_extensions": [
{
"critical": false,
"id": "datekeys.note",
"version": 1,
"data": "4361727461732064656c207669616a652061204c6973626f61"
}
],
"area_len": 32768,
"security_cbor": "a20071646174656b6579732d73656375726974790101",
"head_cbor": "a4006d646174656b6579732d68656164010102582053e04e8d5cd8b60376e77e2df88643871c431f6da7fabd1a80b0c45078bf08db0581a600686e6f74612e7478740116020003160458205d596d5f49c179f0ec337f4610155fc237e727f9dee85377a5fce67a3c853510051a6abcf9c0",
"salt": "53e04e8d5cd8b60376e77e2df88643871c431f6da7fabd1a80b0c45078bf08db",
"content_offset": 32893,
"files": [
{
"path": "nota.txt",
"size": 22,
"start": 0,
"end": 22,
"sha256": "5d596d5f49c179f0ec337f4610155fc237e727f9dee85377a5fce67a3c853510",
"mtime": 1790769600
}
],
"verdicts": {
"signature": "F0",
"seal": "S0",
"lines": [
"Sin firma de autor."
]
},
"stages": [
{
"step": 1,
"name": "parse DKC1",
"ok": true
},
{
"step": 2,
"name": "prelude",
"ok": true
},
{
"step": 3,
"name": "public header",
"ok": true
},
{
"step": 4,
"name": "header validation",
"ok": true
},
{
"step": 5,
"name": "sealed control structure",
"ok": true
},
{
"step": 6,
"name": "payload structure",
"ok": true
},
{
"step": 7,
"name": "condition",
"ok": true
},
{
"step": 8,
"name": "tlock stanza",
"ok": true
},
{
"step": 9,
"name": "release",
"ok": true
},
{
"step": 10,
"name": "release verification",
"ok": true
},
{
"step": 11,
"name": "open sealed control",
"ok": true
},
{
"step": 12,
"name": "policy structure",
"ok": true
},
{
"step": 14,
"name": "control",
"ok": true
},
{
"step": 15,
"name": "header binding",
"ok": true
},
{
"step": 16,
"name": "payload identity",
"ok": true
},
{
"step": 17,
"name": "open payload",
"ok": true
},
{
"step": 18,
"name": "commit",
"ok": true
}
]
}

Binary file not shown.

Binary file not shown.

@ -1,7 +1,7 @@
{ {
"file": "format3_seal_unsupported.dkc", "file": "format3_seal_unsupported.dkc",
"format": 3, "format": 3,
"capsule_id": "3517684914fad908ad9e46d7f7b811d5", "capsule_id": "8e2f648c77bd659a89ca95f96f213780",
"datekey": "dk1_eyJ2ZXJzaW9uIjoxLCJuZXR3b3JrIjoiZGF0ZWtleXM6cXVpY2tuZXQ6djEiLCJyb3VuZCI6MjAwMH0", "datekey": "dk1_eyJ2ZXJzaW9uIjoxLCJuZXR3b3JrIjoiZGF0ZWtleXM6cXVpY2tuZXQ6djEiLCJyb3VuZCI6MjAwMH0",
"profile": "datekeys:quicknet:v1", "profile": "datekeys:quicknet:v1",
"round": 2000, "round": 2000,
@ -31,7 +31,7 @@
"step": 4, "step": 4,
"name": "header validation", "name": "header validation",
"ok": true, "ok": true,
"detail": "capsule_id=3517684914fad908ad9e46d7f7b811d5 datekey=dk1_eyJ2ZXJzaW9uIjoxLCJuZXR3b3JrIjoiZGF0ZWtleXM6cXVpY2tuZXQ6djEiLCJyb3VuZCI6MjAwMH0 policy=time_only profile=datekeys:quicknet:v1" "detail": "capsule_id=8e2f648c77bd659a89ca95f96f213780 datekey=dk1_eyJ2ZXJzaW9uIjoxLCJuZXR3b3JrIjoiZGF0ZWtleXM6cXVpY2tuZXQ6djEiLCJyb3VuZCI6MjAwMH0 policy=time_only profile=datekeys:quicknet:v1"
}, },
{ {
"step": 5, "step": 5,

@ -1,21 +1,21 @@
{ {
"description": "format 3 time_only capsule with an author-signature of alg 4294967295, as in format3_signature_unsupported, and a seal of seal_type 1 with a random token of 32 bytes: verdicts F1 and S1", "description": "format 3 time_only capsule with an author-signature of alg 4294967295, as in format3_signature_unsupported, and a seal of seal_type 4294967295, reserved for tests, with a random token of 32 bytes: verdicts F1 and S1",
"spec": "0.11", "spec": "0.11",
"format": 3, "format": 3,
"file": "format3_seal_unsupported.dkc", "file": "format3_seal_unsupported.dkc",
"sha256": "cd3f68e430c8d41df92a364d65fe29b4aed8ec50e5129595735ede6a8d7df88b", "sha256": "ae3219fbdbd1de4cef6fade1a3fb3f6e5d5e2e8af54d9516b05f0a48136913ad",
"release": { "release": {
"round": 2000, "round": 2000,
"signature": "b6cb8f482a0b15d45936a4c4ea08e98a087e71787caee3f4d07a8a9843b1bc5423c6b3c22f446488b3137eaca799c77e" "signature": "b6cb8f482a0b15d45936a4c4ea08e98a087e71787caee3f4d07a8a9843b1bc5423c6b3c22f446488b3137eaca799c77e"
}, },
"prelude": "444b43310300000000000079000001ca", "prelude": "444b43310300000000000079000001ca",
"public_header": "a5006a646174656b6579636170010102503517684914fad908ad9e46d7f7b811d5037853646b315f65794a325a584a7a61573975496a6f784c434a755a58523362334a72496a6f695a4746305a57746c65584d3663585670593274755a585136646a45694c434a79623356755a4349364d6a41774d48300400", "public_header": "a5006a646174656b6579636170010102508e2f648c77bd659a89ca95f96f213780037853646b315f65794a325a584a7a61573975496a6f784c434a755a58523362334a72496a6f695a4746305a57746c65584d3663585670593274755a585136646a45694c434a79623356755a4349364d6a41774d48300400",
"datekey": "dk1_eyJ2ZXJzaW9uIjoxLCJuZXR3b3JrIjoiZGF0ZWtleXM6cXVpY2tuZXQ6djEiLCJyb3VuZCI6MjAwMH0", "datekey": "dk1_eyJ2ZXJzaW9uIjoxLCJuZXR3b3JrIjoiZGF0ZWtleXM6cXVpY2tuZXQ6djEiLCJyb3VuZCI6MjAwMH0",
"capsule_id": "3517684914fad908ad9e46d7f7b811d5", "capsule_id": "8e2f648c77bd659a89ca95f96f213780",
"access_policy": "time_only", "access_policy": "time_only",
"structure": "time_only", "structure": "time_only",
"unlock_at": "2023-08-23T16:49:24Z", "unlock_at": "2023-08-23T16:49:24Z",
"header_binding": "2c34950c31c80b62c31da9aa1ca72cf46d38361a60fdf37519c1f734c6646fb9", "header_binding": "c6518ed7bd02c8f121493170776eb6c43e2c8162e97a9a6225b310a1836da612",
"outer_stanzas": [ "outer_stanzas": [
{ {
"type": "tlock", "type": "tlock",
@ -29,21 +29,21 @@
{ {
"type": "X25519", "type": "X25519",
"args": [ "args": [
"sfu28SAWdZaPq6c3v4bLopEk8YveD80i8b59V4JMfkM" "nhJ/XFoarftmX9LumtcoVsCBMxXMYQgJwuYYEG8lg3M"
] ]
} }
], ],
"control_cbor": "a60070646174656b6579732d636f6e74726f6c01030258202c34950c31c80b62c31da9aa1ca72cf46d38361a60fdf37519c1f734c6646fb9035820a0cae1dd0fb24ae1ea350ad408afa77bb040fe4553ca5a5e6283be84f1775184064800000000000002930702", "control_cbor": "a60070646174656b6579732d636f6e74726f6c0103025820c6518ed7bd02c8f121493170776eb6c43e2c8162e97a9a6225b310a1836da6120358204b0e0832574c11b161d7bab4d37582fde331eabb027f7d26c23cf11113fd5cc8064800000000000002930702",
"payload_identity": "a0cae1dd0fb24ae1ea350ad408afa77bb040fe4553ca5a5e6283be84f1775184", "payload_identity": "4b0e0832574c11b161d7bab4d37582fde331eabb027f7d26c23cf11113fd5cc8",
"payload_length": 659, "payload_length": 659,
"padding": 2, "padding": 2,
"padded_length": 768, "padded_length": 768,
"plaintext_file": "format3_seal_unsupported.plaintext", "plaintext_file": "format3_seal_unsupported.plaintext",
"plaintext_sha256": "18e5a8d45af211d036dfe64fc4065c8ada927265200f48a3094aa7ded519b94e", "plaintext_sha256": "0f865221d26545762712271faf835cb2e9980f8fb15c9d3b94fb6747cf16c1df",
"area_len": 512, "area_len": 512,
"security_cbor": "a40071646174656b6579732d7365637572697479010102586da3001affffffff0158208beec85fe1db5d53dfa1fa5b95afe208c355a1fabe0d3637c1acc09fef0f04c10258400ccc209b32e7826b70b4befbe7bbe504584631422a3b51086e9491885e8aac6d2a0c92c4c85d6c03750ddaa2d873f6d4dce20030b31669f347ee6b9b4f3abd56035826a20001015820c19dc75c9766f13383b991f54a7cfcb16701ad0299fa68d84c5ce2e82a8f18d7", "security_cbor": "a40071646174656b6579732d7365637572697479010102586da3001affffffff015820d956dada75e3501522321d0e57c5a06dc64e07c394e6e5666f5de65b38871732025840b1f86cea4dfa61201710331694cd3fb811eeb32f7d983d9c5679c5adc41ba25fe5b82d48a74ec76d5db3f887745d0681f2a221c71b014324e8b6ad324a4234b303582aa2001affffffff0158206721210782b8e419b97c9e620bc6247ef6775929a04eb075aaac927687283a9c",
"head_cbor": "a4006d646174656b6579732d6865616401010258208eb5e2f0920209323e39c54391a74cc873690cd6d7ce94a45b12772f2a5081c10581a600686e6f74612e7478740116020003160458205d596d5f49c179f0ec337f4610155fc237e727f9dee85377a5fce67a3c853510051a6abcf9c0", "head_cbor": "a4006d646174656b6579732d686561640101025820f9526586bc95f1bc1a375fc7575c71081312626445cce4aae63a095e35afc84a0581a600686e6f74612e7478740116020003160458205d596d5f49c179f0ec337f4610155fc237e727f9dee85377a5fce67a3c853510051a6abcf9c0",
"salt": "8eb5e2f0920209323e39c54391a74cc873690cd6d7ce94a45b12772f2a5081c1", "salt": "f9526586bc95f1bc1a375fc7575c71081312626445cce4aae63a095e35afc84a",
"content_offset": 637, "content_offset": 637,
"files": [ "files": [
{ {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 659 B

After

Width:  |  Height:  |  Size: 659 B

@ -60,7 +60,7 @@
"seal": "S4", "seal": "S4",
"lines": [ "lines": [
"Firmado con la clave dkauthor1jtf6s2c7ywr6scx40jhye32sj82rjp8a6cj7f54jsh36rd96vaqqcg54xg. No prueba quién la tiene.", "Firmado con la clave dkauthor1jtf6s2c7ywr6scx40jhye32sj82rjp8a6cj7f54jsh36rd96vaqqcg54xg. No prueba quién la tiene.",
"Según un sello a nombre de Autoridad de Sellado de prueba, existía el 2023-08-23T15:09:27Z, antes de que la cápsula pudiera abrirse. DateKeys no comprueba quién emitió el sello." "Según un sello a nombre de «Autoridad de Sellado de prueba», existía el 2023-08-23T15:09:27Z, antes de que la cápsula pudiera abrirse. DateKeys no comprueba quién emitió el sello."
], ],
"author_key": "dkauthor1jtf6s2c7ywr6scx40jhye32sj82rjp8a6cj7f54jsh36rd96vaqqcg54xg" "author_key": "dkauthor1jtf6s2c7ywr6scx40jhye32sj82rjp8a6cj7f54jsh36rd96vaqqcg54xg"
}, },

@ -59,9 +59,10 @@
"signature": "F6", "signature": "F6",
"seal": "S0", "seal": "S0",
"lines": [ "lines": [
"Firmado con un certificado a nombre de Luis Gómez, Ana López. DateKeys no comprueba quién lo emitió: para eso, exporta la firma a un validador oficial.", "Firmado con un certificado a nombre de «Luis Gómez», «Ana López». DateKeys no comprueba quién lo emitió: para eso, exporta la firma a un validador oficial.",
" Luis Gómez (emisor según su certificado: Luis Gómez), sellado el 2023-08-23T15:09:27Z, antes de la fecha de apertura.", " «Luis Gómez» (emisor según su certificado: «Luis Gómez»), sellado por «Autoridad de Sellado de prueba» el 2023-08-23T15:09:27Z, antes de la fecha de apertura.",
" Ana López (emisor según su certificado: Ana López), sellado el 2023-08-23T15:09:27Z, antes de la fecha de apertura." " «Ana López» (emisor según su certificado: «Ana López»), sellado por «Autoridad de Sellado de prueba» el 2023-08-23T15:09:27Z, antes de la fecha de apertura.",
" DateKeys no comprueba quién emitió los sellos."
] ]
}, },
"signature": { "signature": {

Binary file not shown.

@ -0,0 +1,61 @@
{
"file": "format3_unsigned.dkc",
"format": 3,
"capsule_id": "0cced8e8b035d6dd70f39923b407a96c",
"datekey": "dk1_eyJ2ZXJzaW9uIjoxLCJuZXR3b3JrIjoiZGF0ZWtleXM6cXVpY2tuZXQ6djEiLCJyb3VuZCI6MTAwMH0",
"profile": "datekeys:quicknet:v1",
"round": 1000,
"unlock_at": "2023-08-23T15:59:24Z",
"access_policy": "time_only",
"valid": true,
"checks": [
{
"step": 1,
"name": "parse DKC1",
"ok": true,
"detail": "magic DKC1"
},
{
"step": 2,
"name": "prelude",
"ok": true,
"detail": "DKC1 v3, PUBLIC_HEADER_LEN=121, SEALED_CONTROL_LEN=458"
},
{
"step": 3,
"name": "public header",
"ok": true,
"detail": "121 bytes"
},
{
"step": 4,
"name": "header validation",
"ok": true,
"detail": "capsule_id=0cced8e8b035d6dd70f39923b407a96c datekey=dk1_eyJ2ZXJzaW9uIjoxLCJuZXR3b3JrIjoiZGF0ZWtleXM6cXVpY2tuZXQ6djEiLCJyb3VuZCI6MTAwMH0 policy=time_only profile=datekeys:quicknet:v1"
},
{
"step": 5,
"name": "sealed control structure",
"ok": true,
"detail": "one tlock stanza"
},
{
"step": 6,
"name": "payload structure",
"ok": true,
"detail": "one X25519 stanza"
},
{
"step": 7,
"name": "condition",
"ok": true,
"detail": "round 1000, unlock at 2023-08-23T15:59:24Z"
},
{
"step": 8,
"name": "tlock stanza",
"ok": true,
"detail": "round 1000, chain 52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971"
}
]
}

@ -0,0 +1,152 @@
{
"description": "format 3 time_only capsule with a single file, nota.txt, as format3_signed, without a signature: the area of 32 KiB of spec v0.11 holds the empty security, and P is the one of format3_signed",
"spec": "0.11",
"format": 3,
"file": "format3_unsigned.dkc",
"sha256": "317ab722ae3812a25ddd78b4c98c586363e5587c8d3634c881ce7c421af19168",
"release": {
"round": 1000,
"signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39"
},
"prelude": "444b43310300000000000079000001ca",
"public_header": "a5006a646174656b6579636170010102500cced8e8b035d6dd70f39923b407a96c037853646b315f65794a325a584a7a61573975496a6f784c434a755a58523362334a72496a6f695a4746305a57746c65584d3663585670593274755a585136646a45694c434a79623356755a4349364d5441774d48300400",
"datekey": "dk1_eyJ2ZXJzaW9uIjoxLCJuZXR3b3JrIjoiZGF0ZWtleXM6cXVpY2tuZXQ6djEiLCJyb3VuZCI6MTAwMH0",
"capsule_id": "0cced8e8b035d6dd70f39923b407a96c",
"access_policy": "time_only",
"structure": "time_only",
"unlock_at": "2023-08-23T15:59:24Z",
"header_binding": "36fac05bdec31225c4249bbdf14381b72c28e9dc8d6eec8af2500ec83c03ea8c",
"outer_stanzas": [
{
"type": "tlock",
"args": [
"1000",
"52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971"
]
}
],
"payload_stanzas": [
{
"type": "X25519",
"args": [
"NcekpxDZyrUYH4JypnvSI6VsJ70XDGb9dNeny2xewlQ"
]
}
],
"control_cbor": "a60070646174656b6579732d636f6e74726f6c010302582036fac05bdec31225c4249bbdf14381b72c28e9dc8d6eec8af2500ec83c03ea8c0358204207790feb87ae42795751d6a28fee2af78359aeef98f1dc9faef32e9c7ddba4064800000000000080930702",
"payload_identity": "4207790feb87ae42795751d6a28fee2af78359aeef98f1dc9faef32e9c7ddba4",
"payload_length": 32915,
"padding": 2,
"padded_length": 34816,
"plaintext_file": "format3_unsigned.plaintext",
"plaintext_sha256": "25527e5e2e1ce02056d4419fb89f7b0ce35e4920f93217f58dcf62a4377f8af5",
"area_len": 32768,
"security_cbor": "a20071646174656b6579732d73656375726974790101",
"head_cbor": "a4006d646174656b6579732d686561640101025820503815e579869d0498c3267596adc6555a6d9db770c834f5ba0e5ff8fc08fec90581a600686e6f74612e7478740116020003160458205d596d5f49c179f0ec337f4610155fc237e727f9dee85377a5fce67a3c853510051a6abcf9c0",
"salt": "503815e579869d0498c3267596adc6555a6d9db770c834f5ba0e5ff8fc08fec9",
"content_offset": 32893,
"files": [
{
"path": "nota.txt",
"size": 22,
"start": 0,
"end": 22,
"sha256": "5d596d5f49c179f0ec337f4610155fc237e727f9dee85377a5fce67a3c853510",
"mtime": 1790769600
}
],
"verdicts": {
"signature": "F0",
"seal": "S0",
"lines": [
"Sin firma de autor."
]
},
"stages": [
{
"step": 1,
"name": "parse DKC1",
"ok": true
},
{
"step": 2,
"name": "prelude",
"ok": true
},
{
"step": 3,
"name": "public header",
"ok": true
},
{
"step": 4,
"name": "header validation",
"ok": true
},
{
"step": 5,
"name": "sealed control structure",
"ok": true
},
{
"step": 6,
"name": "payload structure",
"ok": true
},
{
"step": 7,
"name": "condition",
"ok": true
},
{
"step": 8,
"name": "tlock stanza",
"ok": true
},
{
"step": 9,
"name": "release",
"ok": true
},
{
"step": 10,
"name": "release verification",
"ok": true
},
{
"step": 11,
"name": "open sealed control",
"ok": true
},
{
"step": 12,
"name": "policy structure",
"ok": true
},
{
"step": 14,
"name": "control",
"ok": true
},
{
"step": 15,
"name": "header binding",
"ok": true
},
{
"step": 16,
"name": "payload identity",
"ok": true
},
{
"step": 17,
"name": "open payload",
"ok": true
},
{
"step": 18,
"name": "commit",
"ok": true
}
]
}

Binary file not shown.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -0,0 +1,98 @@
{
"spec": "0.11",
"description": "The data of the public note, datekeys.note version 1 in the noncritical array of PUBLIC_HEADER (spec §24.1): the text in UTF-8, from 1 to 1024 bytes, that meets the rules of the declared author of §29.6. See testdata/README.md.",
"notes": [
{
"name": "a note",
"data": "4361727461732064656c207669616a652061204c6973626f61",
"result": "ok"
},
{
"name": "letters that are not ASCII and an emoji",
"data": "c391616e64c3ba2c2061c3b16f203230323620f09f8c8d",
"result": "ok"
},
{
"name": "1024 bytes",
"data": "61616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161",
"result": "ok"
},
{
"name": "an emoji with VS16, which the whitelist admits",
"data": "e29da4efb88f",
"result": "ok"
},
{
"name": "no byte",
"data": "",
"result": "ERR_EXTENSION_DATA_INVALID",
"detail": "a public note of 0 bytes, not 1 to 1024"
},
{
"name": "1025 bytes",
"data": "6161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161616161",
"result": "ERR_EXTENSION_DATA_INVALID",
"detail": "a public note of 1025 bytes, not 1 to 1024"
},
{
"name": "a tab",
"data": "610962",
"result": "ERR_EXTENSION_DATA_INVALID",
"detail": "a public note that breaks the rules of text: text: control U+0009 in the declared author"
},
{
"name": "a line feed",
"data": "610a62",
"result": "ERR_EXTENSION_DATA_INVALID",
"detail": "a public note that breaks the rules of text: text: control U+000A in the declared author"
},
{
"name": "a space at the start",
"data": "2061",
"result": "ERR_EXTENSION_DATA_INVALID",
"detail": "a public note that breaks the rules of text: text: the declared author starts or ends with U+0020"
},
{
"name": "a space at the end",
"data": "6120",
"result": "ERR_EXTENSION_DATA_INVALID",
"detail": "a public note that breaks the rules of text: text: the declared author starts or ends with U+0020"
},
{
"name": "a bidi override",
"data": "61e280ae62",
"result": "ERR_EXTENSION_DATA_INVALID",
"detail": "a public note that breaks the rules of text: text: bidirectional control U+202E"
},
{
"name": "a zero width space, ignorable",
"data": "61e2808b62",
"result": "ERR_EXTENSION_DATA_INVALID",
"detail": "a public note that breaks the rules of text: text: invisible U+200B"
},
{
"name": "a byte order mark at the start",
"data": "efbbbf486f6c61",
"result": "ERR_EXTENSION_DATA_INVALID",
"detail": "a public note that breaks the rules of text: text: byte order mark U+FEFF"
},
{
"name": "a byte that is not UTF-8",
"data": "61ff",
"result": "ERR_EXTENSION_DATA_INVALID",
"detail": "a public note that is not valid UTF-8"
},
{
"name": "the UTF-8 of a lone surrogate",
"data": "eda080",
"result": "ERR_EXTENSION_DATA_INVALID",
"detail": "a public note that is not valid UTF-8"
},
{
"name": "a noncharacter",
"data": "61efbfbe62",
"result": "ERR_EXTENSION_DATA_INVALID",
"detail": "a public note that breaks the rules of text: text: noncharacter U+FFFE"
}
]
}

@ -1,132 +1,237 @@
{ {
"spec": "0.11", "spec": "0.11",
"description": "SECURITY_CBOR of format 3, exactly its SECURITY_LEN bytes, and the verdicts of the signature and of the seal (spec §29.3, §29.7), generated by the reference implementation, which implements no alg and no seal_type. See testdata/README.md.", "description": "SECURITY_CBOR of format 3, exactly its SECURITY_LEN bytes, the verdicts of the signature and of the seal in the context of the file, and their lines (spec §29.3, §29.7, §29.9). See testdata/README.md.",
"context": {
"control_commit": "0101010101010101010101010101010101010101010101010101010101010101",
"head_digest": "0202020202020202020202020202020202020202020202020202020202020202",
"round_time": "2030-01-01T00:00:00Z"
},
"vectors": [ "vectors": [
{ {
"name": "empty, as writers of this version write it", "name": "empty, as writers of this version write it",
"hex": "a20071646174656b6579732d73656375726974790101", "hex": "a20071646174656b6579732d73656375726974790101",
"signature": "F0", "signature": "F0",
"seal": "S0" "seal": "S0",
"lines": [
"Sin firma de autor."
]
}, },
{ {
"name": "a signature of alg 1", "name": "a signature of alg 1 that does not verify: its S has bits above 252",
"hex": "a30071646174656b6579732d73656375726974790101025869a30001015820111111111111111111111111111111111111111111111111111111111111111102584022222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222", "hex": "a30071646174656b6579732d73656375726974790101025869a30001015820111111111111111111111111111111111111111111111111111111111111111102584022222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222",
"signature": "F2",
"seal": "S0",
"lines": [
"La firma no corresponde a este contenido."
]
},
{
"name": "a signature of alg 1 that verifies",
"hex": "a30071646174656b6579732d73656375726974790101025869a30001015820c1f26f2fe7a8b5046cadaea50cfd6f1c9eba22a77d964d84cdc9843d98057a0902584099e44f44384abbffd3c0853ce1b3841b89f0677152342210b619c1612707f11fe3b1213d135030fecf018f0c06aa7bc4b170028be79038cf5abd2c3d2ff39900",
"signature": "F4",
"seal": "S0",
"lines": [
"Firmado con la clave dkauthor1c8ex7tl84z6sgm9d46jselt0rj0t5g480ktympxdexzrmxq90gysk64cx5. No prueba quién la tiene."
]
},
{
"name": "a signature of alg 4294967295, reserved for tests",
"hex": "a30071646174656b6579732d7365637572697479010102586da3001affffffff015820111111111111111111111111111111111111111111111111111111111111111102584022222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222",
"signature": "F1", "signature": "F1",
"seal": "S0" "seal": "S0",
"lines": [
"No se ha comprobado ninguna firma: trátala como no firmada."
]
}, },
{ {
"name": "a seal of seal_type 1", "name": "a seal of seal_type 1, reserved",
"hex": "a30071646174656b6579732d73656375726974790101035826a200010158203333333333333333333333333333333333333333333333333333333333333333", "hex": "a30071646174656b6579732d73656375726974790101035826a200010158203333333333333333333333333333333333333333333333333333333333333333",
"signature": "F0", "signature": "F0",
"seal": "S1" "seal": "S1",
"lines": [
"Sin firma de autor.",
"Lleva un sello de tiempo que esta versión no sabe comprobar: aquí no prueba nada."
]
}, },
{ {
"name": "a seal of seal_type 2, RFC 3161", "name": "a seal of seal_type 2 whose token is not DER",
"hex": "a30071646174656b6579732d73656375726974790101035826a200020158203333333333333333333333333333333333333333333333333333333333333333", "hex": "a30071646174656b6579732d73656375726974790101035826a200020158203333333333333333333333333333333333333333333333333333333333333333",
"signature": "F0", "signature": "F0",
"seal": "S1" "seal": "S2",
"lines": [
"Sin firma de autor.",
"El sello de tiempo es ilegible: no prueba nada."
]
}, },
{ {
"name": "a seal of seal_type 3, OpenTimestamps", "name": "a seal of seal_type 3, which no version defines",
"hex": "a30071646174656b6579732d73656375726974790101035826a200030158203333333333333333333333333333333333333333333333333333333333333333", "hex": "a30071646174656b6579732d73656375726974790101035826a200030158203333333333333333333333333333333333333333333333333333333333333333",
"signature": "F0", "signature": "F0",
"seal": "S1" "seal": "S1",
"lines": [
"Sin firma de autor.",
"Lleva un sello de tiempo que esta versión no sabe comprobar: aquí no prueba nada."
]
}, },
{ {
"name": "a signature and a seal", "name": "a seal of seal_type 4294967295, reserved for tests",
"hex": "a30071646174656b6579732d7365637572697479010103582aa2001affffffff0158203333333333333333333333333333333333333333333333333333333333333333",
"signature": "F0",
"seal": "S1",
"lines": [
"Sin firma de autor.",
"Lleva un sello de tiempo que esta versión no sabe comprobar: aquí no prueba nada."
]
},
{
"name": "a signature that does not verify and a seal of seal_type 1",
"hex": "a40071646174656b6579732d73656375726974790101025869a30001015820111111111111111111111111111111111111111111111111111111111111111102584022222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222035826a200010158203333333333333333333333333333333333333333333333333333333333333333", "hex": "a40071646174656b6579732d73656375726974790101025869a30001015820111111111111111111111111111111111111111111111111111111111111111102584022222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222035826a200010158203333333333333333333333333333333333333333333333333333333333333333",
"signature": "F1", "signature": "F2",
"seal": "S1" "seal": "S1",
"lines": [
"La firma no corresponde a este contenido.",
"Lleva un sello de tiempo que esta versión no sabe comprobar: aquí no prueba nada."
]
}, },
{ {
"name": "a signature of alg 0, and the seal intact", "name": "a signature of alg 0, and the seal intact",
"hex": "a40071646174656b6579732d73656375726974790101025849a3000001582011111111111111111111111111111111111111111111111111111111111111110258202222222222222222222222222222222222222222222222222222222222222222035826a200010158203333333333333333333333333333333333333333333333333333333333333333", "hex": "a40071646174656b6579732d73656375726974790101025849a3000001582011111111111111111111111111111111111111111111111111111111111111110258202222222222222222222222222222222222222222222222222222222222222222035826a200010158203333333333333333333333333333333333333333333333333333333333333333",
"signature": "F1", "signature": "F1",
"seal": "S1" "seal": "S1",
"lines": [
"No se ha comprobado ninguna firma: trátala como no firmada.",
"Lleva un sello de tiempo que esta versión no sabe comprobar: aquí no prueba nada."
]
}, },
{ {
"name": "a signature with an empty key, and the seal intact", "name": "a signature with an empty key, and the seal intact",
"hex": "a40071646174656b6579732d73656375726974790101025828a3000101400258202222222222222222222222222222222222222222222222222222222222222222035826a200010158203333333333333333333333333333333333333333333333333333333333333333", "hex": "a40071646174656b6579732d73656375726974790101025828a3000101400258202222222222222222222222222222222222222222222222222222222222222222035826a200010158203333333333333333333333333333333333333333333333333333333333333333",
"signature": "F1", "signature": "F1",
"seal": "S1" "seal": "S1",
"lines": [
"No se ha comprobado ninguna firma: trátala como no firmada.",
"Lleva un sello de tiempo que esta versión no sabe comprobar: aquí no prueba nada."
]
}, },
{ {
"name": "a signature that is not CBOR", "name": "a signature that is not CBOR",
"hex": "a30071646174656b6579732d736563757269747901010241ff", "hex": "a30071646174656b6579732d736563757269747901010241ff",
"signature": "F1", "signature": "F1",
"seal": "S0" "seal": "S0",
"lines": [
"No se ha comprobado ninguna firma: trátala como no firmada."
]
}, },
{ {
"name": "a signature with a byte more", "name": "a signature with a byte more",
"hex": "a30071646174656b6579732d7365637572697479010102586aa3000101582011111111111111111111111111111111111111111111111111111111111111110258402222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222200", "hex": "a30071646174656b6579732d7365637572697479010102586aa3000101582011111111111111111111111111111111111111111111111111111111111111110258402222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222200",
"signature": "F1", "signature": "F1",
"seal": "S0" "seal": "S0",
"lines": [
"No se ha comprobado ninguna firma: trátala como no firmada."
]
}, },
{ {
"name": "a seal of seal_type 0", "name": "a seal of seal_type 0",
"hex": "a30071646174656b6579732d73656375726974790101035826a200000158203333333333333333333333333333333333333333333333333333333333333333", "hex": "a30071646174656b6579732d73656375726974790101035826a200000158203333333333333333333333333333333333333333333333333333333333333333",
"signature": "F0", "signature": "F0",
"seal": "S2" "seal": "S2",
"lines": [
"Sin firma de autor.",
"El sello de tiempo es ilegible: no prueba nada."
]
}, },
{ {
"name": "a seal that breaks its schema, with an unknown seal_type", "name": "a seal that breaks its schema, with an unknown seal_type",
"hex": "a30071646174656b6579732d73656375726974790101035829a300186301582033333333333333333333333333333333333333333333333333333333333333330200", "hex": "a30071646174656b6579732d73656375726974790101035829a300186301582033333333333333333333333333333333333333333333333333333333333333330200",
"signature": "F0", "signature": "F0",
"seal": "S2" "seal": "S2",
"lines": [
"Sin firma de autor.",
"El sello de tiempo es ilegible: no prueba nada."
]
}, },
{ {
"name": "a seal that is not CBOR", "name": "a seal that is not CBOR",
"hex": "a30071646174656b6579732d736563757269747901010341ff", "hex": "a30071646174656b6579732d736563757269747901010341ff",
"signature": "F0", "signature": "F0",
"seal": "S2" "seal": "S2",
"lines": [
"Sin firma de autor.",
"El sello de tiempo es ilegible: no prueba nada."
]
}, },
{ {
"name": "key 2 that is not a byte string", "name": "key 2 that is not a byte string",
"hex": "a30071646174656b6579732d7365637572697479010102a10001", "hex": "a30071646174656b6579732d7365637572697479010102a10001",
"signature": "X", "signature": "X",
"seal": "X" "seal": "X",
"lines": [
"No se han podido comprobar la firma ni el sello: trátala como no firmada y sin fecha probada."
]
}, },
{ {
"name": "key 2 that is an empty byte string", "name": "key 2 that is an empty byte string",
"hex": "a30071646174656b6579732d736563757269747901010240", "hex": "a30071646174656b6579732d736563757269747901010240",
"signature": "X", "signature": "X",
"seal": "X" "seal": "X",
"lines": [
"No se han podido comprobar la firma ni el sello: trátala como no firmada y sin fecha probada."
]
}, },
{ {
"name": "an unknown key 4", "name": "an unknown key 4",
"hex": "a30071646174656b6579732d73656375726974790101044101", "hex": "a30071646174656b6579732d73656375726974790101044101",
"signature": "X", "signature": "X",
"seal": "X" "seal": "X",
"lines": [
"No se han podido comprobar la firma ni el sello: trátala como no firmada y sin fecha probada."
]
}, },
{ {
"name": "a byte more after the map", "name": "a byte more after the map",
"hex": "a20071646174656b6579732d7365637572697479010100", "hex": "a20071646174656b6579732d7365637572697479010100",
"signature": "X", "signature": "X",
"seal": "X" "seal": "X",
"lines": [
"No se han podido comprobar la firma ni el sello: trátala como no firmada y sin fecha probada."
]
}, },
{ {
"name": "version 2", "name": "version 2",
"hex": "a20071646174656b6579732d73656375726974790102", "hex": "a20071646174656b6579732d73656375726974790102",
"signature": "X", "signature": "X",
"seal": "X" "seal": "X",
"lines": [
"No se han podido comprobar la firma ni el sello: trátala como no firmada y sin fecha probada."
]
}, },
{ {
"name": "the type tag of the head", "name": "the type tag of the head",
"hex": "a2006d646174656b6579732d686561640101", "hex": "a2006d646174656b6579732d686561640101",
"signature": "X", "signature": "X",
"seal": "X" "seal": "X",
"lines": [
"No se han podido comprobar la firma ni el sello: trátala como no firmada y sin fecha probada."
]
}, },
{ {
"name": "keys 2 and 3 out of order", "name": "keys 2 and 3 out of order",
"hex": "a40071646174656b6579732d73656375726974790101035826a200010158203333333333333333333333333333333333333333333333333333333333333333025869a30001015820111111111111111111111111111111111111111111111111111111111111111102584022222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222", "hex": "a40071646174656b6579732d73656375726974790101035826a200010158203333333333333333333333333333333333333333333333333333333333333333025869a30001015820111111111111111111111111111111111111111111111111111111111111111102584022222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222",
"signature": "X", "signature": "X",
"seal": "X" "seal": "X",
"lines": [
"No se han podido comprobar la firma ni el sello: trátala como no firmada y sin fecha probada."
]
}, },
{ {
"name": "an array", "name": "an array",
"hex": "8271646174656b6579732d736563757269747901", "hex": "8271646174656b6579732d736563757269747901",
"signature": "X", "signature": "X",
"seal": "X" "seal": "X",
"lines": [
"No se han podido comprobar la firma ni el sello: trátala como no firmada y sin fecha probada."
]
} }
] ]
} }

File diff suppressed because one or more lines are too long
Loading…
Cancel
Save

Powered by TurnKey Linux.