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 ( ) , 0 o644 ) )
t . Logf ( "wrote %s, %d bytes" , * outFlag , buf . Len ( ) )
}