Go opens what the writer of v0.11 writes: capsule-vectors.json again

Fixes T10 of the review of the session of 1 and 2 October: no test checked
that Go opens what encryptFiles writes today, since capsule-vectors.json
came from c3c124a, with an area of 512 bytes and no note.

- capsule-ts-samples.mjs writes format 2 with encryptVectors of
  testing/encrypt.ts, as a generator of test vectors, and format 3 with
  encryptFiles, as any caller writes it: the six samples of before, now
  with the area of 32 KiB, and two more with a public note, one out of
  ASCII, and one of 1024 bytes in time_and_key beside another noncritical
  extension of the header.
- capsule-go-verdicts.go records the size of the area that capsule.Open
  gives, the note that Header.PublicNote reads, the text of capsule.Encrypt
  for a public note in format 2, and the text of capsule.EncryptFiles for
  nine public notes that break their rules.
- interop.test.ts requires Go to open each of the 21 samples with each
  credential, to find the area of 32 KiB in format 3 and to read the note
  written, and this library to read the same note; and the texts of the 22
  options and of the 9 notes to be those of Go.

Go at spec-v0.11 (ae33434) opens the 21 samples with each credential and
with all of them, encodes their objects again byte for byte, and gives the
texts of this library for the 22 options and the 9 notes; the four mixes,
the 500 inputs of the encoder differential and the 22 recipients give what
they gave. npm run verify passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main
dev 6 days ago
parent 4377a87f7f
commit 5b33a765fb

@ -9,11 +9,13 @@
// with each credential alone and with all of them, and the verdict, the
// format, L, the padding rule and P are recorded; in format 2 the SHA-256
// of the content, and in format 3 the files that a Sink receives, with
// their SHA-256, the head encoded again with capsule.EncodeHead and the
// verdicts of the security area. Its PUBLIC_HEADER, its CONTROL_CBOR,
// opened layer by layer with the identities of agewrap, and its .dkk are
// encoded again by the reference and compared with the bytes written,
// and the number of stanzas of its INNER_ACCESS_AGE is recorded.
// their SHA-256, the head encoded again with capsule.EncodeHead, the
// verdicts of the security area and the size of the area. Its
// PUBLIC_HEADER, its CONTROL_CBOR, opened layer by layer with the
// identities of agewrap, and its .dkk are encoded again by the reference
// and compared with the bytes written, the public note that
// Header.PublicNote reads in its PUBLIC_HEADER is recorded, null without
// one, and so is the number of stanzas of its INNER_ACCESS_AGE.
// - mixes: the code and the step at which capsule.Open fails.
// - encoders: capsule.EncodeHeader, capsule.EncodeControl (format 2) and
// AccessKey.MarshalBody on every input, compared with the bytes of the
@ -23,6 +25,8 @@
// - errors: the text of capsule.Encrypt for each invalid option, as a
// generator of test vectors, the only writer of format 2 (spec §62.1
// rule 1).
// - note_errors: the text of capsule.EncryptFiles for each public note that
// breaks the rules of spec §24.1.
//
// Run it from a scratch module that requires the reference implementation
// (replace g.activething.com/go/DateKeys => ../datekeys-go, GOFLAGS=-mod=mod
@ -89,6 +93,8 @@ type sampleIn struct {
Identities []string `json:"identities"`
DKK string `json:"dkk,omitempty"`
Known []extJSON `json:"known"`
// PublicNote is the public note that the writer was given, if any.
PublicNote *string `json:"public_note,omitempty"`
DKC string `json:"dkc"`
}
@ -143,6 +149,16 @@ type errorCase struct {
ControlCritical []extJSON `json:"control_critical,omitempty"`
ControlNoncritical []extJSON `json:"control_noncritical,omitempty"`
ChainHashFlip bool `json:"chain_hash_flip,omitempty"`
PublicNote string `json:"public_note,omitempty"`
TS string `json:"ts"`
Go string `json:"go"`
}
// noteError is a public note that breaks the rules of spec §24.1, and the
// texts of capsule.EncryptFiles and of the TypeScript library for it.
type noteError struct {
Name string `json:"name"`
Note string `json:"note"`
TS string `json:"ts"`
Go string `json:"go"`
}
@ -163,6 +179,7 @@ type input struct {
} `json:"encoders"`
Recipients []string `json:"recipients"`
Errors []errorCase `json:"errors"`
NoteErrors []noteError `json:"note_errors"`
}
// The verdict of one opening: "ok" with what was delivered, or the code and
@ -183,6 +200,7 @@ type openedOut struct {
Files []fileJSON `json:"files,omitempty"`
HeadCBOR string `json:"head_cbor,omitempty"`
Verdicts []string `json:"verdicts,omitempty"`
AreaLen uint32 `json:"area_len,omitempty"`
}
// sink keeps the files of a format 3 capsule in memory.
@ -210,6 +228,7 @@ type sampleOut struct {
Opens []openVerdict `json:"opens"`
InnerStanzas int `json:"inner_stanzas"`
Reencoded bool `json:"reencoded"`
PublicNote *string `json:"public_note"`
} `json:"go"`
}
@ -240,6 +259,7 @@ type output struct {
Encoders encodersOut `json:"encoders"`
Recipients []recipientOut `json:"recipients"`
Errors []errorCase `json:"errors"`
NoteErrors []noteError `json:"note_errors"`
}
type releaseOut struct {
@ -323,6 +343,7 @@ func opened(dkc []byte, round uint64, reg profile.Registry, ex extension.Registr
}
v.Opened.HeadCBOR = hex.EncodeToString(must(capsule.EncodeHead(res.Head)))
v.Opened.Verdicts = []string{string(res.Verdicts.Signature), string(res.Verdicts.Seal)}
v.Opened.AreaLen = res.AreaLen
return v
}
@ -374,7 +395,7 @@ func main() {
v.Credentials = "none"
}
so.Go.Opens = append(so.Go.Opens, v)
so.Go.InnerStanzas, so.Go.Reencoded = layers(dkc, s, p, dkk)
so.Go.InnerStanzas, so.Go.Reencoded, so.Go.PublicNote = layers(dkc, s, p, dkk)
out.Samples = append(out.Samples, so)
}
@ -412,6 +433,11 @@ func main() {
out.Errors = append(out.Errors, c)
}
for _, c := range in.NoteErrors {
c.Go = noteText(c.Note)
out.NoteErrors = append(out.NoteErrors, c)
}
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
must(0, enc.Encode(out))
@ -420,14 +446,19 @@ func main() {
// layers opens SEALED_CONTROL of a sample with the published release and, in
// time_and_key, with its first credential, and encodes PUBLIC_HEADER,
// CONTROL_CBOR and the .dkk again. It returns the number of stanzas of
// INNER_ACCESS_AGE and whether every encoding equals the bytes written.
func layers(dkc []byte, s sampleIn, p *profile.Profile, dkk []byte) (int, bool) {
// INNER_ACCESS_AGE, whether every encoding equals the bytes written, and the
// public note of PUBLIC_HEADER, nil when there is none that is usable.
func layers(dkc []byte, s sampleIn, p *profile.Profile, dkk []byte) (int, bool, *string) {
pre := must(capsule.ParsePrelude(dkc))
header := dkc[capsule.PreludeSize : capsule.PreludeSize+int(pre.PublicHeaderLen)]
sealed := dkc[capsule.PreludeSize+int(pre.PublicHeaderLen) : capsule.PreludeSize+int(pre.PublicHeaderLen)+int(pre.SealedControlLen)]
same := true
h := must(capsule.DecodeHeader(header))
same = same && bytes.Equal(must(capsule.EncodeHeader(h)), header)
var note *string
if text, ok := h.PublicNote(); ok {
note = &text
}
tid := must(agewrap.NewTimeIdentity(p, s.Round, provider.Release{Round: s.Round, Signature: unhex(published[s.Round])}))
inner := must(io.ReadAll(must(age.Decrypt(bytes.NewReader(sealed), tid))))
@ -454,7 +485,7 @@ func layers(dkc []byte, s sampleIn, p *profile.Profile, dkk []byte) (int, bool)
must(0, accesskey.Encode(&b, k))
same = same && bytes.Equal(b.Bytes(), dkk)
}
return stanzas, same
return stanzas, same, note
}
// encoders encodes one input with the reference and names the first encoding
@ -516,6 +547,7 @@ func encryptError(c errorCase) string {
Noncritical: exts(c.Noncritical),
ControlCritical: exts(c.ControlCritical),
ControlNoncritical: exts(c.ControlNoncritical),
PublicNote: c.PublicNote,
TestVectors: true,
Now: func() time.Time { return must(time.Parse(time.RFC3339Nano, c.Now)) },
}
@ -526,6 +558,25 @@ func encryptError(c errorCase) string {
return "ok"
}
// noteText runs capsule.EncryptFiles on a file of one byte, for round 1000
// with now at the genesis, with the public note given, and returns its error
// text, or "ok".
func noteText(note string) string {
genesis := time.Unix(1692803367, 0).UTC()
opts := capsule.EncryptOptions{
Profile: profile.Quicknet(),
UnlockAt: genesis.Add(999 * 3 * time.Second),
Policy: capsule.TimeOnly,
PublicNote: note,
Now: func() time.Time { return genesis },
}
src := []capsule.Source{{Path: "a.txt", Size: 1, Open: func() (io.ReadCloser, error) { return io.NopCloser(strings.NewReader("x")), nil }}}
if _, err := capsule.EncryptFiles(io.Discard, src, opts); err != nil {
return err.Error()
}
return "ok"
}
func libraries() string {
info, ok := debug.ReadBuildInfo()
if !ok {

@ -2,18 +2,23 @@
// Writes, as JSON, what scripts/capsule-go-verdicts.go checks with the Go
// reference (plan of phase 3, decision 11 and section 8, point 9):
//
// - samples: .dkc of format 2 written by encrypt, as a generator of test
// vectors, and of format 3 written by encryptFiles, for rounds 1000, 1001
// and 2000, with now at the genesis and fixed identities, each with the
// credentials that open it and the SHA-256 of its content, generated from
// its length, or in format 3 the head written and the SHA-256 of each file;
// - samples: .dkc of format 2 written by encryptVectors of
// src/lib/dkc/testing/encrypt.ts, as a generator of test vectors, the only
// writer of format 2, and of format 3 written by encryptFiles as any
// caller writes them, with the area of 32 KiB and, in two of them, a
// public note; for rounds 1000, 1001 and 2000, with now at the genesis and
// fixed identities, each with the credentials that open it and the
// SHA-256 of its content, generated from its length, or in format 3 the
// head written and the SHA-256 of each file;
// - mixes: capsules spliced from two capsules of one round;
// - encoders: every input of the encoder differential, drawn from a seed
// (src/lib/dkc/testing/interop.ts), with its encodings by this library;
// - recipients: strings for age.ParseX25519Recipient and
// agewrap.CheckX25519Recipient;
// - errors: the invalid options of encrypt that Go also rejects, with the
// text of this library.
// - errors: the invalid options of encrypt as a generator of test vectors
// that Go also rejects, with the text of this library;
// - note_errors: the public notes that encryptFiles refuses, with the text
// of this library.
//
// The capsules are random (age draws its file keys and shares), so the output
// is generated once and frozen with the verdicts of Go in
@ -26,12 +31,22 @@ import { encodeAccessKey, marshalAccessKeyBody } from '../src/lib/dkc/accesskey.
import { concatBytes, sha256, toHex } from '../src/lib/dkc/bytes.ts';
import { encodeControl } from '../src/lib/dkc/control.ts';
import { parseRFC3339 } from '../src/lib/dkc/datekey.ts';
import { encrypt, encryptFiles } from '../src/lib/dkc/encrypt.ts';
import { encryptFiles } from '../src/lib/dkc/encrypt.ts';
import { FORMAT_2 } from '../src/lib/dkc/framing.ts';
import { encodeHead } from '../src/lib/dkc/head.ts';
import { encodeHeader, TIME_AND_KEY, TIME_ONLY } from '../src/lib/dkc/header.ts';
import { quicknet } from '../src/lib/dkc/profile.ts';
import { encoderCaseJSON, encoderCases, errorCaseInput, errorCases, recipientStrings, sampleIdentity } from '../src/lib/dkc/testing/interop.ts';
import { encryptVectors } from '../src/lib/dkc/testing/encrypt.ts';
import {
encoderCaseJSON,
encoderCases,
errorCaseInput,
errorCases,
noteErrorCases,
noteErrorInput,
recipientStrings,
sampleIdentity,
} from '../src/lib/dkc/testing/interop.ts';
import { x25519PublicKey } from '../src/lib/dkc/x25519.ts';
const GENESIS = parseRFC3339('2023-08-23T15:09:27Z');
@ -50,11 +65,11 @@ function content(n, seed = 1) {
return b;
}
// Format 2, which only a generator of test vectors writes (spec §62.1 rule 1).
const base = (extra) => ({ profile: quicknet(), now: () => GENESIS, policy: TIME_ONLY, unlockAt: roundAt(1000), testVectors: true, ...extra });
const base = (extra) => ({ profile: quicknet(), now: () => GENESIS, policy: TIME_ONLY, unlockAt: roundAt(1000), ...extra });
// Format 2, which only a generator of test vectors writes (spec §62.1 rule 1).
async function sample(name, body, opts, identities = [], dkkExtensions = [], known = []) {
const res = await encrypt(body, opts);
const res = await encryptVectors(body, opts);
let dkk;
if (res.portableKey !== undefined) dkk = encodeAccessKey({ ...res.portableKey, noncritical: dkkExtensions });
return {
@ -122,8 +137,7 @@ samples.push(await sample('an instant one nanosecond after round 1000', content(
// mtimes in milliseconds.
async function filesSample(name, files, opts, identities = [], known = []) {
const sources = files.map(([path, bytes, mtime]) => ({ path, size: bytes.length, ...(mtime === undefined ? {} : { mtime }), open: () => new Blob([bytes]).stream() }));
const { testVectors, ...rest } = opts;
const res = await encryptFiles(sources, rest);
const res = await encryptFiles(sources, opts);
return {
name,
round: res.dateKey.round,
@ -137,6 +151,7 @@ async function filesSample(name, files, opts, identities = [], known = []) {
identities: identities.map(toHex),
...(res.portableKey === undefined ? {} : { dkk: toHex(encodeAccessKey(res.portableKey)) }),
known,
...(opts.publicNote === undefined ? {} : { public_note: opts.publicNote }),
dkc: toHex(res.dkc),
};
}
@ -179,6 +194,17 @@ samples.push(
[{ id: 'org.example.head-critical', version: 1 }],
),
);
// The public note of spec v0.11 §24.1, in PUBLIC_HEADER, which Go reads with
// Header.PublicNote: one out of ASCII, and one of the 1024 bytes of the most,
// beside a noncritical extension of the header, in time_and_key.
samples.push(await filesSample('format 3: a public note', [['carta.txt', content(500, 15)]], base({ publicNote: 'Cartas del viaje a Lisboa · 2026, para abrir en familia' })));
samples.push(
await filesSample(
'format 3: time_and_key, a portable key and a public note of 1024 bytes',
[['fotos/playa.jpg', content(3000, 16), 1_790_683_200_000]],
base({ policy: TIME_AND_KEY, unlockAt: roundAt(2000), newPortableKey: true, noncritical: [ext('org.example.header', 1, 'public data')], publicNote: 'ñ'.repeat(512) }),
),
);
// Mixes of two capsules of round 1000 (section 8, point 8).
const parts = (dkc) => {
@ -194,9 +220,9 @@ const frame = (prelude, header, sealed, payload) => {
v.setUint32(12, sealed.length);
return out;
};
const a = parts((await encrypt(new TextEncoder().encode('A'), base())).dkc);
const b = parts((await encrypt(new TextEncoder().encode('B'), base())).dkc);
const k = parts((await encrypt(new TextEncoder().encode('K'), base({ policy: TIME_AND_KEY, newPortableKey: true }))).dkc);
const a = parts((await encryptVectors(new TextEncoder().encode('A'), base())).dkc);
const b = parts((await encryptVectors(new TextEncoder().encode('B'), base())).dkc);
const k = parts((await encryptVectors(new TextEncoder().encode('K'), base({ policy: TIME_AND_KEY, newPortableKey: true }))).dkc);
const mixes = [
['the header of A with the rest of B', frame(a.prelude, a.header, b.sealed, b.payload)],
['the control of B inside A', frame(a.prelude, a.header, b.sealed, a.payload)],
@ -221,13 +247,26 @@ for (const c of errorCases()) {
const { src, opts } = errorCaseInput(c);
let text = 'ok';
try {
await encrypt(src, opts);
await encryptVectors(src, opts);
} catch (err) {
text = err.message;
}
errors.push({ ...c, ts: text });
}
// The public notes that encryptFiles refuses, with the text of this library.
const noteErrors = [];
for (const c of noteErrorCases()) {
const { files, opts } = noteErrorInput(c);
let text = 'ok';
try {
await encryptFiles(files, opts);
} catch (err) {
text = err.message;
}
noteErrors.push({ ...c, ts: text });
}
process.stdout.write(
`${JSON.stringify(
{
@ -237,6 +276,7 @@ process.stdout.write(
encoders: { seed: ENCODER_SEED, count: ENCODER_COUNT, sha256: toHex(await sha256(concatBytes(...encodings))), cases: encoders },
recipients: recipientStrings(),
errors,
note_errors: noteErrors,
},
null,
1,

@ -1,31 +1,37 @@
// Tests of the writers against the Go reference at the level of the capsule
// (plan of phase 3, decision 11 and section 8, point 9, and plan of format 3,
// step 4): src/lib/dkc/testing/capsule-vectors.json holds what
// scripts/capsule-ts-samples.mjs wrote with encrypt and encryptFiles, frozen
// with the verdicts of scripts/capsule-go-verdicts.go on it: capsule.Inspect
// and capsule.Open with each credential, into a Sink in format 3, the
// encoders of the reference, age.ParseX25519Recipient with
// agewrap.CheckX25519Recipient, and capsule.Encrypt on invalid options. This
// library must reach the same verdicts on the same bytes.
// scripts/capsule-ts-samples.mjs wrote with encryptVectors, as a generator of
// test vectors, and with encryptFiles, the writer of spec v0.11 with its area
// of 32 KiB and its public note, frozen with the verdicts of
// scripts/capsule-go-verdicts.go on it: capsule.Inspect and capsule.Open with
// each credential, into a Sink in format 3, Header.PublicNote, the encoders
// of the reference, age.ParseX25519Recipient with
// agewrap.CheckX25519Recipient, capsule.Encrypt on invalid options and
// capsule.EncryptFiles on public notes that break their rules. This library
// must reach the same verdicts on the same bytes.
import { readFileSync } from 'node:fs';
import { describe, expect, it } from 'vitest';
import { marshalAccessKeyBody } from './accesskey.ts';
import { AREA_LEN } from './body.ts';
import { concatBytes, fromHex, sha256, toHex } from './bytes.ts';
import { encodeControl } from './control.ts';
import type { Instant } from './datekey.ts';
import { encryptFiles } from './encrypt.ts';
import { errorCode } from './errors.ts';
import { ExtensionSet } from './extension.ts';
import { FORMAT_2, FORMAT_3 } from './framing.ts';
import { encodeHead } from './head.ts';
import { encodeHeader } from './header.ts';
import { inspect } from './inspect.ts';
import { publicNote } from './note.ts';
import { open, type OpenOptions } from './open.ts';
import { checkX25519Recipient, parseX25519Recipient } from './recipient.ts';
import { suppliedRelease } from './release.ts';
import { MemorySink } from './sink.ts';
import { encryptVectors } from './testing/encrypt.ts';
import { encoderCases, errorCaseInput, errorCases, recipientStrings, type ErrorCase } from './testing/interop.ts';
import { encoderCases, errorCaseInput, errorCases, noteErrorCases, noteErrorInput, recipientStrings, type ErrorCase, type NoteErrorCase } from './testing/interop.ts';
// A file of a format 3 capsule, as the Go script records it.
interface SampleFile {
@ -47,6 +53,7 @@ interface OpenVerdict {
files?: SampleFile[];
head_cbor?: string;
verdicts?: string[];
area_len?: number;
};
}
interface Sample {
@ -64,8 +71,11 @@ interface Sample {
identities: string[];
dkk?: string;
known: { id: string; version: number }[];
/** The public note that the writer was given, if any. */
public_note?: string;
dkc: string;
go: { inspect: string; opens: OpenVerdict[]; inner_stanzas: number; reencoded: boolean };
/** public_note: what Header.PublicNote reads in PUBLIC_HEADER, null without a note. */
go: { inspect: string; opens: OpenVerdict[]; inner_stanzas: number; reencoded: boolean; public_note: string | null };
}
const V = JSON.parse(readFileSync(new URL('./testing/capsule-vectors.json', import.meta.url), 'utf8')) as {
releases: { round: number; signature: string }[];
@ -74,6 +84,7 @@ const V = JSON.parse(readFileSync(new URL('./testing/capsule-vectors.json', impo
encoders: { seed: number; count: number; sha256: string; equal: number; differences: string[] };
recipients: { string: string; parse: string; check?: string }[];
errors: (ErrorCase & { ts: string; go: string })[];
note_errors: (NoteErrorCase & { ts: string; go: string })[];
};
// The time of a round, when its release is published.
@ -85,7 +96,8 @@ function options(round: number, extra: Partial<OpenOptions> = {}): OpenOptions {
}
// The verdict of open in the form of the Go script: in format 3, the files
// that a sink receives, the head encoded again and the verdicts.
// that a sink receives, the head encoded again, the verdicts and the size of
// the security area.
async function verdict(dkc: Uint8Array, credentials: string, opts: OpenOptions): Promise<OpenVerdict> {
const sink = new MemorySink();
const r = await open(dkc, { ...opts, sink });
@ -108,6 +120,7 @@ async function verdict(dkc: Uint8Array, credentials: string, opts: OpenOptions):
...(files.length === 0 ? {} : { files }),
head_cbor: toHex(encodeHead(r.head!)),
verdicts: [r.verdicts!.signature, r.verdicts!.seal],
area_len: r.areaLen!,
},
};
}
@ -147,30 +160,36 @@ describe('capsules of the writer, opened by the Go reference (capsule-vectors.js
]);
});
it('holds the samples of format 3: one file, a tree over two chunks, a comment alone, bloque256, time_and_key and head extensions', () => {
it('holds the samples of format 3, written as any caller writes them: one file, a tree over two chunks, a comment alone, bloque256, time_and_key, head extensions and public notes', () => {
const three = V.samples.filter((s) => s.format === FORMAT_3);
expect(three.map((s) => [s.name.startsWith('format 3: '), s.files?.length ?? 0, s.padding, s.policy])).toEqual([
[true, 1, 2, 'time_only'],
[true, 7, 2, 'time_only'],
[true, 0, 2, 'time_only'],
[true, 1, 1, 'time_only'],
[true, 1, 2, 'time_and_key'],
[true, 1, 2, 'time_only'],
expect(three.map((s) => [s.name.startsWith('format 3: '), s.files?.length ?? 0, s.padding, s.policy, s.public_note?.length ?? 0])).toEqual([
[true, 1, 2, 'time_only', 0],
[true, 7, 2, 'time_only', 0],
[true, 0, 2, 'time_only', 0],
[true, 1, 1, 'time_only', 0],
[true, 1, 2, 'time_and_key', 0],
[true, 1, 2, 'time_only', 0],
[true, 1, 2, 'time_only', 55],
[true, 1, 2, 'time_and_key', 512],
]);
expect(V.samples.length).toBe(19);
expect(V.samples.length).toBe(21);
// The note of 512 UTF-16 units is the longest, 1024 bytes of UTF-8.
expect(new TextEncoder().encode(three.at(-1)!.public_note).length).toBe(1024);
});
for (const s of V.samples) {
it(`Go opens ${s.name} with each credential, and encodes its objects again byte for byte`, () => {
it(`Go opens ${s.name} with each credential, reads its public note, and encodes its objects again byte for byte`, () => {
expect(s.go.inspect).toBe('ok');
const lengths = { format: s.format, length: s.length, padding: s.padding, padded_length: s.padded_length };
// A capsule of format 3 that encryptFiles writes has the area of 32 KiB and the empty security area.
const opened =
s.format === FORMAT_3
? { ...lengths, ...(s.files === undefined ? {} : { files: s.files }), head_cbor: s.head_cbor, verdicts: ['F0', 'S0'] }
? { ...lengths, ...(s.files === undefined ? {} : { files: s.files }), head_cbor: s.head_cbor, verdicts: ['F0', 'S0'], area_len: AREA_LEN }
: { content_sha256: s.content_sha256, ...lengths };
for (const o of s.go.opens) expect(o, o.credentials).toEqual({ credentials: o.credentials, result: 'ok', opened });
expect(s.go.inner_stanzas).toBe(s.policy === 'time_and_key' ? 16 : 0);
expect(s.go.reencoded).toBe(true);
expect(s.go.public_note).toBe(s.public_note ?? null);
});
it(`this library reaches the verdicts of Go on ${s.name}`, async () => {
@ -178,6 +197,7 @@ describe('capsules of the writer, opened by the Go reference (capsule-vectors.js
const extensions = registry(s.known);
const i = await inspect(dkc, { extensions });
expect(i.error === undefined ? 'ok' : errorCode(i.error)).toBe(s.go.inspect);
expect(publicNote(i.header!.noncritical) ?? null).toBe(s.go.public_note);
const ids = s.identities.map(fromHex);
const got: OpenVerdict[] = [];
for (const [n, id] of ids.entries()) got.push(await verdict(dkc, `identity ${n}`, options(s.round, { extensions, identities: [id] })));
@ -254,3 +274,17 @@ describe('the invalid options against capsule.Encrypt', () => {
});
}
});
describe('the public notes against capsule.EncryptFiles', () => {
it('holds the table of testing/interop.ts', () => {
expect(V.note_errors.map(({ ts: _ts, go: _go, ...c }) => c)).toEqual(noteErrorCases());
});
for (const c of V.note_errors) {
it(`refuses a note with ${c.name} with the text of Go`, async () => {
const { files, opts } = noteErrorInput(c);
const err = await failure(encryptFiles(files, opts));
expect([err.message, c.ts]).toEqual([c.go, c.go]);
});
}
});

File diff suppressed because one or more lines are too long

@ -2,8 +2,9 @@
// reference and interop.test.ts checks again (plan of phase 3, decision 11
// and section 8, point 9): fixed identities, the inputs of the encoder
// differential, drawn from a seed, the corpus of recipient strings and keys,
// and the table of invalid options of encrypt as a generator of test
// vectors, in a form the Go script reads. Everything here is deterministic.
// the table of invalid options of encrypt as a generator of test vectors,
// and that of the public notes that encryptFiles refuses, in a form the Go
// script reads. Everything here is deterministic.
import type { AccessKey } from '../accesskey.ts';
import { ACCESS_TYPE_X25519 } from '../accesskey.ts';
@ -11,7 +12,7 @@ import { fromHex, toHex } from '../bytes.ts';
import type { Control } from '../control.ts';
import { parseRFC3339 } from '../datekey.ts';
import { sha256Hasher } from '../digest.ts';
import type { EncryptOptions, EncryptSource } from '../encrypt.ts';
import type { EncryptOptions, EncryptSource, FileSource } from '../encrypt.ts';
import type { Extension } from '../extension.ts';
import type { Header } from '../header.ts';
import { MAX_PAYLOAD_LENGTH, type Padding } from '../padding.ts';
@ -203,6 +204,8 @@ export interface ErrorCase {
control_noncritical?: ExtensionJSON[];
/** Flips the lowest bit of the chain hash of the profile. */
chain_hash_flip?: boolean;
/** A public note, which format 2 has no place for. */
public_note?: string;
}
/** The invalid options that have an equivalent in Go, whose texts must match. */
@ -242,9 +245,44 @@ export function errorCases(): ErrorCase[] {
{ ...base, name: 'an empty extension_id in the control', control_noncritical: [{ id: '', version: 1 }] },
{ ...base, name: '65 critical header extensions', critical: Array.from({ length: 65 }, (_, i) => ({ id: `e${String(i).padStart(2, '0')}`, version: 1 })) },
{ ...base, name: 'an extension with empty data', noncritical: [{ id: 'a', version: 1, data: '' }] },
{ ...base, name: 'a public note', public_note: 'Cartas del viaje a Lisboa' },
];
}
/** A public note that breaks the rules of spec §24.1, for encryptFiles. */
export interface NoteErrorCase {
name: string;
note: string;
}
/**
* The public notes that encryptFiles refuses, with the texts of
* extension.CheckNote after "capsule: " in Go: every rule of the note but
* the UTF-8, which a lone surrogate breaks and JSON cannot carry to Go.
*/
export function noteErrorCases(): NoteErrorCase[] {
return [
{ name: 'a tab', note: 'a\tb' },
{ name: 'a line feed', note: 'a\nb' },
{ name: 'a carriage return', note: 'a\rb' },
{ name: 'a space at the start', note: ' a' },
{ name: 'a space at the end', note: 'a ' },
{ name: '1025 bytes', note: 'x'.repeat(1025) },
{ name: 'a right-to-left override', note: 'a\u{202e}b' },
{ name: 'a zero-width space', note: 'a\u{200b}b' },
{ name: 'a byte order mark at the start', note: '\u{feff}abc' },
];
}
/** The file and the options of encryptFiles that a case of noteErrorCases stands for: one file of a byte, for round 1000, with now at the genesis. */
export function noteErrorInput(c: NoteErrorCase): { files: FileSource[]; opts: EncryptOptions } {
const x = te.encode('x');
return {
files: [{ path: 'a.txt', size: 1, open: () => new Blob([x]).stream() }],
opts: { profile: quicknet(), unlockAt: parseRFC3339('2023-08-23T15:59:24Z'), now: () => parseRFC3339('2023-08-23T15:09:27Z'), policy: 0, publicNote: c.note },
};
}
/** The source and the options of encrypt that a case of the table stands for. */
export function errorCaseInput(c: ErrorCase): { src: EncryptSource; opts: EncryptOptions } {
const p = quicknet();
@ -269,6 +307,7 @@ export function errorCaseInput(c: ErrorCase): { src: EncryptSource; opts: Encryp
noncritical: ext(c.noncritical),
controlCritical: ext(c.control_critical),
controlNoncritical: ext(c.control_noncritical),
...(c.public_note === undefined ? {} : { publicNote: c.public_note }),
},
};
}

Loading…
Cancel
Save

Powered by TurnKey Linux.