You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
DateKeys-App/scripts/signing-go-vectors_test.go

497 lines
18 KiB

The writer signs and seals: the hooks of capsule.EncryptFiles of Go encryptFiles gains authorKey (alg 1, an AuthorSigner such as AuthorKey), cmsSigner (alg 2, a CMS signature with certificates), sealer (seal_type 2, an RFC 3161 token) and largeArea, as EncryptOptions of Go at spec-v0.12: the same checks in the same order with the same texts, the signature and the seal made with the final control and head and before anything is written, and the security area evaluated by the reader of this library in the context of the capsule before it is written, as Go's security does. The hooks may be asynchronous. The area grows to 64 KiB only when what was signed does not fit and largeArea allows it, and the larger capsule counts in the limit of memory. security.ts encodes the area with its signature and seal, and securitycms.ts encodes SIGNERS. scripts/signing-go-vectors_test.go, run as a test in an export of datekeys-go at spec-v0.12, writes testing/signing-vectors.json: with the draws of crypto/rand of Go and the signatures and tokens of its hooks, encryptFiles writes the eight signed and sealed capsules of Go byte for byte, asks the hooks over the same messages, and fails with the text of Go in the other 15 recipes; and Go opens the five capsules that scripts/signing-ts-samples.mjs writes with this library, its own random values and certificates, with the same verdicts and lines. check-build.mjs fails when a page loads the author keys with the page, or when /inspect can load them at all. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 days ago
// Writes src/lib/dkc/testing/signing-vectors.json, the interoperability of
// the hooks of the writer of this library with the Go reference at
// spec-v0.12: what capsule.EncryptFiles writes when it signs with alg 1 or
// alg 2 and seals with seal_type 2, and how Go reads what encryptFiles of
// this library writes.
//
// cases: for each recipe of this file, EncryptFiles runs while crypto/rand
// reads a ChaCha20 keystream under SHA-256(seed) and a zero nonce, and each
// draw is recorded in hexadecimal, in its order: the salt of the head,
// capsule_id, I_PAYLOAD, what age draws for the measured seal, the real seal
// and PAYLOAD_AGE. The hooks are those of the tests of package capsule
// (signed_test.go): an author key from a seed (alg 1), and the CMS signatures
// and RFC 3161 tokens of internal/cms/cmstest (alg 2 and seal_type 2), whose
// ECDSA and RSA draw from Go's internal generator, which only
// testing/cryptotest.SetGlobalRandom fixes, in a test binary: this file runs
// as a test. What each hook was given and returned is recorded, so that the
// tests of this library hand the writer the same signatures and tokens, check
// that it asks for them over the same messages, and write the same bytes with
// the same draws. For each capsule it records its length, its SHA-256, its
// SECURITY_CBOR and the size of its area, and what capsule.Open gives, with
// and without the author key saved under a label: the verdicts, the lines that
// show them, the head and the files. For each error, its text.
//
// samples: with -samples, the capsules that scripts/signing-ts-samples.mjs
// writes with encryptFiles of this library, with its own random values and
// its own certificates, opened with capsule.Open, with their verdicts and
// lines.
//
// It imports internal packages, so it runs as a test in an export of
Specification 0.16 draft: a seal without accuracy, drand's JSON read strictly The draft v0.16 of datekeys-go at 4f78854 (branch v0.16): SPEC_VERSION 0.16, and testdata, wordlists and annex synced from that commit. The annex is §79 of the draft, with the CC BY-ND 4.0 license of the specification in its title and the key of words in 79.7. A seal without accuracy proves nothing before the opening date (§29.7, §29.11, as 7e3b810): a valid seal is S4 only when its token carries accuracy and t plus the accuracy is before round_time; otherwise S5, with the first reason that holds: late, no accuracy under the BTSP policy of ETSI EN 319 421 (0.4.0.2023.1.1), or no accuracy. cms.ts reads hasAccuracy and the policy of the token (tokenIsBTSP); securitycms.ts gives SealReason, Detail.sealReason and SignerLine.reason; S5 has no fixed text any more, and verdictLines writes it and the line of a signer of F6 with the reason, the texts of Go byte for byte (sealReasonText). encryptFiles returns the verdicts of the area it wrote in Encrypted.security, as Result.Security of Go, so that a writer warns of a seal without accuracy (§62.1 rule 19). drand's JSON is read strictly (§47.1, as b570338): parseDrandJSON, as ParseDrandJSON of Go, reads RFC 8259 JSON in valid UTF-8 whose value is an object, with no name repeated in any object, names compared exactly once their escapes are decoded, a lone escaped surrogate malformed, the round a number without sign, fraction or exponent from 1 to 2^53 - 1, and signature and randomness strings, with the error texts of Go. ParsedRelease is now a Release: no round above 2^53 - 1 is read. The page reads the answers of the relays with it (drand.ts), as the client of Go does, and the pasted release with strictJSON and jsonRound (release-input.ts), so that it never reads another round than step 10. Tests: security_cms.json with seal_reason (143 cases), the 38 JSON inputs of release.json, the new cases of signature2_test.go and drandjson_test.go (with the escapes written as escapes), and the new fixtures: format3_time_and_key_words opens with the identity that the words of its words_text give with normalizeWords and wordKey, in the library and in the page, and format3_full_chunk, whose PAYLOAD_AGE ends in a full STREAM chunk, opens. check-build.mjs counts words_text among the secrets of the fixtures. Reference files made again with Go at 4f78854: mutation-texts.json (its spec field only), ibe-vectors.json (the two new fixtures, the rest unchanged) and signing-vectors.json, in an export of 4f78854 with the same frozen samples read again: the capsules are the same, and the tokens of the sealer, without accuracy, now give S5. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 day ago
// datekeys-go made with git archive, which it does not change, never in the
// repository itself: at the tag spec-v0.12 when it was written, and at
// 4f78854, the draft v0.16, since. From the root of this repository:
The writer signs and seals: the hooks of capsule.EncryptFiles of Go encryptFiles gains authorKey (alg 1, an AuthorSigner such as AuthorKey), cmsSigner (alg 2, a CMS signature with certificates), sealer (seal_type 2, an RFC 3161 token) and largeArea, as EncryptOptions of Go at spec-v0.12: the same checks in the same order with the same texts, the signature and the seal made with the final control and head and before anything is written, and the security area evaluated by the reader of this library in the context of the capsule before it is written, as Go's security does. The hooks may be asynchronous. The area grows to 64 KiB only when what was signed does not fit and largeArea allows it, and the larger capsule counts in the limit of memory. security.ts encodes the area with its signature and seal, and securitycms.ts encodes SIGNERS. scripts/signing-go-vectors_test.go, run as a test in an export of datekeys-go at spec-v0.12, writes testing/signing-vectors.json: with the draws of crypto/rand of Go and the signatures and tokens of its hooks, encryptFiles writes the eight signed and sealed capsules of Go byte for byte, asks the hooks over the same messages, and fails with the text of Go in the other 15 recipes; and Go opens the five capsules that scripts/signing-ts-samples.mjs writes with this library, its own random values and certificates, with the same verdicts and lines. check-build.mjs fails when a page loads the author keys with the page, or when /inspect can load them at all. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 days ago
//
// node scripts/signing-ts-samples.mjs > /tmp/ts-signing.json
Specification 0.16 draft: a seal without accuracy, drand's JSON read strictly The draft v0.16 of datekeys-go at 4f78854 (branch v0.16): SPEC_VERSION 0.16, and testdata, wordlists and annex synced from that commit. The annex is §79 of the draft, with the CC BY-ND 4.0 license of the specification in its title and the key of words in 79.7. A seal without accuracy proves nothing before the opening date (§29.7, §29.11, as 7e3b810): a valid seal is S4 only when its token carries accuracy and t plus the accuracy is before round_time; otherwise S5, with the first reason that holds: late, no accuracy under the BTSP policy of ETSI EN 319 421 (0.4.0.2023.1.1), or no accuracy. cms.ts reads hasAccuracy and the policy of the token (tokenIsBTSP); securitycms.ts gives SealReason, Detail.sealReason and SignerLine.reason; S5 has no fixed text any more, and verdictLines writes it and the line of a signer of F6 with the reason, the texts of Go byte for byte (sealReasonText). encryptFiles returns the verdicts of the area it wrote in Encrypted.security, as Result.Security of Go, so that a writer warns of a seal without accuracy (§62.1 rule 19). drand's JSON is read strictly (§47.1, as b570338): parseDrandJSON, as ParseDrandJSON of Go, reads RFC 8259 JSON in valid UTF-8 whose value is an object, with no name repeated in any object, names compared exactly once their escapes are decoded, a lone escaped surrogate malformed, the round a number without sign, fraction or exponent from 1 to 2^53 - 1, and signature and randomness strings, with the error texts of Go. ParsedRelease is now a Release: no round above 2^53 - 1 is read. The page reads the answers of the relays with it (drand.ts), as the client of Go does, and the pasted release with strictJSON and jsonRound (release-input.ts), so that it never reads another round than step 10. Tests: security_cms.json with seal_reason (143 cases), the 38 JSON inputs of release.json, the new cases of signature2_test.go and drandjson_test.go (with the escapes written as escapes), and the new fixtures: format3_time_and_key_words opens with the identity that the words of its words_text give with normalizeWords and wordKey, in the library and in the page, and format3_full_chunk, whose PAYLOAD_AGE ends in a full STREAM chunk, opens. check-build.mjs counts words_text among the secrets of the fixtures. Reference files made again with Go at 4f78854: mutation-texts.json (its spec field only), ibe-vectors.json (the two new fixtures, the rest unchanged) and signing-vectors.json, in an export of 4f78854 with the same frozen samples read again: the capsules are the same, and the tokens of the sealer, without accuracy, now give S5. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 day ago
// commit=$(git -C ../datekeys-go rev-parse '4f78854^{commit}')
The writer signs and seals: the hooks of capsule.EncryptFiles of Go encryptFiles gains authorKey (alg 1, an AuthorSigner such as AuthorKey), cmsSigner (alg 2, a CMS signature with certificates), sealer (seal_type 2, an RFC 3161 token) and largeArea, as EncryptOptions of Go at spec-v0.12: the same checks in the same order with the same texts, the signature and the seal made with the final control and head and before anything is written, and the security area evaluated by the reader of this library in the context of the capsule before it is written, as Go's security does. The hooks may be asynchronous. The area grows to 64 KiB only when what was signed does not fit and largeArea allows it, and the larger capsule counts in the limit of memory. security.ts encodes the area with its signature and seal, and securitycms.ts encodes SIGNERS. scripts/signing-go-vectors_test.go, run as a test in an export of datekeys-go at spec-v0.12, writes testing/signing-vectors.json: with the draws of crypto/rand of Go and the signatures and tokens of its hooks, encryptFiles writes the eight signed and sealed capsules of Go byte for byte, asks the hooks over the same messages, and fails with the text of Go in the other 15 recipes; and Go opens the five capsules that scripts/signing-ts-samples.mjs writes with this library, its own random values and certificates, with the same verdicts and lines. check-build.mjs fails when a page loads the author keys with the page, or when /inspect can load them at all. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 days ago
// tmp=$(mktemp -d)
// git -C ../datekeys-go archive "$commit" | tar -x -C "$tmp"
// mkdir "$tmp/signingvectors"
// cp scripts/signing-go-vectors_test.go "$tmp/signingvectors/"
// (cd "$tmp/signingvectors" && go test -run TestSigningVectors -count=1 \
// -args -source "$commit" -samples /tmp/ts-signing.json \
// -out "$OLDPWD/src/lib/dkc/testing/signing-vectors.json")
// rm -rf "$tmp"
//
// The cases are the same on every run with Go 1.26.8; the samples are
Specification 0.16 draft: a seal without accuracy, drand's JSON read strictly The draft v0.16 of datekeys-go at 4f78854 (branch v0.16): SPEC_VERSION 0.16, and testdata, wordlists and annex synced from that commit. The annex is §79 of the draft, with the CC BY-ND 4.0 license of the specification in its title and the key of words in 79.7. A seal without accuracy proves nothing before the opening date (§29.7, §29.11, as 7e3b810): a valid seal is S4 only when its token carries accuracy and t plus the accuracy is before round_time; otherwise S5, with the first reason that holds: late, no accuracy under the BTSP policy of ETSI EN 319 421 (0.4.0.2023.1.1), or no accuracy. cms.ts reads hasAccuracy and the policy of the token (tokenIsBTSP); securitycms.ts gives SealReason, Detail.sealReason and SignerLine.reason; S5 has no fixed text any more, and verdictLines writes it and the line of a signer of F6 with the reason, the texts of Go byte for byte (sealReasonText). encryptFiles returns the verdicts of the area it wrote in Encrypted.security, as Result.Security of Go, so that a writer warns of a seal without accuracy (§62.1 rule 19). drand's JSON is read strictly (§47.1, as b570338): parseDrandJSON, as ParseDrandJSON of Go, reads RFC 8259 JSON in valid UTF-8 whose value is an object, with no name repeated in any object, names compared exactly once their escapes are decoded, a lone escaped surrogate malformed, the round a number without sign, fraction or exponent from 1 to 2^53 - 1, and signature and randomness strings, with the error texts of Go. ParsedRelease is now a Release: no round above 2^53 - 1 is read. The page reads the answers of the relays with it (drand.ts), as the client of Go does, and the pasted release with strictJSON and jsonRound (release-input.ts), so that it never reads another round than step 10. Tests: security_cms.json with seal_reason (143 cases), the 38 JSON inputs of release.json, the new cases of signature2_test.go and drandjson_test.go (with the escapes written as escapes), and the new fixtures: format3_time_and_key_words opens with the identity that the words of its words_text give with normalizeWords and wordKey, in the library and in the page, and format3_full_chunk, whose PAYLOAD_AGE ends in a full STREAM chunk, opens. check-build.mjs counts words_text among the secrets of the fixtures. Reference files made again with Go at 4f78854: mutation-texts.json (its spec field only), ibe-vectors.json (the two new fixtures, the rest unchanged) and signing-vectors.json, in an export of 4f78854 with the same frozen samples read again: the capsules are the same, and the tokens of the sealer, without accuracy, now give S5. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 day ago
// random, and frozen with what Go gives for them. For v0.16 the frozen
// samples were read again, with their "ts" as this library reads them now,
// and the tokens of the sealer, without accuracy, give S5 (spec v0.16,
// §29.11).
The writer signs and seals: the hooks of capsule.EncryptFiles of Go encryptFiles gains authorKey (alg 1, an AuthorSigner such as AuthorKey), cmsSigner (alg 2, a CMS signature with certificates), sealer (seal_type 2, an RFC 3161 token) and largeArea, as EncryptOptions of Go at spec-v0.12: the same checks in the same order with the same texts, the signature and the seal made with the final control and head and before anything is written, and the security area evaluated by the reader of this library in the context of the capsule before it is written, as Go's security does. The hooks may be asynchronous. The area grows to 64 KiB only when what was signed does not fit and largeArea allows it, and the larger capsule counts in the limit of memory. security.ts encodes the area with its signature and seal, and securitycms.ts encodes SIGNERS. scripts/signing-go-vectors_test.go, run as a test in an export of datekeys-go at spec-v0.12, writes testing/signing-vectors.json: with the draws of crypto/rand of Go and the signatures and tokens of its hooks, encryptFiles writes the eight signed and sealed capsules of Go byte for byte, asks the hooks over the same messages, and fails with the text of Go in the other 15 recipes; and Go opens the five capsules that scripts/signing-ts-samples.mjs writes with this library, its own random values and certificates, with the same verdicts and lines. check-build.mjs fails when a page loads the author keys with the page, or when /inspect can load them at all. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 days ago
package signingvectors
import (
"bytes"
"context"
"crypto/ed25519"
"crypto/elliptic"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"flag"
"io"
"os"
"runtime"
"testing"
"testing/cryptotest"
"time"
"golang.org/x/crypto/chacha20"
datekeys "g.activething.com/go/DateKeys"
"g.activething.com/go/DateKeys/authorkey"
"g.activething.com/go/DateKeys/capsule"
"g.activething.com/go/DateKeys/datekey"
"g.activething.com/go/DateKeys/internal/cms/cmstest"
"g.activething.com/go/DateKeys/internal/testkit"
"g.activething.com/go/DateKeys/profile"
"g.activething.com/go/DateKeys/provider"
)
var (
sourceFlag = flag.String("source", "", "the commit of datekeys-go that this tree exports")
outFlag = flag.String("out", "", "the JSON file to write")
samplesFlag = flag.String("samples", "", "the output of scripts/signing-ts-samples.mjs")
)
type obj = map[string]any
func h(b []byte) string { return hex.EncodeToString(b) }
func sum(b []byte) string {
s := sha256.Sum256(b)
return h(s[:])
}
func must[T any](v T, err error) T {
if err != nil {
panic(err)
}
return v
}
func unhex(s string) []byte { return must(hex.DecodeString(s)) }
// ---------------------------------------------------------------------------
// The recipes
type authorIn struct {
Seed string `json:"seed"` // hex, 32 bytes
Bad string `json:"bad,omitempty"` // zero_signature, short_key
}
type cmsIn struct {
Signers []string `json:"signers"` // names of the certificates that sign
Extra string `json:"extra,omitempty"` // a required signer that does not sign
Unsealed bool `json:"unsealed,omitempty"` // no seal in the signatures
Junk int `json:"junk,omitempty"` // bytes of an unsigned attribute; -1 for the most that still fails as too large
Error string `json:"error,omitempty"` // Sign fails with this text
Garbage bool `json:"garbage,omitempty"` // Sign returns bytes that are no signature
}
type sealerIn struct {
Error string `json:"error,omitempty"` // Seal fails with this text
Late bool `json:"late,omitempty"` // the authority seals an hour after the time of the round
}
type fileIn struct {
Path string `json:"path"`
Text string `json:"text"`
}
type recipe struct {
Name string `json:"name"`
Seed string `json:"seed"`
Files []fileIn `json:"files,omitempty"`
Comment string `json:"comment,omitempty"`
Author string `json:"author,omitempty"`
AuthorKey *authorIn `json:"author_key,omitempty"`
CMS *cmsIn `json:"cms,omitempty"`
Sealer *sealerIn `json:"sealer,omitempty"`
LargeArea bool `json:"large_area,omitempty"`
TestAreaLen uint32 `json:"test_area_len,omitempty"`
}
const authorSeed = "a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0"
func recipes() []recipe {
files := []fileIn{{"nota.txt", "Hola.\n"}, {"fotos/año 2026.txt", "Una foto que no es una foto.\n"}}
key := func() *authorIn { return &authorIn{Seed: authorSeed} }
var out []recipe
add := func(r recipe) {
r.Seed = "signing " + r.Name
out = append(out, r)
}
// Capsules.
add(recipe{Name: "alg 1", Files: files, Comment: "Firmado con mi clave.", Author: "Ana López", AuthorKey: key()})
add(recipe{Name: "alg 1 and a seal", Files: files, AuthorKey: key(), Sealer: &sealerIn{}})
add(recipe{Name: "a seal alone", Files: files[:1], Sealer: &sealerIn{}})
add(recipe{Name: "alg 2, two signers, sealed", Files: files, Comment: "Firmado por los dos.", CMS: &cmsIn{Signers: []string{"Ana López", "Luis Gómez"}}})
add(recipe{Name: "alg 2, a large area", Files: files[:1], CMS: &cmsIn{Signers: []string{"Ana López"}, Junk: 40000}, LargeArea: true})
add(recipe{Name: "alg 1, a large area that does not widen", Comment: "Solo un comentario.", AuthorKey: key(), LargeArea: true})
add(recipe{Name: "alg 1, an area of 512", Files: files[:1], AuthorKey: key(), TestAreaLen: 512})
add(recipe{Name: "a seal after the date", Files: files[:1], AuthorKey: key(), Sealer: &sealerIn{Late: true}})
// Errors.
add(recipe{Name: "AuthorKey and CMSSigner", Files: files, AuthorKey: key(), CMS: &cmsIn{Signers: []string{"Ana López"}}})
add(recipe{Name: "CMSSigner and Sealer", Files: files, CMS: &cmsIn{Signers: []string{"Ana López"}}, Sealer: &sealerIn{}})
add(recipe{Name: "a test area and LargeArea", Files: files, AuthorKey: key(), TestAreaLen: 512, LargeArea: true})
add(recipe{Name: "an author key of 31 bytes", Files: files, AuthorKey: &authorIn{Seed: authorSeed, Bad: "short_key"}})
add(recipe{Name: "a signature of zeros", Files: files, AuthorKey: &authorIn{Seed: authorSeed, Bad: "zero_signature"}})
add(recipe{Name: "a signature of zeros and a seal", Files: files, AuthorKey: &authorIn{Seed: authorSeed, Bad: "zero_signature"}, Sealer: &sealerIn{}})
add(recipe{Name: "alg 2 that does not fit", Files: files, CMS: &cmsIn{Signers: []string{"Ana López"}, Junk: 40000}})
add(recipe{Name: "the signing application fails", Files: files, CMS: &cmsIn{Signers: []string{"Ana López"}, Error: "la persona canceló la firma"}})
add(recipe{Name: "the authority fails", Files: files, AuthorKey: key(), Sealer: &sealerIn{Error: "the authority does not answer"}})
add(recipe{Name: "alg 2 without a required signer", Files: files, CMS: &cmsIn{Signers: []string{"Ana López"}, Extra: "Luis Gómez"}})
add(recipe{Name: "alg 2 without seals", Files: files, CMS: &cmsIn{Signers: []string{"Luis Gómez"}, Unsealed: true}})
add(recipe{Name: "alg 2 that is not a signature", Files: files, CMS: &cmsIn{Signers: []string{"Ana López"}, Garbage: true}})
add(recipe{Name: "SIGNERS empty", Files: files, CMS: &cmsIn{}})
add(recipe{Name: "SIGNERS twice", Files: files, CMS: &cmsIn{Signers: []string{"Ana López", "Ana López"}}})
// Last, since its search draws from the generator of the test.
add(recipe{Name: "alg 2 too large for any area", Files: files[:1], CMS: &cmsIn{Signers: []string{"Ana López"}, Junk: -1}, LargeArea: true})
return out
}
// seeded is the crypto/rand.Reader of a case: the ChaCha20 keystream under
// SHA-256(seed) and a zero nonce. It records every draw.
type seeded struct {
c *chacha20.Cipher
draws [][]byte
}
func newSeeded(seed string) *seeded {
key := sha256.Sum256([]byte(seed))
return &seeded{c: must(chacha20.NewUnauthenticatedCipher(key[:], make([]byte, chacha20.NonceSize)))}
}
func (s *seeded) Read(p []byte) (int, error) {
clear(p)
s.c.XORKeyStream(p, p)
s.draws = append(s.draws, bytes.Clone(p))
return len(p), nil
}
// ---------------------------------------------------------------------------
// The keys and the hooks
type certs struct {
byName map[string]cmstest.Signer
tsa cmstest.Signer
}
var certFrom, certTo = time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), time.Date(2040, 1, 1, 0, 0, 0, 0, time.UTC)
func newCerts() *certs {
c := &certs{byName: map[string]cmstest.Signer{}}
c.byName["Ana López"] = cmstest.NewECDSA("Ana López", elliptic.P256(), certFrom, certTo)
c.byName["Luis Gómez"] = cmstest.NewRSA("Luis Gómez", 2048, certFrom, certTo)
c.tsa = cmstest.NewECDSA("TSA de prueba", elliptic.P256(), certFrom, certTo)
return c
}
type hooks struct{ rec obj }
func (k *hooks) set(name string, v any) { k.rec[name] = v }
type authorHook struct {
key *authorkey.Key
bad string
k *hooks
}
func (a *authorHook) Public() []byte {
pub := a.key.Public()
if a.bad == "short_key" {
pub = pub[:31]
}
a.k.set("author_public", h(pub))
return pub
}
func (a *authorHook) Sign(msg []byte) []byte {
sig := a.key.Sign(msg)
if a.bad == "zero_signature" {
sig = make([]byte, ed25519.SignatureSize)
}
a.k.set("author_message", h(msg))
a.k.set("author_signature", h(sig))
return sig
}
type cmsHook struct {
in cmsIn
c *certs
when time.Time
k *hooks
}
func (s *cmsHook) signers() []cmstest.Signer {
var out []cmstest.Signer
for _, n := range s.in.Signers {
out = append(out, s.c.byName[n])
}
return out
}
func (s *cmsHook) Signers() [][32]byte {
out := [][32]byte{}
for _, x := range s.signers() {
out = append(out, sha256.Sum256(x.Cert.Raw))
}
if s.in.Extra != "" {
out = append(out, sha256.Sum256(s.c.byName[s.in.Extra].Cert.Raw))
}
list := []string{}
for _, x := range out {
list = append(list, h(x[:]))
}
s.k.set("cms_signers", list)
return out
}
func (s *cmsHook) der(msg []byte, junk int) []byte {
o := cmstest.Options{Junk: junk}
if !s.in.Unsealed {
o.Token = func(sig []byte) []byte {
return cmstest.Token(sig, s.when, cmstest.TokenOptions{Accuracy: time.Second}, s.c.tsa)
}
}
return cmstest.Signature(msg, o, s.signers()...)
}
func (s *cmsHook) Sign(msg []byte) ([]byte, error) {
s.k.set("cms_message", h(msg))
if s.in.Error != "" {
return nil, errors.New(s.in.Error)
}
var der []byte
switch {
case s.in.Garbage:
der = []byte("not a signature")
case s.in.Junk < 0:
// The most junk whose signature still fits in key 2 of the
// reader, 64 KiB, while SECURITY_CBOR is more than 64 KiB.
base := len(s.der(msg, 1))
junk := 65536 - 45 - base
for der = s.der(msg, junk); len(der) > 65536-45; der = s.der(msg, junk) {
junk--
}
default:
der = s.der(msg, s.in.Junk)
}
s.k.set("cms_der", h(der))
return der, nil
}
type sealHook struct {
in sealerIn
c *certs
when time.Time
k *hooks
}
func (s *sealHook) Seal(subject [32]byte) ([]byte, error) {
s.k.set("seal_subject", h(subject[:]))
if s.in.Error != "" {
return nil, errors.New(s.in.Error)
}
token := cmstest.Token(subject[:], s.when, cmstest.TokenOptions{}, s.c.tsa)
s.k.set("seal_token", h(token))
return token, nil
}
// ---------------------------------------------------------------------------
// Writing and opening
func sourceOf(f fileIn) capsule.Source {
return capsule.Source{Path: f.Path, Size: int64(len(f.Text)), Open: func() (io.ReadCloser, error) {
return io.NopCloser(bytes.NewReader([]byte(f.Text))), nil
}}
}
// The genesis of Quicknet: the capsules open at round 1000.
var genesis = time.Unix(profile.Quicknet().GenesisTime, 0).UTC()
func run(r recipe, c *certs) (*capsule.Result, []byte, *seeded, obj, error) {
q := profile.Quicknet()
opts := capsule.EncryptOptions{
Profile: q, UnlockAt: must(datekey.RoundTime(q, 1000)), Now: func() time.Time { return genesis },
Comment: r.Comment, Author: r.Author, LargeArea: r.LargeArea,
TestVectors: r.TestAreaLen != 0, TestAreaLen: r.TestAreaLen,
}
k := &hooks{rec: obj{}}
if r.AuthorKey != nil {
opts.AuthorKey = &authorHook{key: must(authorkey.NewFromSeed(unhex(r.AuthorKey.Seed))), bad: r.AuthorKey.Bad, k: k}
}
if r.CMS != nil {
opts.CMSSigner = &cmsHook{in: *r.CMS, c: c, when: genesis, k: k}
}
if r.Sealer != nil {
when := genesis
if r.Sealer.Late {
when = opts.UnlockAt.Add(time.Hour)
}
opts.Sealer = &sealHook{in: *r.Sealer, c: c, when: when, k: k}
}
var sources []capsule.Source
for _, f := range r.Files {
sources = append(sources, sourceOf(f))
}
s := newSeeded(r.Seed)
old := rand.Reader
rand.Reader = s
var dst bytes.Buffer
res, err := capsule.EncryptFiles(&dst, sources, opts)
rand.Reader = old
return res, dst.Bytes(), s, k.rec, err
}
func releases() provider.ReleaseSource {
return testkit.NewSource(testkit.Release(1000))
}
// opened is what capsule.Open gives for dkc, with the author keys saved.
func opened(dkc []byte, keys map[string]string) obj {
files := &testkit.MemorySink{}
o := capsule.OpenOptions{
Registry: testkit.Registry(), Source: releases(), Sink: files, AuthorKeys: keys,
Now: func() time.Time { return time.Date(2026, 10, 6, 0, 0, 0, 0, time.UTC) },
}
res, err := capsule.Open(context.Background(), nil, bytes.NewReader(dkc), o)
v := obj{}
if err != nil {
v["result"] = datekeys.Code(err)
if v["result"] == "" {
v["result"] = "error: " + err.Error()
}
return v
}
v["result"] = "ok"
fs := []obj{}
for i, f := range res.Head.Files {
fs = append(fs, obj{"path": f.Path, "size": f.Size, "sha256": sum(files.Files[i])})
}
v["files"] = fs
v["head"] = h(must(capsule.EncodeHead(res.Head)))
v["verdicts"] = []string{string(res.Verdicts.Signature), string(res.Verdicts.Seal)}
if res.Verdicts.AuthorKey != ([32]byte{}) {
v["author_key"] = h(res.Verdicts.AuthorKey[:])
}
v["lines"] = res.Verdicts.Lines()
v["area_len"] = res.AreaLen
v["length"] = res.PayloadLength
return v
}
func caseOf(r recipe, c *certs) obj {
res, dkc, s, rec, err := run(r, c)
draws := []obj{}
for _, d := range s.draws {
draws = append(draws, obj{"n": len(d), "hex": h(d)})
}
out := obj{"recipe": r, "draws": draws}
if len(rec) > 0 {
out["hooks"] = rec
}
if err != nil {
out["error"] = err.Error()
if len(dkc) != 0 {
panic(r.Name + ": an error after writing")
}
return out
}
out["written"] = obj{"length": len(dkc), "sha256": sum(dkc), "capsule_id": h(res.CapsuleID[:]), "body_length": res.Length}
out["opened"] = opened(dkc, nil)
if r.AuthorKey != nil {
pub := must(authorkey.PublicString(must(authorkey.NewFromSeed(unhex(r.AuthorKey.Seed))).Public()))
out["opened_saved"] = opened(dkc, map[string]string{pub: "mi clave de 2026"})
}
return out
}
type sampleIn struct {
Name string `json:"name"`
DKC string `json:"dkc"`
AuthorKeys map[string]string `json:"author_keys,omitempty"`
TS obj `json:"ts"`
}
func TestSigningVectors(t *testing.T) {
if *sourceFlag == "" || *outFlag == "" {
t.Skip("run with -args -source COMMIT -out FILE [-samples FILE]")
}
cryptotest.SetGlobalRandom(t, 20261006)
c := newCerts()
cases := []obj{}
for _, r := range recipes() {
v := caseOf(r, c)
cases = append(cases, v)
if e, ok := v["error"]; ok {
t.Logf("%s: %s", r.Name, e)
} else {
t.Logf("%s: %d bytes, %v", r.Name, v["written"].(obj)["length"], v["opened"].(obj)["verdicts"])
}
}
samples := []obj{}
if *samplesFlag != "" {
var in struct {
Samples []sampleIn `json:"samples"`
}
must(0, json.Unmarshal(must(os.ReadFile(*samplesFlag)), &in))
for _, s := range in.Samples {
dkc := unhex(s.DKC)
v := obj{"name": s.Name, "dkc": s.DKC, "ts": s.TS, "opened": opened(dkc, nil)}
if s.AuthorKeys != nil {
v["author_keys"] = s.AuthorKeys
v["opened_saved"] = opened(dkc, s.AuthorKeys)
}
samples = append(samples, v)
t.Logf("sample %s: %v", s.Name, v["opened"].(obj)["verdicts"])
}
}
var buf bytes.Buffer
enc := json.NewEncoder(&buf)
enc.SetEscapeHTML(false)
enc.SetIndent("", " ")
must(0, enc.Encode(obj{
"description": "What capsule.EncryptFiles of datekeys-go writes when it signs and seals, for each recipe, while crypto/rand reads the keystream of ChaCha20 under SHA-256(seed) with a zero nonce, with each draw and what each hook was given and returned, and how capsule.Open reads it; the text of each error; and how capsule.Open reads the samples that encryptFiles of datekeys-ts wrote (scripts/signing-go-vectors_test.go). The capsules are time_only for round 1000 of Quicknet, written at its genesis.",
"source": *sourceFlag,
"go": runtime.Version(),
"release": h(testkit.Release(1000).Signature),
"cases": cases,
"samples": samples,
}))
must(0, os.WriteFile(*outFlag, buf.Bytes(), 0o644))
t.Logf("wrote %s, %d bytes", *outFlag, buf.Len())
}

Powered by TurnKey Linux.