testdata synced from datekeys-go 3820066 (verified with sync-testdata check --against): cbor.json, mutations.json, inspect_differential.json, the inspect goldens, testdata/README.md and the three new dk1.json vectors. The alignment of src/lib/dkc and of the vector harness was cut off by a usage limit halfway through a refactor: tests and typecheck fail. Kept on this branch so that main stays green; it is finished here and merged when every check passes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>main
parent
ee2dba93cc
commit
3305bbb1a8
@ -0,0 +1,43 @@
|
||||
// Helpers shared by the decoders of the protocol objects (PUBLIC_HEADER,
|
||||
// CONTROL_CBOR, .dkk BODY_CBOR), as the Go reference writes each of them: a
|
||||
// loop over the keys of the map, one read per key, "key N: " before the
|
||||
// error of a read, and the required keys checked after the loop.
|
||||
|
||||
import type { Encoder, Uint } from './cbor.ts';
|
||||
import { DateKeysError, withContext } from './errors.ts';
|
||||
import { encodeArray, type Extension } from './extension.ts';
|
||||
|
||||
/** Runs the read of the value of key `k`, prefixing its error with "key k". */
|
||||
export function fieldOf(k: Uint): <T>(read: () => T) => T {
|
||||
return (read) => withContext(`key ${k}`, read);
|
||||
}
|
||||
|
||||
/** Requires the keys 0 to n-1 in `seen` (ERR_NON_CANONICAL_CBOR otherwise). */
|
||||
export function requireKeys(seen: ReadonlySet<number>, n: number): void {
|
||||
for (let k = 0; k < n; k++) {
|
||||
if (!seen.has(k)) throw new DateKeysError('ERR_NON_CANONICAL_CBOR', `key ${k} is missing`);
|
||||
}
|
||||
}
|
||||
|
||||
/** 1 for an extension array that is written, 0 for one that is omitted (spec §58.1). */
|
||||
export function presence(exts: readonly Extension[] | undefined): number {
|
||||
return exts !== undefined && exts.length > 0 ? 1 : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the critical and noncritical extension arrays at keys `key` and
|
||||
* `key + 1`, each only when it is present (spec §58.1).
|
||||
*/
|
||||
export function encodeExtensionArrays(
|
||||
e: Encoder,
|
||||
key: number,
|
||||
critical: readonly Extension[] | undefined,
|
||||
noncritical: readonly Extension[] | undefined,
|
||||
): void {
|
||||
for (const [i, exts] of [critical, noncritical].entries()) {
|
||||
if (presence(exts) === 1) {
|
||||
e.uint(key + i);
|
||||
encodeArray(e, exts!);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,131 @@
|
||||
// Readers of the shared vector formats of the Go reference, as
|
||||
// testdata/README.md documents them. Every reader checks the structure it
|
||||
// reads and throws on anything else: an unknown key, a missing one or a value
|
||||
// of another type, so that a format change fails the harness instead of
|
||||
// being skipped. Tests only.
|
||||
|
||||
import { fromHex } from '../bytes.ts';
|
||||
import { isErrorCode } from '../errors.ts';
|
||||
|
||||
export type Json = Record<string, unknown>;
|
||||
|
||||
/** The version of the specification every vector file must name. */
|
||||
export const SPEC_VERSION = '0.8.2';
|
||||
|
||||
export class FormatError extends Error {
|
||||
constructor(where: string, what: string) {
|
||||
super(`${where}: ${what}`);
|
||||
this.name = 'FormatError';
|
||||
}
|
||||
}
|
||||
|
||||
/** Requires a plain JSON object. */
|
||||
export function object(v: unknown, where: string): Json {
|
||||
if (v === null || typeof v !== 'object' || Array.isArray(v)) throw new FormatError(where, 'not an object');
|
||||
return v as Json;
|
||||
}
|
||||
|
||||
/** Requires an array. */
|
||||
export function array(v: unknown, where: string): unknown[] {
|
||||
if (!Array.isArray(v)) throw new FormatError(where, 'not an array');
|
||||
return v;
|
||||
}
|
||||
|
||||
/**
|
||||
* Requires exactly the keys of `required` and at most those of `optional`:
|
||||
* an unknown key fails, and so does a missing one.
|
||||
*/
|
||||
export function keys(o: Json, where: string, required: readonly string[], optional: readonly string[] = []): void {
|
||||
for (const k of required) if (!Object.hasOwn(o, k)) throw new FormatError(where, `missing "${k}"`);
|
||||
for (const k of Object.keys(o)) {
|
||||
if (!required.includes(k) && !optional.includes(k)) throw new FormatError(where, `unknown key "${k}"`);
|
||||
}
|
||||
}
|
||||
|
||||
export function str(v: unknown, where: string): string {
|
||||
if (typeof v !== 'string') throw new FormatError(where, 'not a string');
|
||||
return v;
|
||||
}
|
||||
|
||||
export function bool(v: unknown, where: string): boolean {
|
||||
if (typeof v !== 'boolean') throw new FormatError(where, 'not a boolean');
|
||||
return v;
|
||||
}
|
||||
|
||||
/** A non-negative safe integer. */
|
||||
export function int(v: unknown, where: string): number {
|
||||
if (!Number.isSafeInteger(v) || (v as number) < 0) throw new FormatError(where, 'not a non-negative integer');
|
||||
return v as number;
|
||||
}
|
||||
|
||||
/** Lowercase hexadecimal, as every binary value of the files (README conventions). */
|
||||
export function hexBytes(v: unknown, where: string): Uint8Array {
|
||||
const s = str(v, where);
|
||||
if (!/^(?:[0-9a-f]{2})*$/.test(s)) throw new FormatError(where, 'not lowercase hex');
|
||||
return fromHex(s);
|
||||
}
|
||||
|
||||
/** A normative code of spec §69. */
|
||||
export function code(v: unknown, where: string): string {
|
||||
const s = str(v, where);
|
||||
if (!isErrorCode(s)) throw new FormatError(where, `"${s}" is not a normative code`);
|
||||
return s;
|
||||
}
|
||||
|
||||
/** "ok" or a normative code. */
|
||||
export function result(v: unknown, where: string): string {
|
||||
const s = str(v, where);
|
||||
return s === 'ok' ? s : code(s, where);
|
||||
}
|
||||
|
||||
/** A step of the reading flow of spec §63, 1 to 18. */
|
||||
export function step(v: unknown, where: string): number {
|
||||
const n = int(v, where);
|
||||
if (n < 1 || n > 18) throw new FormatError(where, `step ${n} outside 1..18`);
|
||||
return n;
|
||||
}
|
||||
|
||||
/** The file-level "spec" field. */
|
||||
export function checkSpec(o: Json, where: string): void {
|
||||
if (str(o.spec, `${where}.spec`) !== SPEC_VERSION) throw new FormatError(where, `spec ${String(o.spec)}, want ${SPEC_VERSION}`);
|
||||
}
|
||||
|
||||
/** One edit [at, delete, insert] of README "Edited files". */
|
||||
export interface Edit {
|
||||
readonly at: number;
|
||||
readonly delete: number;
|
||||
readonly insert: Uint8Array;
|
||||
}
|
||||
|
||||
export function edits(v: unknown, where: string): Edit[] {
|
||||
return array(v, where).map((e, i) => {
|
||||
const w = `${where}[${i}]`;
|
||||
const a = array(e, w);
|
||||
if (a.length !== 3) throw new FormatError(w, 'an edit is [at, delete, insert]');
|
||||
return { at: int(a[0], `${w}.at`), delete: int(a[1], `${w}.delete`), insert: hexBytes(a[2], `${w}.insert`) };
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies edits to `base` in one pass, as README "Edited files" states: the
|
||||
* edits refer to offsets of the unmodified base, are sorted by offset, do not
|
||||
* overlap and stay within the base. Anything else throws.
|
||||
*/
|
||||
export function applyEdits(base: Uint8Array, list: readonly Edit[]): Uint8Array {
|
||||
const parts: Uint8Array[] = [];
|
||||
let pos = 0;
|
||||
for (const [i, e] of list.entries()) {
|
||||
if (e.at < pos) throw new Error(`edit ${i} at ${e.at} is out of order or overlaps the previous one`);
|
||||
if (e.at + e.delete > base.length) throw new Error(`edit ${i} goes beyond the base of ${base.length} bytes`);
|
||||
parts.push(base.subarray(pos, e.at), e.insert);
|
||||
pos = e.at + e.delete;
|
||||
}
|
||||
parts.push(base.subarray(pos));
|
||||
const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
|
||||
let o = 0;
|
||||
for (const p of parts) {
|
||||
out.set(p, o);
|
||||
o += p.length;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,60 @@
|
||||
{
|
||||
"file": "empty_payload.dkc",
|
||||
"capsule_id": "ab10174561a9a19a6d9dc9ab1ef59c66",
|
||||
"datekey": "dk1_eyJ2ZXJzaW9uIjoxLCJuZXR3b3JrIjoiZGF0ZWtleXM6cXVpY2tuZXQ6djEiLCJyb3VuZCI6MTAwMX0",
|
||||
"profile": "datekeys:quicknet:v1",
|
||||
"round": 1001,
|
||||
"unlock_at": "2023-08-23T15:59:27Z",
|
||||
"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 v1, PUBLIC_HEADER_LEN=121, SEALED_CONTROL_LEN=446"
|
||||
},
|
||||
{
|
||||
"step": 3,
|
||||
"name": "public header",
|
||||
"ok": true,
|
||||
"detail": "121 bytes"
|
||||
},
|
||||
{
|
||||
"step": 4,
|
||||
"name": "header validation",
|
||||
"ok": true,
|
||||
"detail": "capsule_id=ab10174561a9a19a6d9dc9ab1ef59c66 datekey=dk1_eyJ2ZXJzaW9uIjoxLCJuZXR3b3JrIjoiZGF0ZWtleXM6cXVpY2tuZXQ6djEiLCJyb3VuZCI6MTAwMX0 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 1001, unlock at 2023-08-23T15:59:27Z"
|
||||
},
|
||||
{
|
||||
"step": 8,
|
||||
"name": "tlock stanza",
|
||||
"ok": true,
|
||||
"detail": "round 1001, chain 52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971"
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -0,0 +1,60 @@
|
||||
{
|
||||
"file": "time_and_key_portable.dkc",
|
||||
"capsule_id": "448e134a13457c319cab7fceaf7ffa1f",
|
||||
"datekey": "dk1_eyJ2ZXJzaW9uIjoxLCJuZXR3b3JrIjoiZGF0ZWtleXM6cXVpY2tuZXQ6djEiLCJyb3VuZCI6MTAwMH0",
|
||||
"profile": "datekeys:quicknet:v1",
|
||||
"round": 1000,
|
||||
"unlock_at": "2023-08-23T15:59:24Z",
|
||||
"access_policy": "time_and_key",
|
||||
"valid": true,
|
||||
"checks": [
|
||||
{
|
||||
"step": 1,
|
||||
"name": "parse DKC1",
|
||||
"ok": true,
|
||||
"detail": "magic DKC1"
|
||||
},
|
||||
{
|
||||
"step": 2,
|
||||
"name": "prelude",
|
||||
"ok": true,
|
||||
"detail": "DKC1 v1, PUBLIC_HEADER_LEN=121, SEALED_CONTROL_LEN=646"
|
||||
},
|
||||
{
|
||||
"step": 3,
|
||||
"name": "public header",
|
||||
"ok": true,
|
||||
"detail": "121 bytes"
|
||||
},
|
||||
{
|
||||
"step": 4,
|
||||
"name": "header validation",
|
||||
"ok": true,
|
||||
"detail": "capsule_id=448e134a13457c319cab7fceaf7ffa1f datekey=dk1_eyJ2ZXJzaW9uIjoxLCJuZXR3b3JrIjoiZGF0ZWtleXM6cXVpY2tuZXQ6djEiLCJyb3VuZCI6MTAwMH0 policy=time_and_key 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,60 @@
|
||||
{
|
||||
"file": "time_and_key_recipients.dkc",
|
||||
"capsule_id": "c75dfc8e9c576d1369910664df93693a",
|
||||
"datekey": "dk1_eyJ2ZXJzaW9uIjoxLCJuZXR3b3JrIjoiZGF0ZWtleXM6cXVpY2tuZXQ6djEiLCJyb3VuZCI6MTAwMX0",
|
||||
"profile": "datekeys:quicknet:v1",
|
||||
"round": 1001,
|
||||
"unlock_at": "2023-08-23T15:59:27Z",
|
||||
"access_policy": "time_and_key",
|
||||
"valid": true,
|
||||
"checks": [
|
||||
{
|
||||
"step": 1,
|
||||
"name": "parse DKC1",
|
||||
"ok": true,
|
||||
"detail": "magic DKC1"
|
||||
},
|
||||
{
|
||||
"step": 2,
|
||||
"name": "prelude",
|
||||
"ok": true,
|
||||
"detail": "DKC1 v1, PUBLIC_HEADER_LEN=121, SEALED_CONTROL_LEN=842"
|
||||
},
|
||||
{
|
||||
"step": 3,
|
||||
"name": "public header",
|
||||
"ok": true,
|
||||
"detail": "121 bytes"
|
||||
},
|
||||
{
|
||||
"step": 4,
|
||||
"name": "header validation",
|
||||
"ok": true,
|
||||
"detail": "capsule_id=c75dfc8e9c576d1369910664df93693a datekey=dk1_eyJ2ZXJzaW9uIjoxLCJuZXR3b3JrIjoiZGF0ZWtleXM6cXVpY2tuZXQ6djEiLCJyb3VuZCI6MTAwMX0 policy=time_and_key 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 1001, unlock at 2023-08-23T15:59:27Z"
|
||||
},
|
||||
{
|
||||
"step": 8,
|
||||
"name": "tlock stanza",
|
||||
"ok": true,
|
||||
"detail": "round 1001, chain 52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971"
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -0,0 +1,60 @@
|
||||
{
|
||||
"file": "time_only.dkc",
|
||||
"capsule_id": "ad4d676812b134ff8a3de263f77018b4",
|
||||
"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 v1, PUBLIC_HEADER_LEN=121, SEALED_CONTROL_LEN=446"
|
||||
},
|
||||
{
|
||||
"step": 3,
|
||||
"name": "public header",
|
||||
"ok": true,
|
||||
"detail": "121 bytes"
|
||||
},
|
||||
{
|
||||
"step": 4,
|
||||
"name": "header validation",
|
||||
"ok": true,
|
||||
"detail": "capsule_id=ad4d676812b134ff8a3de263f77018b4 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,60 @@
|
||||
{
|
||||
"file": "time_only_extensions.dkc",
|
||||
"capsule_id": "4286085c21ca34d1a71e649326a4a0f6",
|
||||
"datekey": "dk1_eyJ2ZXJzaW9uIjoxLCJuZXR3b3JrIjoiZGF0ZWtleXM6cXVpY2tuZXQ6djEiLCJyb3VuZCI6MjAwMH0",
|
||||
"profile": "datekeys:quicknet:v1",
|
||||
"round": 2000,
|
||||
"unlock_at": "2023-08-23T16:49: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 v1, PUBLIC_HEADER_LEN=159, SEALED_CONTROL_LEN=482"
|
||||
},
|
||||
{
|
||||
"step": 3,
|
||||
"name": "public header",
|
||||
"ok": true,
|
||||
"detail": "159 bytes"
|
||||
},
|
||||
{
|
||||
"step": 4,
|
||||
"name": "header validation",
|
||||
"ok": true,
|
||||
"detail": "capsule_id=4286085c21ca34d1a71e649326a4a0f6 datekey=dk1_eyJ2ZXJzaW9uIjoxLCJuZXR3b3JrIjoiZGF0ZWtleXM6cXVpY2tuZXQ6djEiLCJyb3VuZCI6MjAwMH0 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 2000, unlock at 2023-08-23T16:49:24Z"
|
||||
},
|
||||
{
|
||||
"step": 8,
|
||||
"name": "tlock stanza",
|
||||
"ok": true,
|
||||
"detail": "round 2000, chain 52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971"
|
||||
}
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Loading…
Reference in new issue