The signature of alg 1 in the library: strict Ed25519, what is signed, F2 to F4

ed25519strict.ts checks the four conditions of spec v0.11 29.9 on top of the
arithmetic of @noble/curves and gives the answer of Go on the 18 vectors of
ed25519_strict.json. author.ts computes payload_commit, control_commit,
head_digest, signers_digest, AUTHOR_MESSAGE and its code, as the record of
format3_signed says. evaluateSecurity takes the context of the capsule and
gives F2, F3 or F4; open passes it, and OpenOptions.authorKeys are the keys
the person saved. No new package: both modules use @noble/curves and
@noble/hashes, which were already in the bundle.

Alg 2 and the time seal are still read as v0.10 reads them, and the tests say
so with testing/pending.ts. npm run verify passes.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
main
dev 6 days ago
parent e3cf2409cd
commit c31ab2d2e7

@ -7,7 +7,8 @@ Cambios notables de la librería TypeScript y de la página. El proyecto usa ver
### `testdata` en `spec-v0.11` (01-10-2026) ### `testdata` en `spec-v0.11` (01-10-2026)
- `testdata` se sincroniza con el tag `spec-v0.11` de `datekeys-go` (`ae33434`), y `SPEC_VERSION` pasa a `0.11`. Trae tres fixtures (`format3_signed`, con firma de clave propia; `format3_signed_cms`, con dos certificados sellados; `format3_sealed`, con firma y sello RFC 3161) y tres ficheros de vectores (`ed25519_strict.json`, `security_cms.json` y `locator.json`). El corpus de mutaciones pasa a 210 casos, con uno fuera del §64: una firma de `alg` 1 que no verifica, F2. `ibe-vectors.json` añade los tres fixtures y rehace los de `format3_signature_unsupported` y `format3_seal_unsupported`; `mutation-texts.json` se rehace con el `capsule.Open` de esa referencia. - `testdata` se sincroniza con el tag `spec-v0.11` de `datekeys-go` (`ae33434`), y `SPEC_VERSION` pasa a `0.11`. Trae tres fixtures (`format3_signed`, con firma de clave propia; `format3_signed_cms`, con dos certificados sellados; `format3_sealed`, con firma y sello RFC 3161) y tres ficheros de vectores (`ed25519_strict.json`, `security_cms.json` y `locator.json`). El corpus de mutaciones pasa a 210 casos, con uno fuera del §64: una firma de `alg` 1 que no verifica, F2. `ibe-vectors.json` añade los tres fixtures y rehace los de `format3_signature_unsupported` y `format3_seal_unsupported`; `mutation-texts.json` se rehace con el `capsule.Open` de esa referencia.
- **Lo que esta biblioteca no hace todavía:** lee el área de seguridad como un lector de la v0.10, así que una firma o un sello que la referencia comprueba da F1 o S1 aquí, y la referencia da F2 a F6 y S3 a S5. El tipo `Verdict` y los textos ya los conocen. Los tests lo dicen en lugar de ocultarlo: `testing/pending.ts` da lo que esta biblioteca devuelve donde la referencia registra una verificación, y un bloque de `vectors.test.ts` comprueba la estructura de los vectores que aún no se portan. Portar la verificación (§29.8 a §29.11 y el localizador del §44.1) hace esas funciones la identidad. - **La firma de clave propia, `alg` 1** (§29.8, §29.9), portada: `ed25519strict.ts` comprueba las cuatro condiciones del perfil estricto con la aritmética de `@noble/curves` (que solo ofrece la ecuación con cofactor) y da la respuesta de Go en los 18 vectores de `ed25519_strict.json`; `author.ts` calcula `payload_commit`, `control_commit`, `head_digest`, `signers_digest`, `AUTHOR_MESSAGE` y su código, y los registros de `format3_signed` los confirman. `evaluateSecurity(área, contexto)` da F2, F3 y F4, `open` pasa el contexto del control y del head, y `OpenOptions.authorKeys` son las claves que la persona guardó. Los dos módulos usan solo `@noble/curves` y `@noble/hashes`, que ya iban en el bundle: ningún paquete nuevo, y entran en la lista de quien puede importar noble.
- **Lo que esta biblioteca no hace todavía:** la firma con certificados (`alg` 2: F5, F6) y el sello RFC 3161 (S3 a S5), que lee como un lector de la v0.10 y dan F1 o S1 aquí; y el localizador del §44.1. El tipo `Verdict` y los textos ya los conocen. Los tests lo dicen en lugar de ocultarlo: `testing/pending.ts` da lo que esta biblioteca devuelve donde la referencia registra esas verificaciones, y un bloque de `vectors.test.ts` comprueba la estructura de los vectores que aún no se portan. Portarlas hace esas funciones la identidad.
El formato 3 de la especificación 0.10, según `PLAN_formato3_ts.md` (en `../docs`). La versión que lo publique la decide el autor. El formato 3 de la especificación 0.10, según `PLAN_formato3_ts.md` (en `../docs`). La versión que lo publique la decide el autor.

@ -1,6 +1,6 @@
# datekeys-ts # datekeys-ts
Implementación en TypeScript del protocolo DateKeys (formato 3 de la v0.10; con los datos de prueba de la v0.11, cuya verificación de firmas y sellos está pendiente) y página de prueba en el navegador. Sustituye al prototipo, archivado en `../archive/prototype` (API Quicknet en Go, CLI tlock y cliente Svelte, commit `4d2b0a1`). Implementación en TypeScript del protocolo DateKeys (formato 3 de la v0.10; de la v0.11, la firma de clave propia; la firma con certificados, el sello y el localizador están pendientes) y página de prueba en el navegador. Sustituye al prototipo, archivado en `../archive/prototype` (API Quicknet en Go, CLI tlock y cliente Svelte, commit `4d2b0a1`).
La implementación de referencia es la librería Go `g.activething.com/go/DateKeys`, en `../datekeys-go`. Los planes y el estado del trabajo están en `../docs`, el repositorio privado de documentación del proyecto. La implementación de referencia es la librería Go `g.activething.com/go/DateKeys`, en `../datekeys-go`. Los planes y el estado del trabajo están en `../docs`, el repositorio privado de documentación del proyecto.

@ -15,8 +15,8 @@
// another 2.x copy anywhere but under @noble/post-quantum, which pins // another 2.x copy anywhere but under @noble/post-quantum, which pins
// ~2.0.0 and uses its copy for ML-KEM only (plan decision 5); // ~2.0.0 and uses its copy for ML-KEM only (plan decision 5);
// - a file of src/ imports tlock-js or drand-client; // - a file of src/ imports tlock-js or drand-client;
// - a file of src/ other than digest.ts, ibe.ts, release.ts, x25519.ts and // - a file of src/ other than author.ts, digest.ts, ed25519strict.ts, ibe.ts,
// the tests names @noble/, or a noble import is not a subpath of @noble/curves, // release.ts, x25519.ts and the tests names @noble/, or a noble import is not a subpath of @noble/curves,
// @noble/hashes or @noble/ciphers, the root copies that those files // @noble/hashes or @noble/ciphers, the root copies that those files
// resolve to. // resolve to.
// //
@ -46,7 +46,14 @@ const NOBLE = ['@noble/ciphers', '@noble/curves', '@noble/hashes'];
// and releases are verified locally (plan decisions 1 and 4). // and releases are verified locally (plan decisions 1 and 4).
const FORBIDDEN = ['tlock-js', 'drand-client']; const FORBIDDEN = ['tlock-js', 'drand-client'];
// The only files besides the tests that may import noble. // The only files besides the tests that may import noble.
const NOBLE_IMPORTERS = ['src/lib/dkc/digest.ts', 'src/lib/dkc/ibe.ts', 'src/lib/dkc/release.ts', 'src/lib/dkc/x25519.ts']; const NOBLE_IMPORTERS = [
'src/lib/dkc/author.ts',
'src/lib/dkc/digest.ts',
'src/lib/dkc/ed25519strict.ts',
'src/lib/dkc/ibe.ts',
'src/lib/dkc/release.ts',
'src/lib/dkc/x25519.ts',
];
// The only files besides the tests that may import age-encryption (plan of // The only files besides the tests that may import age-encryption (plan of
// phase 3, decision 13): the opening, the tlock recipient and the writer. // phase 3, decision 13): the opening, the tlock recipient and the writer.
const AGE_IMPORTERS = ['src/lib/dkc/agefile.ts', 'src/lib/dkc/open.ts', 'src/lib/dkc/tlock.ts', 'src/lib/dkc/writer.ts']; const AGE_IMPORTERS = ['src/lib/dkc/agefile.ts', 'src/lib/dkc/open.ts', 'src/lib/dkc/tlock.ts', 'src/lib/dkc/writer.ts'];
@ -61,8 +68,10 @@ const WRITER_IMPORTER_DIRS = ['src/lib/dkc/testing/'];
// paths, whose Unicode tables load with the opening and the writer. // paths, whose Unicode tables load with the opening and the writer.
const NOT_IN_INDEX = [ const NOT_IN_INDEX = [
'agefile.ts', 'agefile.ts',
'author.ts',
'bech32.ts', 'bech32.ts',
'digest.ts', 'digest.ts',
'ed25519strict.ts',
'encrypt.ts', 'encrypt.ts',
'head.ts', 'head.ts',
'ibe.ts', 'ibe.ts',
@ -205,7 +214,7 @@ describe('runtime dependencies', () => {
} }
}); });
it('only digest.ts, ibe.ts, release.ts, x25519.ts and the tests import noble, only from @noble/curves, @noble/hashes and @noble/ciphers, and nothing imports tlock-js or drand-client', () => { it('only author.ts, digest.ts, ed25519strict.ts, ibe.ts, release.ts, x25519.ts and the tests import noble, only from @noble/curves, @noble/hashes and @noble/ciphers, and nothing imports tlock-js or drand-client', () => {
expect(importProblems(sources())).toEqual([]); expect(importProblems(sources())).toEqual([]);
}); });

@ -0,0 +1,135 @@
// Tests of author.ts and of the verdicts of a signature of alg 1 (spec v0.11,
// §29.7 to §29.9) against format3_signed, the fixture of the Go reference whose
// record gives every commitment, the message and the key.
import { describe, expect, it } from 'vitest';
import { ALG_CMS, ALG_ED25519, AUTHOR_MESSAGE_SIZE, authorCode, authorMessage, controlCommit, headDigest, payloadCommit, signersDigest } from './author.ts';
import { bech32Decode, bech32Encode } from './bech32.ts';
import { Encoder } from './cbor.ts';
import { type Control, decodeControl, encodeControl } from './control.ts';
import { FORMAT_3 } from './framing.ts';
import { encodeSecurity, evaluateSecurity, type SecurityContext, verdictLines } from './security.ts';
import { h, hx, readJSON } from './testing/testdata.ts';
interface Record {
control_cbor: string;
head_cbor: string;
security_cbor: string;
signature: {
control_commit: string;
head_digest: string;
signers_digest: string;
author_message: string;
author_code: string;
author_key: string;
signature: string;
};
verdicts: { lines: string[] };
}
const fx = readJSON<Record>('fixtures/format3_signed.json');
const control = decodeControl(h(fx.control_cbor), FORMAT_3);
const context = (): SecurityContext => ({ controlCommit: controlCommit(control, FORMAT_3), headDigest: headDigest(h(fx.head_cbor)) });
const security = h(fx.security_cbor);
const key = bech32Decode(fx.signature.author_key).data;
const value = h(fx.signature.signature);
// SECURITY_CBOR with an author-signature of alg `alg`, the key and the signature value given.
function area(alg: number, k: Uint8Array, v: Uint8Array): Uint8Array {
const c = new Encoder();
c.map(3);
c.uint(0);
c.uint(alg);
c.uint(1);
c.bstr(k);
c.uint(2);
c.bstr(v);
return withKey2(c.out());
}
function withKey2(content: Uint8Array): Uint8Array {
const e = new Encoder();
e.map(3);
e.uint(0);
e.text('datekeys-security');
e.uint(1);
e.uint(1);
e.uint(2);
e.bstr(content);
return e.out();
}
describe('what is signed', () => {
it('computes the commitments, the message and the code that the reference records', () => {
const cc = controlCommit(control, FORMAT_3);
const hd = headDigest(h(fx.head_cbor));
const sd = signersDigest(ALG_ED25519);
const message = authorMessage(cc, hd, sd);
expect([hx(cc), hx(hd), hx(sd), new TextDecoder().decode(message), authorCode(message)]).toEqual([
fx.signature.control_commit,
fx.signature.head_digest,
fx.signature.signers_digest,
fx.signature.author_message,
fx.signature.author_code,
]);
expect(message.length).toBe(AUTHOR_MESSAGE_SIZE);
expect(AUTHOR_MESSAGE_SIZE).toBe(99);
expect(authorCode(message.subarray(1))).toBe('');
});
it('binds alg and the list of signers, and leaves L out and I_PAYLOAD hidden', () => {
expect(hx(signersDigest(ALG_ED25519))).not.toBe(hx(signersDigest(ALG_CMS)));
expect(hx(signersDigest(ALG_CMS, Uint8Array.of(1)))).not.toBe(hx(signersDigest(ALG_CMS, Uint8Array.of(2))));
const base = controlCommit(control, FORMAT_3);
// L does not enter control_commit, so an area can grow after signing.
expect(hx(controlCommit({ ...control, payloadLength: 1 << 30 }, FORMAT_3))).toBe(hx(base));
// I_PAYLOAD does, through its commitment.
const other: Control = { ...control, payloadIdentity: control.payloadIdentity.map((b) => b ^ 1) };
expect(hx(controlCommit(other, FORMAT_3))).not.toBe(hx(base));
expect(hx(payloadCommit(control.payloadIdentity))).not.toBe(hx(control.payloadIdentity));
expect(encodeControl(control, FORMAT_3)).toHaveLength(encodeControl(other, FORMAT_3).length);
});
});
describe('the verdicts of a signature of alg 1', () => {
it('gives F4 with the key of the signature, and F3 when the person saved it', () => {
const v = evaluateSecurity(security, context());
expect([v.signature, v.seal, hx(v.authorKey!)]).toEqual(['F4', 'S0', hx(key)]);
expect(verdictLines(v)).toEqual(fx.verdicts.lines);
const saved = evaluateSecurity(security, { ...context(), authorKeys: new Map([[fx.signature.author_key, 'Ana']]) });
expect([saved.signature, saved.authorLabel]).toEqual(['F3', 'Ana']);
expect(verdictLines(saved)).toEqual(['Firmado con la clave que guardaste como Ana.']);
// Another saved key is not this one.
const luis = new Map([[bech32Encode('dkauthor', new Uint8Array(32).fill(7)), 'Luis']]);
expect(evaluateSecurity(security, { ...context(), authorKeys: luis }).signature).toBe('F4');
});
it('gives F2 in another capsule, and F1 without a context or for what it does not check', () => {
expect(evaluateSecurity(security, { ...context(), headDigest: headDigest(Uint8Array.of(1)) }).signature).toBe('F2');
expect(evaluateSecurity(security, { ...context(), controlCommit: new Uint8Array(32) }).signature).toBe('F2');
expect(evaluateSecurity(security).signature).toBe('F1');
expect(evaluateSecurity(area(ALG_ED25519, key, value), context()).signature).toBe('F4');
const flipped = value.slice();
flipped[5] = flipped[5]! ^ 1;
expect(evaluateSecurity(area(ALG_ED25519, key, flipped), context()).signature).toBe('F2');
expect(evaluateSecurity(area(ALG_CMS, key, value), context()).signature).toBe('F1');
expect(evaluateSecurity(area(4294967295, key, value), context()).signature).toBe('F1');
expect(evaluateSecurity(area(ALG_ED25519, key.subarray(1), value), context()).signature).toBe('F1');
expect(evaluateSecurity(area(ALG_ED25519, key, value.subarray(1)), context()).signature).toBe('F1');
// Content that does not decode, or whose map has a key that no version defines.
expect(evaluateSecurity(withKey2(Uint8Array.of(0xff)), context()).signature).toBe('F1');
const unknown = new Encoder();
unknown.map(3);
unknown.uint(0);
unknown.uint(ALG_ED25519);
unknown.uint(1);
unknown.bstr(key);
unknown.uint(3);
unknown.bstr(value);
expect(evaluateSecurity(withKey2(unknown.out()), context()).signature).toBe('F1');
// No signature: F0, whatever the context.
expect(evaluateSecurity(encodeSecurity(), context()).signature).toBe('F0');
});
});

@ -0,0 +1,77 @@
// What an author signs and a seal seals (spec v0.11, §29.8, §29.11), as the Go
// package capsule computes it (signature.go): payload_commit, control_commit
// over CONTROL_SIG, head_digest, signers_digest, and AUTHOR_MESSAGE, the ASCII
// text of 99 bytes that is signed, with its code. Every value is recomputed
// from the capsule once it is open; none is stored. Internal: index.ts does
// not re-export it.
import { sha256 } from '@noble/hashes/sha2.js';
import { concatBytes, toHex } from './bytes.ts';
import { type Control, encodeControl } from './control.ts';
import type { Format } from './framing.ts';
const PAYLOAD_COMMIT_PREFIX = 'datekeys:dkc3:payload:v1';
const CONTROL_COMMIT_PREFIX = 'datekeys:dkc3:control:v1';
const HEAD_DIGEST_PREFIX = 'datekeys:dkc3:head:v1';
const SIGNERS_DIGEST_PREFIX = 'datekeys:dkc3:signers:v1';
/** The first line of AUTHOR_MESSAGE. */
export const AUTHOR_MESSAGE_PREFIX = 'datekeys:dkc3:author-signature:v1';
/** The length of AUTHOR_MESSAGE: the prefix, a line feed, the 64 hexadecimal digits of its digest and a line feed. */
export const AUTHOR_MESSAGE_SIZE = AUTHOR_MESSAGE_PREFIX.length + 1 + 64 + 1;
/** The values of alg that spec v0.11 defines (§29.3). */
export const ALG_ED25519 = 1;
export const ALG_CMS = 2;
const text = new TextEncoder();
// SHA-256(prefix || 0x00 || parts...): each prefix is ASCII and is followed by a byte 0x00.
function domainHash(prefix: string, ...parts: Uint8Array[]): Uint8Array {
return sha256(concatBytes(text.encode(prefix), Uint8Array.of(0), ...parts));
}
/** The commitment to I_PAYLOAD that replaces it in what is signed (spec §29.8). */
export function payloadCommit(identity: Uint8Array): Uint8Array {
return domainHash(PAYLOAD_COMMIT_PREFIX, identity);
}
/**
* control_commit: the hash of CONTROL_SIG, the control of a capsule of format
* 3 with payload_commit in place of I_PAYLOAD and the eight bytes of L at
* zero (spec §29.8). It does not depend on L, so the area can grow after
* signing. The encoded control holds the commitment and not I_PAYLOAD, and
* is wiped.
*/
export function controlCommit(c: Control, format: Format): Uint8Array {
const b = encodeControl({ ...c, payloadIdentity: payloadCommit(c.payloadIdentity), payloadLength: 0 }, format);
try {
return domainHash(CONTROL_COMMIT_PREFIX, b);
} finally {
b.fill(0);
}
}
/** head_digest, the hash of HEAD_CBOR (spec §29.8). The salt of the head makes it a commitment that hides the files. */
export function headDigest(head: Uint8Array): Uint8Array {
return domainHash(HEAD_DIGEST_PREFIX, head);
}
/** signers_digest for alg and the exact content of key 1 of a signature of alg 2, `signers`; none with alg 1 (spec §29.8). */
export function signersDigest(alg: number, signers?: Uint8Array): Uint8Array {
const a = new Uint8Array(4);
new DataView(a.buffer).setUint32(0, alg);
return domainHash(SIGNERS_DIGEST_PREFIX, a, signers ?? new Uint8Array(0));
}
/** AUTHOR_MESSAGE: the prefix, a line feed, the hexadecimal digest D of the three commitments and a line feed (spec §29.8). */
export function authorMessage(controlCommitment: Uint8Array, headDigestValue: Uint8Array, signersDigestValue: Uint8Array): Uint8Array {
const d = sha256(concatBytes(controlCommitment, headDigestValue, signersDigestValue));
return text.encode(`${AUTHOR_MESSAGE_PREFIX}\n${toHex(d)}\n`);
}
/** The code of AUTHOR_MESSAGE that a person compares before signing: the first 8 hexadecimal digits of its digest, in two groups of 4. */
export function authorCode(message: Uint8Array): string {
if (message.length !== AUTHOR_MESSAGE_SIZE) return '';
const d = new TextDecoder().decode(message.subarray(AUTHOR_MESSAGE_PREFIX.length + 1));
return `${d.slice(0, 4)}-${d.slice(4, 8)}`;
}

@ -0,0 +1,48 @@
// Tests of ed25519strict.ts against the vectors of the Go reference: the
// strict profile of the author signature (spec v0.11, §29.9).
import { describe, expect, it } from 'vitest';
import { verifyStrict } from './ed25519strict.ts';
import { h, readJSON } from './testing/testdata.ts';
interface Vector {
name: string;
message: string;
public_key: string;
signature: string;
valid: boolean;
stdlib: boolean;
}
describe('verifyStrict', () => {
const file = readJSON<{ vectors: Vector[] }>('vectors/ed25519_strict.json');
it('gives the answer of the reference on every vector of ed25519_strict.json', () => {
expect(file.vectors.length).toBeGreaterThanOrEqual(18);
for (const v of file.vectors) {
expect(verifyStrict(h(v.public_key), h(v.message), h(v.signature)), v.name).toBe(v.valid);
}
});
it('refuses what a looser verifier accepts: the cases where the standard library and the profile differ are in the file', () => {
const loose = file.vectors.filter((v) => v.stdlib && !v.valid);
expect(loose.length).toBeGreaterThan(0);
for (const v of loose) expect(verifyStrict(h(v.public_key), h(v.message), h(v.signature)), v.name).toBe(false);
});
it('refuses a key or a signature of another length', () => {
const v = file.vectors[0]!;
expect(verifyStrict(h(v.public_key).subarray(1), h(v.message), h(v.signature))).toBe(false);
expect(verifyStrict(h(v.public_key), h(v.message), h(v.signature).subarray(1))).toBe(false);
expect(verifyStrict(h(v.public_key), h(v.message), h(v.signature))).toBe(true);
// A bit of the message, of R and of S.
const bad = (i: number): Uint8Array => {
const s = h(v.signature);
s[i] = s[i]! ^ 1;
return s;
};
expect(verifyStrict(h(v.public_key), Uint8Array.of(1), h(v.signature))).toBe(false);
expect(verifyStrict(h(v.public_key), h(v.message), bad(0))).toBe(false);
expect(verifyStrict(h(v.public_key), h(v.message), bad(40))).toBe(false);
});
});

@ -0,0 +1,63 @@
// The strict verification of the author signature of alg 1 (spec v0.11,
// §29.9), as the Go package internal/ed25519strict does it. A signature is
// valid if and only if all four conditions hold:
//
// 1. A is the canonical encoding of a point: its y is less than p, and if y
// is 1 or p - 1 the sign bit is 0;
// 2. A decodes to a point that is not of small order;
// 3. sig[63] & 0xE0 is 0, and S, as a little-endian integer, is less than
// the order of the group, l;
// 4. [S]B - [k]A encodes exactly R, with k = SHA-512(R || A || message) mod
// l: the equation of RFC 8032 without the cofactor.
//
// @noble/curves 2.4.0 `verify` always uses the equation with the cofactor and
// accepts what condition 1 and the check of the encoding of R refuse, so the
// checks are made here and noble only does the arithmetic of the group. The
// vectors are testdata/vectors/ed25519_strict.json. Internal: index.ts does
// not re-export it.
import { ed25519 } from '@noble/curves/ed25519.js';
import { sha512 } from '@noble/hashes/sha2.js';
import { concatBytes, equalBytes } from './bytes.ts';
const P = 2n ** 255n - 19n;
const L = 2n ** 252n + 27742317777372353535851937790883648493n;
const MASK_255 = (1n << 255n) - 1n;
function leToBigint(b: Uint8Array): bigint {
let n = 0n;
for (let i = b.length - 1; i >= 0; i--) n = (n << 8n) | BigInt(b[i]!);
return n;
}
/** Whether `sig` is a valid Ed25519 signature of `message` by the key `publicKey` under the strict profile of spec §29.9. */
export function verifyStrict(publicKey: Uint8Array, message: Uint8Array, sig: Uint8Array): boolean {
if (publicKey.length !== 32 || sig.length !== 64) return false;
// 1. A is canonical.
const y = leToBigint(publicKey) & MASK_255;
const sign = publicKey[31]! >> 7;
if (y >= P) return false;
if ((y === 1n || y === P - 1n) && sign !== 0) return false;
// 2. A decodes, and is not of small order.
let a;
try {
a = ed25519.Point.fromBytes(publicKey, false);
} catch {
return false;
}
if (a.isSmallOrder()) return false;
// 3. S is canonical.
if ((sig[63]! & 0xe0) !== 0) return false;
const s = leToBigint(sig.subarray(32));
if (s >= L) return false;
// 4. [S]B - [k]A encodes exactly R.
const r = sig.subarray(0, 32);
const k = leToBigint(sha512(concatBytes(r, publicKey, message))) % L;
const sb = s === 0n ? ed25519.Point.ZERO : ed25519.Point.BASE.multiplyUnsafe(s);
const ka = k === 0n ? ed25519.Point.ZERO : a.multiplyUnsafe(k);
return equalBytes(sb.subtract(ka).toBytes(), r);
}

@ -9,6 +9,7 @@ import { decodeAccessKey, encodeAccessKey } from './accesskey.ts';
import { ACCESS_SLOTS } from './age.ts'; import { ACCESS_SLOTS } from './age.ts';
import { bodyFrameBytes, checkArea, contentLength, parseBodyFrame } from './body.ts'; import { bodyFrameBytes, checkArea, contentLength, parseBodyFrame } from './body.ts';
import { sha256 } from './bytes.ts'; import { sha256 } from './bytes.ts';
import { ALG_CMS, authorCode, authorMessage, controlCommit, headDigest, signersDigest } from './author.ts';
import { decodeControl } from './control.ts'; import { decodeControl } from './control.ts';
import type { Extension } from './extension.ts'; import type { Extension } from './extension.ts';
import { type Format, FORMAT_1, FORMAT_2, FORMAT_3, headerBinding, isPadded, parsePrelude, payloadOffset } from './framing.ts'; import { type Format, FORMAT_1, FORMAT_2, FORMAT_3, headerBinding, isPadded, parsePrelude, payloadOffset } from './framing.ts';
@ -16,7 +17,7 @@ import { checkHeadEnd, decodeHead, encodeHead } from './head.ts';
import { inspect, inspectView } from './inspect.ts'; import { inspect, inspectView } from './inspect.ts';
import { paddedLength, type Padding, payloadAgeLength } from './padding.ts'; import { paddedLength, type Padding, payloadAgeLength } from './padding.ts';
import { evaluateSecurity, type Verdict, verdictLines } from './security.ts'; import { evaluateSecurity, type Verdict, verdictLines } from './security.ts';
import { isPending, ported } from './testing/pending.ts'; import { isPending, kinds, ported } from './testing/pending.ts';
import { h, hx, listTestdata, readBytes, readJSON } from './testing/testdata.ts'; import { h, hx, listTestdata, readBytes, readJSON } from './testing/testdata.ts';
interface FixtureExt { interface FixtureExt {
@ -66,6 +67,16 @@ interface DkcFixture {
content_offset?: number; content_offset?: number;
files?: { path: string; size: number; start: number; end: number; sha256: string; mtime?: number }[]; files?: { path: string; size: number; start: number; end: number; sha256: string; mtime?: number }[];
verdicts?: { signature: Verdict; seal: Verdict; lines: string[] }; verdicts?: { signature: Verdict; seal: Verdict; lines: string[] };
/** The record of a signature (spec v0.11, §29.8): the commitments and the message that the reference computed. */
signature?: {
alg: number;
signers?: string;
control_commit: string;
head_digest: string;
signers_digest: string;
author_message: string;
author_code: string;
};
} }
interface DkkFixture { interface DkkFixture {
@ -207,11 +218,27 @@ describe.each(dkcFixtures)('$json.file', ({ json: fx }) => {
expect(hx(await sha256(bytes)), f.path).toBe(hx(f.sha256)); expect(hx(await sha256(bytes)), f.path).toBe(hx(f.sha256));
} }
expect(hx(encodeHead(decoded))).toBe(fx.head_cbor); expect(hx(encodeHead(decoded))).toBe(fx.head_cbor);
const verdicts = evaluateSecurity(security); // The signature is checked in the context of the capsule: its control, which the record holds, and its head.
// Where the reference checks a signature or a seal, this library gives what const control = decodeControl(h(fx.control_cbor), FORMAT_3);
const verdicts = evaluateSecurity(security, { controlCommit: controlCommit(control, FORMAT_3), headDigest: headDigest(head) });
if (fx.signature !== undefined) {
// What is signed, recomputed from the control and the head of the fixture as the reference recomputes it.
const cc = controlCommit(control, FORMAT_3);
const hd = headDigest(head);
const sd = signersDigest(fx.signature.alg, fx.signature.alg === ALG_CMS ? h(fx.signature.signers!) : undefined);
const message = authorMessage(cc, hd, sd);
expect({ cc: hx(cc), hd: hx(hd), sd: hx(sd), message: new TextDecoder().decode(message), code: authorCode(message) }).toEqual({
cc: fx.signature.control_commit,
hd: fx.signature.head_digest,
sd: fx.signature.signers_digest,
message: fx.signature.author_message,
code: fx.signature.author_code,
});
}
// Where the reference checks a signature of alg 2 or a seal, this library gives what
// a reader of v0.10 gives, until it ports the verification (testing/pending.ts). // a reader of v0.10 gives, until it ports the verification (testing/pending.ts).
const want = ported(fx.verdicts!); const want = ported(fx.verdicts!);
expect(verdicts).toEqual(want); expect(kinds(verdicts)).toEqual(want);
if (!isPending(fx.verdicts!)) expect(verdictLines(verdicts)).toEqual(fx.verdicts!.lines); if (!isPending(fx.verdicts!)) expect(verdictLines(verdicts)).toEqual(fx.verdicts!.lines);
}); });
}); });

@ -26,7 +26,7 @@ import { type Release, type ReleaseSource, suppliedRelease } from './release.ts'
import { type Verdict, verdictLines } from './security.ts'; import { type Verdict, verdictLines } from './security.ts';
import { MemorySink, type Sink } from './sink.ts'; import { MemorySink, type Sink } from './sink.ts';
import { frame, split } from './testing/capsule.ts'; import { frame, split } from './testing/capsule.ts';
import { isPending, ported } from './testing/pending.ts'; import { isPending, kinds, ported } from './testing/pending.ts';
import { h, hx, listTestdata, readBytes, readJSON } from './testing/testdata.ts'; import { h, hx, listTestdata, readBytes, readJSON } from './testing/testdata.ts';
import { applyEdits, edits } from './testing/vectors.ts'; import { applyEdits, edits } from './testing/vectors.ts';
import { parseX25519Identity, unwrapX25519, x25519PublicKey } from './x25519.ts'; import { parseX25519Identity, unwrapX25519, x25519PublicKey } from './x25519.ts';
@ -102,7 +102,7 @@ function expectFiles(f: Fixture, r: Opened, sink: MemorySink): void {
f.name, f.name,
).toEqual(f.side.files ?? []); ).toEqual(f.side.files ?? []);
expect(opened.files.map(hx), f.name).toEqual(r.head!.files.map((x) => hx(f.plaintext.subarray(f.side.content_offset! + x.start, f.side.content_offset! + x.end)))); expect(opened.files.map(hx), f.name).toEqual(r.head!.files.map((x) => hx(f.plaintext.subarray(f.side.content_offset! + x.start, f.side.content_offset! + x.end))));
expect(r.verdicts, f.name).toEqual(ported(f.side.verdicts!)); expect(kinds(r.verdicts!), f.name).toEqual(ported(f.side.verdicts!));
if (!isPending(f.side.verdicts!)) expect(verdictLines(r.verdicts!), f.name).toEqual(f.side.verdicts!.lines); if (!isPending(f.side.verdicts!)) expect(verdictLines(r.verdicts!), f.name).toEqual(f.side.verdicts!.lines);
expect(r.areaLen, f.name).toBe(f.side.area_len); expect(r.areaLen, f.name).toBe(f.side.area_len);
expect([r.plaintext, r.unusableHeadExtensions], f.name).toEqual([undefined, []]); expect([r.plaintext, r.unusableHeadExtensions], f.name).toEqual([undefined, []]);
@ -146,6 +146,18 @@ function reseal(sealed: Uint8Array, key: Uint8Array, plaintext: Uint8Array): Uin
} }
describe('open', () => { describe('open', () => {
// Spec v0.11 §29.7: a capsule signed with alg 1 is F4 with its key, and F3 with the label of that key when the person saved it.
it('gives F4 to a capsule signed with an author key, and F3 with the key the person saved', async () => {
const f = fixture('format3_signed');
const key = (f.side as unknown as { signature: { author_key: string } }).signature.author_key;
const plain = await open(f.dkc, options(f, { sink: new MemorySink() }));
expect([plain.error, plain.verdicts?.signature, plain.verdicts?.seal]).toEqual([undefined, 'F4', 'S0']);
expect(verdictLines(plain.verdicts!)).toEqual(f.side.verdicts!.lines);
const saved = await open(f.dkc, options(f, { sink: new MemorySink(), authorKeys: new Map([[key, 'Ana']]) }));
expect([saved.error, saved.verdicts?.signature, saved.verdicts?.authorLabel]).toEqual([undefined, 'F3', 'Ana']);
expect(verdictLines(saved.verdicts!)).toEqual(['Firmado con la clave que guardaste como Ana.']);
});
it('opens every official fixture to its content, with the checks of the reference', async () => { it('opens every official fixture to its content, with the checks of the reference', async () => {
expect(NAMES.length).toBeGreaterThanOrEqual(21); expect(NAMES.length).toBeGreaterThanOrEqual(21);
for (const name of NAMES) { for (const name of NAMES) {

@ -34,6 +34,7 @@ import { type Identity, type Stanza as AgeStanza } from 'age-encryption';
import { type AccessKey, checkAccessKeyMaterial, decodeAccessKey, wipeAccessKey } from './accesskey.ts'; import { type AccessKey, checkAccessKeyMaterial, decodeAccessKey, wipeAccessKey } from './accesskey.ts';
import { ACCESS_SLOTS, ageStanzas, checkAccessStanzas, checkPayloadStanzas, checkTimeStanzas, type Stanza } from './age.ts'; import { ACCESS_SLOTS, ageStanzas, checkAccessStanzas, checkPayloadStanzas, checkTimeStanzas, type Stanza } from './age.ts';
import { chunked, classify, decrypt, decryptAll, STREAM_FAILURE } from './agefile.ts'; import { chunked, classify, decrypt, decryptAll, STREAM_FAILURE } from './agefile.ts';
import { controlCommit } from './author.ts';
import { equalBytes, sha256, toHex } from './bytes.ts'; import { equalBytes, sha256, toHex } from './bytes.ts';
import { type Control, decodeControl } from './control.ts'; import { type Control, decodeControl } from './control.ts';
import { compareInstants, formatRFC3339, type Instant } from './datekey.ts'; import { compareInstants, formatRFC3339, type Instant } from './datekey.ts';
@ -56,6 +57,12 @@ import { MalformedX25519Stanza, unwrapX25519 } from './x25519.ts';
/** Options of open. */ /** Options of open. */
export interface OpenOptions { export interface OpenOptions {
/**
* The author keys that the person saved, by their dkauthor1… string, with
* the label she gave each: a valid signature of alg 1 with one of them is F3
* and not F4 (spec v0.11, §29.7).
*/
readonly authorKeys?: ReadonlyMap<string, string>;
/** The locally pinned profiles; the default registry (Quicknet) when omitted. */ /** The locally pinned profiles; the default registry (Quicknet) when omitted. */
readonly registry?: ProfileRegistry; readonly registry?: ProfileRegistry;
/** The extensions the application implements; see InspectOptions. */ /** The extensions the application implements; see InspectOptions. */
@ -411,7 +418,10 @@ async function openCapsule(
try { try {
const plain = await decrypt(payload, payloadIdentity(control.payloadIdentity), 'PAYLOAD_AGE'); const plain = await decrypt(payload, payloadIdentity(control.payloadIdentity), 'PAYLOAD_AGE');
if (format === FORMAT_3) { if (format === FORMAT_3) {
body = await openBody(plain, padded!.l, padded!.p, opts.sink!, opts.extensions); body = await openBody(plain, padded!.l, padded!.p, opts.sink!, opts.extensions, {
controlCommit: controlCommit(control, format),
...(opts.authorKeys === undefined ? {} : { authorKeys: opts.authorKeys }),
});
} else if (opts.output === undefined) { } else if (opts.output === undefined) {
// The content is never longer than PAYLOAD_AGE. // The content is never longer than PAYLOAD_AGE.
const buf = new Uint8Array(padded === undefined ? ciphertextLength : Math.min(padded.l, ciphertextLength)); const buf = new Uint8Array(padded === undefined ? ciphertextLength : Math.min(padded.l, ciphertextLength));

@ -27,6 +27,7 @@ import { sha256Hasher } from './digest.ts';
import { DateKeysError } from './errors.ts'; import { DateKeysError } from './errors.ts';
import { checkNoncritical, type ExtensionRegistry, type Unusable } from './extension.ts'; import { checkNoncritical, type ExtensionRegistry, type Unusable } from './extension.ts';
import { checkHeadEnd, decodeHead, type Head } from './head.ts'; import { checkHeadEnd, decodeHead, type Head } from './head.ts';
import { headDigest } from './author.ts';
import { evaluateSecurity, type Verdicts } from './security.ts'; import { evaluateSecurity, type Verdicts } from './security.ts';
import type { Sink } from './sink.ts'; import type { Sink } from './sink.ts';
@ -179,6 +180,7 @@ export async function openBody(
p: number, p: number,
sink: Sink, sink: Sink,
reg: ExtensionRegistry | undefined, reg: ExtensionRegistry | undefined,
security: { readonly controlCommit: Uint8Array; readonly authorKeys?: ReadonlyMap<string, string> },
): Promise<OpenedBody> { ): Promise<OpenedBody> {
const r = new Plaintext(plain, p); const r = new Plaintext(plain, p);
let begun = false; let begun = false;
@ -189,12 +191,18 @@ export async function openBody(
checkArea(area, frame.securityLen); checkArea(area, frame.securityLen);
// 17.3 and 17.6: the security area never fails; its verdicts are shown // 17.3 and 17.6: the security area never fails; its verdicts are shown
// after step 18 only. // after step 18 only. They need the digest of the head (spec §29.8), so
const verdicts = evaluateSecurity(area.subarray(0, frame.securityLen)); // they are evaluated once its bytes are read.
const securityBytes = area.subarray(0, frame.securityLen);
// 17.4: the head. Its codes other than ERR_INTEGRITY are reported only // 17.4: the head. Its codes other than ERR_INTEGRITY are reported only
// after reading to the end. // after reading to the end.
const hb = await r.readN(frame.headLen); const hb = await r.readN(frame.headLen);
const verdicts = evaluateSecurity(securityBytes, {
controlCommit: security.controlCommit,
headDigest: headDigest(hb),
...(security.authorKeys === undefined ? {} : { authorKeys: security.authorKeys }),
});
let head: Head; let head: Head;
try { try {
head = decodeHead(hb, reg); head = decodeHead(hb, reg);

@ -7,8 +7,11 @@
// security area never decides the opening, and its verdicts carry no error // security area never decides the opening, and its verdicts carry no error
// code. Internal: index.ts does not re-export it. // code. Internal: index.ts does not re-export it.
import { ALG_ED25519, authorMessage, signersDigest } from './author.ts';
import { bech32Encode } from './bech32.ts';
import { utf8Length } from './bytes.ts'; 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 { DateKeysError } from './errors.ts'; import { DateKeysError } from './errors.ts';
import { fieldOf, requireKeys } from './schema.ts'; import { fieldOf, requireKeys } from './schema.ts';
@ -36,6 +39,23 @@ export type Verdict = 'X' | 'F0' | 'F1' | 'F2' | 'F3' | 'F4' | 'F5' | 'F6' | 'S0
export interface Verdicts { export interface Verdicts {
readonly signature: Verdict; readonly signature: Verdict;
readonly seal: Verdict; readonly seal: Verdict;
/** The public key of a valid signature of alg 1 (F3, F4). */
readonly authorKey?: Uint8Array;
/** The label of the saved key that signed (F3). */
readonly authorLabel?: string;
}
/**
* What the verdicts of a signature need besides SECURITY_CBOR (spec v0.11,
* §29.7): the commitments of the capsule, and the author keys that the person
* saved, by their dkauthor1… string, with the label she gave each (F3). A
* reader builds it at step 17.6. Without it the area is read as v0.10 reads
* it, and any signature is F1.
*/
export interface SecurityContext {
readonly controlCommit: Uint8Array;
readonly headDigest: Uint8Array;
readonly authorKeys?: ReadonlyMap<string, string>;
} }
/** The text of a verdict that the official SDK shows, in Spanish (spec §29.7), and '' for S0, which shows nothing. */ /** The text of a verdict that the official SDK shows, in Spanish (spec §29.7), and '' for S0, which shows nothing. */
@ -72,8 +92,12 @@ 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 verdicts as the official SDK shows them, in order: X alone, or the signature and then the seal, when it shows something. */
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);
// F3 and F4 name a key (spec §29.7).
if (v.signature === 'F3') signature = `Firmado con la clave que guardaste como ${v.authorLabel!}.`;
if (v.signature === 'F4') signature = `Firmado con la clave ${bech32Encode('dkauthor', v.authorKey!)}. No prueba quién la tiene.`;
const seal = verdictText(v.seal); const seal = verdictText(v.seal);
return seal === '' ? [verdictText(v.signature)] : [verdictText(v.signature), seal]; return seal === '' ? [signature] : [signature, seal];
} }
// The outer map of SECURITY_CBOR: keys 2 and 3, undefined when absent. // The outer map of SECURITY_CBOR: keys 2 and 3, undefined when absent.
@ -135,6 +159,51 @@ export function encodeSecurity(): Uint8Array {
return e.out(); return e.out();
} }
// The content of key 2: {0: alg, 1: key, 2: signature}.
interface AuthorSignature {
alg: number;
key: Uint8Array;
value: Uint8Array;
}
function decodeAuthorSignature(d: Decoder): AuthorSignature {
const a: AuthorSignature = { alg: 0, key: new Uint8Array(0), value: new Uint8Array(0) };
const pairs = d.map(3);
const seen = new Set<number>();
for (let i = 0; i < pairs; i++) {
const k = d.key();
fieldOf(k)(() => {
switch (k) {
case 0:
a.alg = decodeAlg(d);
break;
case 1:
a.key = d.bstr(0, MAX_SECURITY_ITEM);
break;
case 2:
a.value = d.bstr(0, MAX_SECURITY_ITEM);
break;
default:
throw new DateKeysError('ERR_NON_CANONICAL_CBOR', `key ${k} is not defined`);
}
});
seen.add(Number(k));
}
requireKeys(seen, 3);
d.endMap();
return a;
}
function encodeAuthorSignature(e: Encoder, a: AuthorSignature): void {
e.map(3);
e.uint(0);
e.uint(a.alg);
e.uint(1);
e.bstr(a.key);
e.uint(2);
e.bstr(a.value);
}
// The content of key 3: {0: seal_type, 1: token}. // The content of key 3: {0: seal_type, 1: token}.
interface Seal { interface Seal {
sealType: number; sealType: number;
@ -201,7 +270,7 @@ function attempt<T>(decode: () => T): T | undefined {
* decides: alg and seal_type are read only from content that decodes and * decides: alg and seal_type are read only from content that decodes and
* meets its schema. * meets its schema.
*/ */
export function evaluateSecurity(b: Uint8Array): Verdicts { export function evaluateSecurity(b: Uint8Array, context?: SecurityContext): Verdicts {
const w = attempt(() => { const w = attempt(() => {
const h = peek(b); const h = peek(b);
return h.typeTag === SECURITY_TYPE_TAG && h.version === SECURITY_VERSION ? unmarshal(b, decodeWire, encodeWire) : undefined; return h.typeTag === SECURITY_TYPE_TAG && h.version === SECURITY_VERSION ? unmarshal(b, decodeWire, encodeWire) : undefined;
@ -209,9 +278,23 @@ export function evaluateSecurity(b: Uint8Array): Verdicts {
if (w === undefined) return { signature: 'X', seal: 'X' }; if (w === undefined) return { signature: 'X', seal: 'X' };
const { signature, seal } = w; const { signature, seal } = w;
return { return {
// A content that does not decode and an alg this version does not ...(signature === undefined ? { signature: 'F0' as const } : evaluateSignature(signature, context)),
// implement give the same verdict, and this version implements none.
signature: signature === undefined ? 'F0' : 'F1',
seal: seal === undefined ? 'S0' : attempt(() => unmarshal(seal, decodeSeal, encodeSeal)) === undefined ? 'S2' : 'S1', seal: seal === undefined ? 'S0' : attempt(() => unmarshal(seal, decodeSeal, encodeSeal)) === undefined ? 'S2' : 'S1',
}; };
} }
// The verdict of the content of key 2 (spec §29.7, §29.9): F1 for content
// that does not decode, an alg this reader does not implement, or a key or a
// signature of another length, and without the context of a capsule, as in
// v0.10; F2 when the signature does not verify with the strict profile; F3
// or F4 when it does. This version implements alg 1 only.
function evaluateSignature(content: Uint8Array, context: SecurityContext | undefined): Pick<Verdicts, 'signature' | 'authorKey' | 'authorLabel'> {
const unchecked = { signature: 'F1' } as const;
if (context === undefined) return unchecked;
const a = attempt(() => unmarshal(content, decodeAuthorSignature, encodeAuthorSignature));
if (a === undefined || a.alg !== ALG_ED25519 || a.key.length !== 32 || a.value.length !== 64) return unchecked;
const message = authorMessage(context.controlCommit, context.headDigest, signersDigest(ALG_ED25519));
if (!verifyStrict(a.key, message, a.value)) return { signature: 'F2' };
const label = context.authorKeys?.get(bech32Encode('dkauthor', a.key));
return label === undefined ? { signature: 'F4', authorKey: a.key } : { signature: 'F3', authorKey: a.key, authorLabel: label };
}

@ -1,6 +1,7 @@
// What this library does not do yet of spec v0.11: it reads the security area // What this library does not do yet of spec v0.11: it checks the signature of
// of a format 3 capsule as a reader of v0.10 does, so a signature or a seal that // alg 1 (F2 to F4) but not that of alg 2 with certificates (F5, F6) nor the
// the reference checks gives F1 or S1 here. The shared fixtures and vectors // time seal (S3 to S5), which it reads as a reader of v0.10 does: they give F1
// or S1 here. The shared fixtures and vectors
// already record the verdicts of the reference (F2 to F6, S3 to S5), and these // already record the verdicts of the reference (F2 to F6, S3 to S5), and these
// helpers say what this library gives meanwhile, so that the tests state the // helpers say what this library gives meanwhile, so that the tests state the
// gap instead of hiding it. Porting the verification (spec §29.8 to §29.11) // gap instead of hiding it. Porting the verification (spec §29.8 to §29.11)
@ -9,7 +10,7 @@
import type { Verdict } from '../security'; import type { Verdict } from '../security';
/** Verdicts that a reader of v0.10 cannot reach: those of a signature or a seal that is checked. */ /** Verdicts that a reader of v0.10 cannot reach: those of a signature or a seal that is checked. */
const SIGNATURE_CHECKED: readonly Verdict[] = ['F2', 'F3', 'F4', 'F5', 'F6']; const SIGNATURE_CHECKED: readonly Verdict[] = ['F5', 'F6'];
const SEAL_CHECKED: readonly Verdict[] = ['S3', 'S4', 'S5']; const SEAL_CHECKED: readonly Verdict[] = ['S3', 'S4', 'S5'];
export interface Recorded { export interface Recorded {
@ -17,6 +18,11 @@ export interface Recorded {
readonly seal: Verdict; readonly seal: Verdict;
} }
/** The two verdicts of `v`, without the key or the label that a signature of alg 1 adds. */
export function kinds(v: Recorded): Recorded {
return { signature: v.signature, seal: v.seal };
}
/** The verdicts that this library gives where the reference records `recorded`. */ /** The verdicts that this library gives where the reference records `recorded`. */
export function ported(recorded: Recorded): Recorded { export function ported(recorded: Recorded): Recorded {
return { return {

@ -61,7 +61,7 @@ import {
import type { Release, ReleaseSource } from './release.ts'; import type { Release, ReleaseSource } from './release.ts';
import { evaluateSecurity, type Verdict, verdictLines } from './security.ts'; import { evaluateSecurity, type Verdict, verdictLines } from './security.ts';
import { MemorySink } from './sink.ts'; import { MemorySink } from './sink.ts';
import { isPending, ported } from './testing/pending.ts'; import { isPending, kinds, ported } from './testing/pending.ts';
import { hasTestdata, hx, listTestdata, readBytes, readJSON } from './testing/testdata.ts'; import { hasTestdata, hx, listTestdata, readBytes, readJSON } from './testing/testdata.ts';
import { import {
applyEdits, applyEdits,
@ -681,7 +681,7 @@ describe('vectors/mutations.json', () => {
expect(r.error?.message ?? 'ok', 'the text of capsule.Open').toBe(TEXTS.cases[c.index]!.text); expect(r.error?.message ?? 'ok', 'the text of capsule.Open').toBe(TEXTS.cases[c.index]!.text);
if (c.error === 'ok') { if (c.error === 'ok') {
expect({ step: last.step, ok: last.ok, error: r.error }).toEqual({ step: 18, ok: true, error: undefined }); expect({ step: last.step, ok: last.ok, error: r.error }).toEqual({ step: 18, ok: true, error: undefined });
expect(r.verdicts).toEqual(ported(c.verdicts!)); expect(kinds(r.verdicts!)).toEqual(ported(c.verdicts!));
if (!isPending(c.verdicts!)) expect(verdictLines(r.verdicts!)).toEqual(c.verdicts!.lines); if (!isPending(c.verdicts!)) expect(verdictLines(r.verdicts!)).toEqual(c.verdicts!.lines);
expect(sink.opened?.head).toBe(r.head); expect(sink.opened?.head).toBe(r.head);
} else { } else {

Loading…
Cancel
Save

Powered by TurnKey Linux.