Test data for the second implementation: alg 2, the seal and the locator

Fixtures format3_signed_cms (alg 2, two certificates, each sealed, F6) and
format3_sealed (alg 1 and a seal of seal_type 2, F4 and S4), with the
certificates, SIGNERS, the commitments, SEAL_SUBJECT, the token and the
result of each signer in their records. vectors/security_cms.json has 22
frozen areas with their context and verdicts (F1, F2, F5, F6, S1 to S5), and
vectors/locator.json the extension datekeys.capsule with its envelope hidden
in a host, its locator sealed with tlock, the padding at the boundaries and
the rules of the addresses. The README of testdata describes all of it.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
v0.11
dev 6 days ago
parent b0bda15d20
commit 78f56a2f1c

@ -0,0 +1,61 @@
package capsule_test
import (
"encoding/hex"
"path/filepath"
"reflect"
"testing"
"time"
"g.activething.com/go/DateKeys/capsule"
"g.activething.com/go/DateKeys/internal/testkit"
)
// testdata/vectors/security_cms.json is frozen: each case is evaluated again,
// with its context, and must give its verdicts, the results of each signer
// and the authority of a valid seal (spec v0.11 §29.7, §29.10, §29.11).
func TestCMSVectors(t *testing.T) {
var f testkit.CMSVectorFile
if err := testkit.ReadJSON(filepath.Join("..", "testdata", "vectors", "security_cms.json"), &f); err != nil {
t.Fatal(err)
}
if len(f.Cases) < 20 {
t.Fatalf("%d cases", len(f.Cases))
}
for _, c := range f.Cases {
t.Run(c.Name, func(t *testing.T) {
area, err := hex.DecodeString(c.SecurityCBOR)
if err != nil {
t.Fatal(err)
}
var v capsule.Verdicts
if c.NoContext {
v = capsule.EvaluateSecurity(area)
} else {
var sc capsule.SecurityContext
cc, _ := hex.DecodeString(c.Context.ControlCommit)
hd, _ := hex.DecodeString(c.Context.HeadDigest)
copy(sc.ControlCommit[:], cc)
copy(sc.HeadDigest[:], hd)
if sc.RoundTime, err = time.Parse(time.RFC3339, c.Context.RoundTime); err != nil {
t.Fatal(err)
}
v = capsule.EvaluateSecurityIn(area, &sc)
}
if string(v.Signature) != c.Signature || string(v.Seal) != c.Seal {
t.Fatalf("verdicts %s and %s, want %s and %s", v.Signature, v.Seal, c.Signature, c.Seal)
}
var signers, foreign []testkit.FixtureSignerResult
var holder, when string
if d := v.Detail; d != nil {
signers, foreign = signerResults(d.Signers), signerResults(d.Foreign)
if !d.SealTime.IsZero() {
holder, when = d.SealHolder, d.SealTime.UTC().Format(time.RFC3339)
}
}
if !reflect.DeepEqual(signers, c.Signers) || !reflect.DeepEqual(foreign, c.Foreign) || holder != c.SealHolder || when != c.SealTime {
t.Errorf("signers %+v foreign %+v seal %q %q; want %+v %+v %q %q", signers, foreign, holder, when, c.Signers, c.Foreign, c.SealHolder, c.SealTime)
}
})
}
}

@ -36,7 +36,7 @@ var fixtureNames = []string{
"format2_time_and_key_portable", "format2_time_and_key_recipients", "format2_time_and_key_sixteen",
"format3_single", "format3_tree", "format3_comment_only", "format3_bloque256", "format3_time_and_key_portable",
"format3_area_1024", "format3_security_v2", "format3_signature_unsupported", "format3_seal_unsupported",
"format3_signed",
"format3_signed", "format3_signed_cms", "format3_sealed",
}
type fixture struct {
@ -328,15 +328,16 @@ func TestConformanceFixtures(t *testing.T) {
}
}
// checkSignature3 checks the record of the signature of a format 3 fixture
// against what this implementation computes from the control, the head and
// the security of the fixture (spec v0.11, §29.8, §29.9): the commitments,
// AUTHOR_MESSAGE and its code, the signature, which Ed25519 makes again
// from the seed of the record, and the key. It returns the verdicts in the
// checkSignature3 checks the record of the signature and of the seal of a
// format 3 fixture against what this implementation computes from the
// control, the head and the security of the fixture (spec v0.11, §29.8 to
// §29.11): the commitments, AUTHOR_MESSAGE and its code, the signature, which
// Ed25519 makes again from the seed of the record with alg 1, the key, and
// with alg 2 SIGNERS, the certificates and the result of each signer;
// SEAL_SUBJECT and the token of a seal. It returns the verdicts in the
// context of the capsule.
func checkSignature3(t *testing.T, f *fixture, security, head []byte) capsule.Verdicts {
t.Helper()
s := f.Signature
cb, err := hex.DecodeString(f.ControlCBOR)
if err != nil {
t.Fatal(err)
@ -349,32 +350,92 @@ func checkSignature3(t *testing.T, f *fixture, security, head []byte) capsule.Ve
if err != nil {
t.Fatal(err)
}
hd, sd := capsule.HeadDigest(head), capsule.SignersDigest(capsule.AlgEd25519, nil)
msg := capsule.AuthorMessage(cc, hd, sd)
content, value, err := capsule.SecurityKey2(security)
hd := capsule.HeadDigest(head)
unlock, err := time.Parse(time.RFC3339, f.UnlockAt)
if err != nil {
t.Fatal(err)
}
seed, err := hex.DecodeString(s.SecretSeed)
v := capsule.EvaluateSecurityIn(security, &capsule.SecurityContext{ControlCommit: cc, HeadDigest: hd, RoundTime: unlock})
if s := f.Signature; s != nil {
checkSignatureRecord(t, s, security, cc, hd, v)
}
if s := f.Seal; s != nil {
var part []byte
if content, _, err := capsule.SecurityKey2(security); err == nil {
part = content
}
subject := capsule.SealSubject(cc, hd, capsule.SigPart(part))
typ, token, err := capsule.SecurityKey3(security)
if err != nil {
t.Fatal(err)
}
if s.SealType != capsule.SealTypeRFC3161 || uint64(s.SealType) != typ || s.SealSubject != hex.EncodeToString(subject[:]) || s.Token != hex.EncodeToString(token) ||
v.Detail == nil || s.Holder != v.Detail.SealHolder || s.Time != v.Detail.SealTime.UTC().Format(time.RFC3339) {
t.Fatalf("the record of the seal differs from what is computed: %+v", s)
}
}
return v
}
func checkSignatureRecord(t *testing.T, s *testkit.FixtureSignature, security []byte, cc, hd [32]byte, v capsule.Verdicts) {
t.Helper()
content, value, err := capsule.SecurityKey2(security)
if err != nil {
t.Fatal(err)
}
key, err := authorkey.NewFromSeed(seed)
alg, key, _, err := capsule.DecodeAuthorSignature(content)
if err != nil {
t.Fatal(err)
}
pub, _ := authorkey.PublicString(key.Public())
if s.Alg != capsule.AlgEd25519 || s.AuthorKey != pub || s.ControlCommit != hex.EncodeToString(cc[:]) || s.HeadDigest != hex.EncodeToString(hd[:]) ||
s.SignersDigest != hex.EncodeToString(sd[:]) || s.AuthorMessage != string(msg) || s.AuthorCode != capsule.AuthorCode(msg) ||
s.SignatureValue != hex.EncodeToString(value) || s.SecurityKey2 != hex.EncodeToString(content) ||
hex.EncodeToString(key.Sign(msg)) != s.SignatureValue {
t.Fatalf("the record of the signature differs from what is computed: %+v", s)
if uint64(s.Alg) != alg || s.SignatureValue != hex.EncodeToString(value) || s.SecurityKey2 != hex.EncodeToString(content) ||
s.ControlCommit != hex.EncodeToString(cc[:]) || s.HeadDigest != hex.EncodeToString(hd[:]) {
t.Fatalf("the record of the signature differs from the security area: %+v", s)
}
unlock, err := time.Parse(time.RFC3339, f.UnlockAt)
if err != nil {
t.Fatal(err)
var sd [32]byte
switch alg {
case capsule.AlgEd25519:
sd = capsule.SignersDigest(capsule.AlgEd25519, nil)
seed, err := hex.DecodeString(s.SecretSeed)
if err != nil {
t.Fatal(err)
}
k, err := authorkey.NewFromSeed(seed)
if err != nil {
t.Fatal(err)
}
pub, _ := authorkey.PublicString(k.Public())
msg := capsule.AuthorMessage(cc, hd, sd)
if s.AuthorKey != pub || hex.EncodeToString(k.Sign(msg)) != s.SignatureValue {
t.Fatalf("the key or the signature of the record: %+v", s)
}
case capsule.AlgCMS:
sd = capsule.SignersDigest(capsule.AlgCMS, key)
if s.Signers != hex.EncodeToString(key) || v.Detail == nil ||
!reflect.DeepEqual(s.Results, signerResults(v.Detail.Signers)) || !reflect.DeepEqual(s.Foreign, signerResults(v.Detail.Foreign)) {
t.Fatalf("SIGNERS or the results of the record: %+v", s)
}
if len(s.Certificates) == 0 {
t.Fatal("no certificates in the record")
}
default:
t.Fatalf("alg %d", alg)
}
msg := capsule.AuthorMessage(cc, hd, sd)
if s.SignersDigest != hex.EncodeToString(sd[:]) || s.AuthorMessage != string(msg) || s.AuthorCode != capsule.AuthorCode(msg) {
t.Fatalf("SIGNERS digest or AUTHOR_MESSAGE of the record: %+v", s)
}
}
func signerResults(lines []capsule.SignerLine) []testkit.FixtureSignerResult {
var out []testkit.FixtureSignerResult
for _, l := range lines {
r := testkit.FixtureSignerResult{Holder: l.Holder, Issuer: l.Issuer, Result: l.Result, Before: l.Before}
if !l.SealTime.IsZero() {
r.SealTime = l.SealTime.UTC().Format(time.RFC3339)
}
out = append(out, r)
}
return capsule.EvaluateSecurityIn(security, &capsule.SecurityContext{ControlCommit: cc, HeadDigest: hd, RoundTime: unlock})
return out
}
// checkBody3 checks BODY, the plaintext file of a format 3 fixture, against
@ -399,7 +460,7 @@ func checkBody3(t *testing.T, f *fixture) {
t.Fatal("the area, security, the head or the offset of CONTENT differ")
}
v := capsule.EvaluateSecurity(security)
if f.Signature != nil {
if f.Signature != nil || f.Seal != nil {
v = checkSignature3(t, f, security, hb)
}
if f.Verdicts == nil || string(v.Signature) != f.Verdicts.Signature || string(v.Seal) != f.Verdicts.Seal || !reflect.DeepEqual(v.Lines(), f.Verdicts.Lines) {

@ -160,6 +160,31 @@ func SecurityKey2(security []byte) (content, value []byte, err error) {
return w.signature, a.value, nil
}
// SecurityKey3 returns the seal_type and the token of key 3 of SECURITY_CBOR:
// what a generator of test vectors records about a seal. It fails when
// security has no key 3 or its content does not decode.
func SecurityKey3(security []byte) (sealType uint64, token []byte, err error) {
w, ok := decodeSecurity(security)
if !ok || w.seal == nil {
return 0, nil, errors.New("capsule: SECURITY_CBOR holds no seal")
}
s, err := decodeSeal(w.seal)
if err != nil {
return 0, nil, err
}
return s.sealType, s.token, nil
}
// DecodeAuthorSignature reads the content of key 2 of SECURITY_CBOR: the alg,
// key 1 (the public key of alg 1, SIGNERS of alg 2) and the signature value.
func DecodeAuthorSignature(content []byte) (alg uint64, key, value []byte, err error) {
a, err := decodeAuthorSignature(content)
if err != nil {
return 0, nil, nil, err
}
return a.alg, a.key, a.value, nil
}
// SecurityContext is what the verdicts of a signature or a seal need besides
// SECURITY_CBOR: the commitments of the capsule, the time of its round, and
// the author keys that the person saved, by their dkauthor1… string, with the

@ -134,6 +134,8 @@ func TestDecryptFormat3Fixtures(t *testing.T) {
{"format3_time_and_key_portable", "format3_time_and_key_portable.dkk"},
{"format3_seal_unsupported", ""},
{"format3_signed", ""},
{"format3_signed_cms", ""},
{"format3_sealed", ""},
} {
t.Run(tc.name, func(t *testing.T) {
var f testkit.DKCFixture
@ -476,8 +478,8 @@ func TestInspectJSONGoldens(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if len(names) != len(dkcs) || len(dkcs) != 22 {
t.Fatalf("%d frozen inspect outputs, want one per official .dkc (%d, 22)", len(names), len(dkcs))
if len(names) != len(dkcs) || len(dkcs) != 24 {
t.Fatalf("%d frozen inspect outputs, want one per official .dkc (%d, 24)", len(names), len(dkcs))
}
t.Chdir(fixtures)
for _, path := range names {

@ -0,0 +1,97 @@
package testkit
// CMSVectorFile is testdata/vectors/security_cms.json: SECURITY_CBOR areas
// with an author signature of alg 2 (CMS with certificates) or a time seal
// of seal_type 2 (RFC 3161), each with the context of its capsule and the
// verdicts that spec v0.11 §29.7, §29.10 and §29.11 give. The certificates
// and the tokens are made once, with test keys, and the file is frozen: a
// second implementation reads them and must reach the same verdicts.
type CMSVectorFile struct {
Description string `json:"description"`
Spec string `json:"spec"`
Cases []CMSVectorCase `json:"cases"`
}
// CMSVectorContext is what a verdict needs besides SECURITY_CBOR (§29.7):
// control_commit and head_digest in hexadecimal, and round_time in RFC 3339.
type CMSVectorContext struct {
ControlCommit string `json:"control_commit"`
HeadDigest string `json:"head_digest"`
RoundTime string `json:"round_time"`
}
// CMSVectorCase is one case: the area, its context, and the verdicts.
type CMSVectorCase struct {
Name string `json:"name"`
SecurityCBOR string `json:"security_cbor"`
// NoContext is true when the area is read without the context of a
// capsule, as a reader of v0.10 does: any signature is F1, any seal S1.
NoContext bool `json:"no_context,omitempty"`
Context CMSVectorContext `json:"context"`
Signature string `json:"signature"`
Seal string `json:"seal"`
// Signers and Foreign are the results of an alg 2 signature, in the
// order of SIGNERS; SealHolder and SealTime are those of a valid seal.
Signers []FixtureSignerResult `json:"signers,omitempty"`
Foreign []FixtureSignerResult `json:"foreign_signers,omitempty"`
SealHolder string `json:"seal_holder,omitempty"`
SealTime string `json:"seal_time,omitempty"`
}
// LocatorVectorFile is testdata/vectors/locator.json: the extension
// datekeys.capsule of a .dkk and what it points to (spec v0.11, §44.1). It is
// made once and frozen: the envelope and the sealed locator hold randomness.
type LocatorVectorFile struct {
Description string `json:"description"`
Spec string `json:"spec"`
// Round is the round of the DateKey of the extension, and the locator is
// sealed with tlock for it: it opens with the release of that round.
Round uint64 `json:"round"`
DateKey string `json:"datekey"`
Note string `json:"note"`
// DKC is the capsule that the envelope hides, in hexadecimal.
DKC string `json:"dkc"`
// Rest is what is kept outside, Header what the locator carries, and Host
// a file with the rest appended at HostOffset.
Rest string `json:"rest"`
Header string `json:"envelope_header"`
Host string `json:"host"`
HostOffset uint64 `json:"host_offset"`
// Plaintext is the plaintext of the locator, in 4096 bytes; Sealed, the
// age file with the tlock stanza; Extension, the data of datekeys.capsule.
Plaintext string `json:"locator_plaintext"`
Sealed string `json:"locator_sealed"`
Extension string `json:"extension_data"`
// The fields of the locator, for a reader that compares them.
Addresses []LocatorVectorAddress `json:"addresses"`
EnvelopeKey string `json:"envelope_key"`
RestDigest string `json:"rest_digest"`
RestSize uint64 `json:"rest_size"`
CapsuleDigest string `json:"capsule_digest"`
// PaddingCases give the length of the plaintext for a length of the CBOR
// without key 6, around the boundaries where the CBOR length of key 6
// changes: the least multiple of 4096 that key 6 can fill exactly.
PaddingCases []LocatorPaddingCase `json:"padding_cases"`
// URICases give the verdict of the rules of §44.1 on an address.
URICases []LocatorURICase `json:"uri_cases"`
}
// LocatorVectorAddress is an address of the locator.
type LocatorVectorAddress struct {
URI string `json:"uri"`
Offset uint64 `json:"offset"`
Host string `json:"host"`
}
// LocatorPaddingCase is a length of the CBOR without padding and the length of
// the plaintext that results.
type LocatorPaddingCase struct {
Base int `json:"base"`
Total int `json:"total"`
}
// LocatorURICase is an address and whether it is accepted.
type LocatorURICase struct {
URI string `json:"uri"`
OK bool `json:"ok"`
}

@ -88,7 +88,10 @@ type DKCFixture struct {
// implementation needs to check the signature and to make it again
// (spec v0.11, §29.8, §29.9).
Signature *FixtureSignature `json:"signature,omitempty"`
Stages []FixtureStage `json:"stages"`
// Seal is, in a fixture with a valid time seal, what a second
// implementation needs to check it (spec v0.11, §29.11).
Seal *FixtureSeal `json:"seal,omitempty"`
Stages []FixtureStage `json:"stages"`
}
// FixtureFile is a file of a format 3 fixture: its entry of the head. Its
@ -120,16 +123,47 @@ type FixtureVerdicts struct {
// signed, and AuthorCode its code. SecurityKey2 is the exact content of key
// 2 of SECURITY_CBOR, in hexadecimal.
type FixtureSignature struct {
Alg int `json:"alg"`
SecretSeed string `json:"secret_seed"`
AuthorKey string `json:"author_key"`
ControlCommit string `json:"control_commit"`
HeadDigest string `json:"head_digest"`
SignersDigest string `json:"signers_digest"`
AuthorMessage string `json:"author_message"`
AuthorCode string `json:"author_code"`
Alg int `json:"alg"`
// SecretSeed and AuthorKey are those of an alg 1 signature.
SecretSeed string `json:"secret_seed,omitempty"`
AuthorKey string `json:"author_key,omitempty"`
ControlCommit string `json:"control_commit"`
HeadDigest string `json:"head_digest"`
SignersDigest string `json:"signers_digest"`
AuthorMessage string `json:"author_message"`
AuthorCode string `json:"author_code"`
// SignatureValue is the 64 bytes of alg 1, or the DER of the CMS
// signature of alg 2.
SignatureValue string `json:"signature"`
SecurityKey2 string `json:"security_key_2"`
// Signers is, with alg 2, SIGNERS in hexadecimal, Certificates the DER of
// the certificates inside the signature, and Results what each required
// signer gave, in the order of SIGNERS; Foreign are the others.
Signers string `json:"signers,omitempty"`
Certificates []string `json:"certificates,omitempty"`
Results []FixtureSignerResult `json:"signer_results,omitempty"`
Foreign []FixtureSignerResult `json:"foreign_signers,omitempty"`
}
// FixtureSignerResult is a signer of an alg 2 signature as a reader shows it
// (spec §29.7, §29.10).
type FixtureSignerResult struct {
Holder string `json:"holder"`
Issuer string `json:"issuer"`
Result string `json:"result"`
SealTime string `json:"seal_time,omitempty"`
Before bool `json:"before_round_time"`
}
// FixtureSeal describes the seal of seal_type 2 of a format 3 fixture:
// SEAL_SUBJECT, the token in hexadecimal, the holder of the certificate of
// the authority as §29.7 shows it, and t.
type FixtureSeal struct {
SealType int `json:"seal_type"`
SealSubject string `json:"seal_subject"`
Token string `json:"token"`
Holder string `json:"holder"`
Time string `json:"time"`
}
// FixtureExt is an extension in a fixture.

@ -0,0 +1,71 @@
package main
import (
"crypto/elliptic"
"crypto/sha256"
"time"
"g.activething.com/go/DateKeys/capsule"
"g.activething.com/go/DateKeys/internal/cms/cmstest"
"g.activething.com/go/DateKeys/internal/testkit"
)
// The fixtures with certificates (spec v0.11, §29.10, §29.11) are signed by
// test certificates made when the fixture is generated, so their bytes are
// random and, like those of the other fixtures, frozen once written. The
// private keys are not kept: a reader only verifies.
var (
certFrom = time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)
certTo = time.Date(2040, 1, 1, 0, 0, 0, 0, time.UTC)
)
func sum256(b []byte) [32]byte { return sha256.Sum256(b) }
// certSigner is a CMSSigner that signs as a signing application would, and
// seals each signature with the authority tsa at the writing time.
type certSigner struct {
signers []cmstest.Signer
tsa cmstest.Signer
when time.Time
}
func (c *certSigner) Signers() (out [][32]byte) {
for _, s := range c.signers {
out = append(out, sum256(s.Cert.Raw))
}
return out
}
func (c *certSigner) Sign(message []byte) ([]byte, error) {
return cmstest.Signature(message, cmstest.Options{Token: func(sig []byte) []byte {
return cmstest.Token(sig, c.when, cmstest.TokenOptions{Accuracy: time.Second}, c.tsa)
}}, c.signers...), nil
}
// tokenSealer asks the authority tsa for the token over SEAL_SUBJECT.
type tokenSealer struct {
tsa cmstest.Signer
when time.Time
}
func (s tokenSealer) Seal(subject [32]byte) ([]byte, error) {
return cmstest.Token(subject[:], s.when, cmstest.TokenOptions{Accuracy: time.Second}, s.tsa), nil
}
// configureCMS makes the capsule be signed by two certificates, an ECDSA one
// and an RSA one, each sealed by a time-stamping authority: verdict F6.
func configureCMS(o *capsule.EncryptOptions) error {
ana := cmstest.NewECDSA("Ana López", elliptic.P256(), certFrom, certTo)
luis := cmstest.NewRSA("Luis Gómez", 2048, certFrom, certTo)
tsa := cmstest.NewECDSA("Autoridad de Sellado de prueba", elliptic.P256(), certFrom, certTo)
o.CMSSigner = &certSigner{signers: []cmstest.Signer{ana, luis}, tsa: tsa, when: testkit.Genesis()}
return nil
}
// configureSeal makes the capsule carry a seal of seal_type 2 over the
// signature of alg 1 that its spec gives a key for: verdicts F4 and S4.
func configureSeal(o *capsule.EncryptOptions) error {
tsa := cmstest.NewECDSA("Autoridad de Sellado de prueba", elliptic.P256(), certFrom, certTo)
o.Sealer = tokenSealer{tsa: tsa, when: testkit.Genesis()}
return nil
}

@ -0,0 +1,260 @@
package main
import (
"bytes"
"crypto"
"crypto/elliptic"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"os"
"path/filepath"
"time"
"g.activething.com/go/DateKeys/authorkey"
"g.activething.com/go/DateKeys/capsule"
"g.activething.com/go/DateKeys/datekey"
"g.activething.com/go/DateKeys/extension"
"g.activething.com/go/DateKeys/internal/cms/cmstest"
"g.activething.com/go/DateKeys/internal/testkit"
"g.activething.com/go/DateKeys/locator"
"g.activething.com/go/DateKeys/profile"
)
// frozenVectors writes testdata/vectors/security_cms.json and locator.json
// when they are missing: their bytes hold the randomness of certificates, of
// age and of tlock, so they are made once and kept, as the fixtures are.
// Delete a file to make it again.
func frozenVectors(dir string) error {
for name, gen := range map[string]func() (any, error){
"security_cms.json": securityCMSVectors,
"locator.json": locatorVectors,
} {
path := filepath.Join(dir, name)
if _, err := os.Stat(path); err == nil {
continue
}
v, err := gen()
if err != nil {
return fmt.Errorf("%s: %w", name, err)
}
if err := testkit.WriteJSON(path, v); err != nil {
return err
}
}
return nil
}
var (
vecRound = time.Date(2030, 1, 1, 0, 0, 0, 0, time.UTC)
vecSigned = time.Date(2026, 9, 30, 12, 0, 0, 0, time.UTC)
)
func hex32(b [32]byte) string { return hex.EncodeToString(b[:]) }
// cmsArea builds SECURITY_CBOR with an alg 2 signature by the signers over
// the context c, sealed by tsa at when; required lists who must sign.
func cmsArea(c *capsule.SecurityContext, required, signers []cmstest.Signer, tsa cmstest.Signer, when time.Time, seal []byte) ([]byte, error) {
var hashes [][32]byte
for _, s := range required {
hashes = append(hashes, sum256(s.Cert.Raw))
}
list, err := capsule.EncodeSigners(hashes)
if err != nil {
return nil, err
}
msg := capsule.AuthorMessage(c.ControlCommit, c.HeadDigest, capsule.SignersDigest(capsule.AlgCMS, list))
opts := cmstest.Options{}
if tsa.Key != nil {
opts.Token = func(sig []byte) []byte {
return cmstest.Token(sig, when, cmstest.TokenOptions{Accuracy: time.Second}, tsa)
}
}
content, err := capsule.EncodeAuthorSignature(capsule.AlgCMS, list, cmstest.Signature(msg, opts, signers...))
if err != nil {
return nil, err
}
return capsule.EncodeSecurityWith(content, seal)
}
func securityCMSVectors() (any, error) {
ana := cmstest.NewECDSA("Ana López", elliptic.P256(), certFrom, certTo)
luis := cmstest.NewRSA("Luis Gómez", 2048, certFrom, certTo)
otro := cmstest.NewECDSA("Otro", elliptic.P384(), certFrom, certTo)
tsa := cmstest.NewECDSA("Autoridad de Sellado de prueba", elliptic.P256(), certFrom, certTo)
ctx := func() *capsule.SecurityContext {
return &capsule.SecurityContext{ControlCommit: sha256.Sum256([]byte("control")), HeadDigest: sha256.Sum256([]byte("head")), RoundTime: vecRound}
}
key, err := authorkey.NewFromSeed(bytes.Repeat([]byte{7}, 32))
if err != nil {
return nil, err
}
f := testkit.CMSVectorFile{
Spec: testkit.SpecVersion,
Description: "SECURITY_CBOR with an author signature of alg 2 or a time seal of seal_type 2, its context and the verdicts of spec v0.11 29.7, 29.10 and 29.11. " +
"Certificates and tokens are made once with test keys and the file is frozen. See testdata/README.md.",
}
var errs []error
add := func(name string, area []byte, c *capsule.SecurityContext, wantSig, wantSeal capsule.Verdict) {
var v capsule.Verdicts
vc := testkit.CMSVectorCase{Name: name, SecurityCBOR: hex.EncodeToString(area)}
if c == nil {
v, vc.NoContext = capsule.EvaluateSecurity(area), true
c = ctx()
} else {
v = capsule.EvaluateSecurityIn(area, c)
}
vc.Context = testkit.CMSVectorContext{ControlCommit: hex32(c.ControlCommit), HeadDigest: hex32(c.HeadDigest), RoundTime: c.RoundTime.UTC().Format(time.RFC3339)}
vc.Signature, vc.Seal = string(v.Signature), string(v.Seal)
if v.Signature != wantSig || v.Seal != wantSeal {
errs = append(errs, fmt.Errorf("%s: verdicts %s and %s, want %s and %s", name, v.Signature, v.Seal, wantSig, wantSeal))
}
if d := v.Detail; d != nil {
vc.Signers, vc.Foreign = signerResults(d.Signers), signerResults(d.Foreign)
if !d.SealTime.IsZero() {
vc.SealHolder, vc.SealTime = d.SealHolder, d.SealTime.UTC().Format(time.RFC3339)
}
}
f.Cases = append(f.Cases, vc)
}
must := func(b []byte, err error) []byte {
if err != nil {
errs = append(errs, err)
}
return b
}
both := []cmstest.Signer{ana, luis}
c := ctx()
add("alg 2: two signers, each sealed before the round time", must(cmsArea(c, both, both, tsa, vecSigned, nil)), c, capsule.VerdictSignedComplete, capsule.VerdictNoSeal)
add("alg 2: a seal after the round time proves nothing before it", must(cmsArea(c, []cmstest.Signer{ana}, []cmstest.Signer{ana}, tsa, vecRound.Add(time.Hour), nil)), c, capsule.VerdictSignedComplete, capsule.VerdictNoSeal)
add("alg 2: a signer who is not required shows apart", must(cmsArea(c, []cmstest.Signer{ana}, []cmstest.Signer{ana, otro}, tsa, vecSigned, nil)), c, capsule.VerdictSignedComplete, capsule.VerdictNoSeal)
add("alg 2: a required signer is absent", must(cmsArea(c, both, []cmstest.Signer{ana}, tsa, vecSigned, nil)), c, capsule.VerdictSignedIncomplete, capsule.VerdictNoSeal)
add("alg 2: no seal", must(cmsArea(c, []cmstest.Signer{ana}, []cmstest.Signer{ana}, cmstest.Signer{}, vecSigned, nil)), c, capsule.VerdictSignedIncomplete, capsule.VerdictNoSeal)
add("alg 2: a seal from before the certificate was valid", must(cmsArea(c, []cmstest.Signer{ana}, []cmstest.Signer{ana}, tsa, certFrom.AddDate(-1, 0, 0), nil)), c, capsule.VerdictSignedIncomplete, capsule.VerdictNoSeal)
add("alg 2: a key 3 beside it", must(cmsArea(c, []cmstest.Signer{ana}, []cmstest.Signer{ana}, tsa, vecSigned, must(capsule.EncodeSeal(1, []byte{1})))), c, capsule.VerdictSignedIncomplete, capsule.VerdictSealUnsupported)
other := ctx()
other.HeadDigest[5] ^= 9
add("alg 2: another head", must(cmsArea(c, []cmstest.Signer{ana}, []cmstest.Signer{ana}, tsa, vecSigned, nil)), other, capsule.VerdictSignatureInvalid, capsule.VerdictNoSeal)
add("alg 2: without the context of a capsule", must(cmsArea(c, []cmstest.Signer{ana}, []cmstest.Signer{ana}, tsa, vecSigned, nil)), nil, capsule.VerdictSignatureUnchecked, capsule.VerdictNoSeal)
one, two := sum256([]byte("a")), sum256([]byte("b"))
if bytes.Compare(one[:], two[:]) > 0 {
one, two = two, one
}
unsorted := append(append([]byte{0x82, 0x58, 0x20}, two[:]...), append([]byte{0x58, 0x20}, one[:]...)...)
for name, signers := range map[string][]byte{"alg 2: SIGNERS out of order": unsorted, "alg 2: an empty SIGNERS": {0x80}} {
content, _ := capsule.EncodeAuthorSignature(capsule.AlgCMS, signers, []byte{0x30, 0x00})
add(name, must(capsule.EncodeSecurityWith(content, nil)), c, capsule.VerdictSignatureUnchecked, capsule.VerdictNoSeal)
}
{
list, _ := capsule.EncodeSigners([][32]byte{sum256(ana.Cert.Raw)})
content, _ := capsule.EncodeAuthorSignature(capsule.AlgCMS, list, []byte("not DER"))
add("alg 2: not a CMS", must(capsule.EncodeSecurityWith(content, nil)), c, capsule.VerdictSignatureUnchecked, capsule.VerdictNoSeal)
}
// Seals of seal_type 2 over an alg 1 signature.
msg := capsule.AuthorMessage(c.ControlCommit, c.HeadDigest, capsule.SignersDigest(capsule.AlgEd25519, nil))
sig := must(capsule.EncodeAuthorSignature(capsule.AlgEd25519, key.Public(), key.Sign(msg)))
subject := capsule.SealSubject(c.ControlCommit, c.HeadDigest, capsule.SigPart(sig))
sealed := func(token []byte) []byte {
return must(capsule.EncodeSecurityWith(sig, must(capsule.EncodeSeal(capsule.SealTypeRFC3161, token))))
}
tok := func(subject []byte, when time.Time, o cmstest.TokenOptions, s cmstest.Signer) []byte {
return cmstest.Token(subject, when, o, s)
}
okSig := capsule.VerdictSignedOther
add("seal: before the round time", sealed(tok(subject[:], vecSigned, cmstest.TokenOptions{}, tsa)), c, okSig, capsule.VerdictSealed)
add("seal: after the round time", sealed(tok(subject[:], vecRound.Add(time.Minute), cmstest.TokenOptions{}, tsa)), c, okSig, capsule.VerdictSealedLate)
add("seal: the accuracy reaches the round time", sealed(tok(subject[:], vecRound.Add(-time.Second), cmstest.TokenOptions{Accuracy: 2 * time.Second}, tsa)), c, okSig, capsule.VerdictSealedLate)
add("seal: over another subject", sealed(tok([]byte("other"), vecSigned, cmstest.TokenOptions{}, tsa)), c, okSig, capsule.VerdictSealInvalid)
add("seal: the authority had expired at its time", sealed(tok(subject[:], certTo.AddDate(1, 0, 0), cmstest.TokenOptions{}, tsa)), c, okSig, capsule.VerdictSealInvalid)
add("seal: a TSTInfo of version 2", sealed(tok(subject[:], vecSigned, cmstest.TokenOptions{Version: 2}, tsa)), c, okSig, capsule.VerdictSealUnreadable)
add("seal: not DER", sealed([]byte("not DER")), c, okSig, capsule.VerdictSealUnreadable)
add("seal: SHA-384 in the imprint", sealed(tok(subject[:], vecSigned, cmstest.TokenOptions{Hash: crypto.SHA384}, tsa)), c, okSig, capsule.VerdictSealUnsupported)
add("seal: without a context, as a reader of v0.10", sealed(tok(subject[:], vecSigned, cmstest.TokenOptions{}, tsa)), nil, capsule.VerdictSignatureUnchecked, capsule.VerdictSealUnsupported)
noSig := capsule.SealSubject(c.ControlCommit, c.HeadDigest, capsule.SigPart(nil))
add("seal: over a capsule without a signature", must(capsule.EncodeSecurityWith(nil, must(capsule.EncodeSeal(capsule.SealTypeRFC3161, tok(noSig[:], vecSigned, cmstest.TokenOptions{}, tsa))))),
c, capsule.VerdictNoSignature, capsule.VerdictSealed)
if err := errors.Join(errs...); err != nil {
return nil, err
}
return f, nil
}
// locatorVectors makes the vector of the extension datekeys.capsule: a .dkc of
// patterned bytes in an envelope, hidden in a host, its locator sealed with
// tlock for round 1000, and the data of the extension with a note.
func locatorVectors() (any, error) {
p := profile.Quicknet()
dkc := patterned("locator dkc", 5000)
loc, rest, err := locator.NewEnvelope(dkc)
if err != nil {
return nil, err
}
host := patterned("host file", 3000)
file, offset := locator.Hide(host, rest)
const cid = "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi"
loc.Addresses = []locator.Address{{URI: "https://ejemplo.org/foto.jpg", Offset: offset}, {URI: "ipfs://" + cid}}
plain, err := loc.Marshal()
if err != nil {
return nil, err
}
const round = 1000
sealed, err := locator.Seal(p, round, loc)
if err != nil {
return nil, err
}
dk, err := datekey.Resolve(p, mustRoundTime(p, round))
if err != nil {
return nil, err
}
const note = "Cartas del viaje a Lisboa"
x, err := (&locator.Info{Note: note, DateKey: dk, Sealed: sealed}).Extension()
if err != nil {
return nil, err
}
if x.ID != extension.CapsuleID {
return nil, errors.New("not datekeys.capsule")
}
v := testkit.LocatorVectorFile{
Spec: testkit.SpecVersion,
Description: "The extension datekeys.capsule of a .dkk and what it points to (spec v0.11, 44.1): an envelope of age with its header apart from its rest, " +
"the rest hidden in a host file, the locator sealed with tlock for round 1000, and the data of the extension. Frozen. See testdata/README.md.",
Round: round, DateKey: dk.Compact(), Note: note, DKC: hex.EncodeToString(dkc), Rest: hex.EncodeToString(rest), Header: hex.EncodeToString(loc.EnvelopeHeader),
Host: hex.EncodeToString(file), HostOffset: offset, Plaintext: hex.EncodeToString(plain), Sealed: hex.EncodeToString(sealed), Extension: hex.EncodeToString(x.Data),
EnvelopeKey: hex.EncodeToString(loc.EnvelopeKey[:]), RestDigest: hex32(loc.RestDigest), RestSize: loc.RestSize, CapsuleDigest: hex32(loc.CapsuleDigest),
}
for _, a := range loc.Addresses {
v.Addresses = append(v.Addresses, testkit.LocatorVectorAddress{URI: a.URI, Offset: a.Offset, Host: a.Host()})
}
for _, base := range []int{100, 3000, 4000, 4060, 4066, 4067, 4068, 4069, 4070, 4071, 4072, 4090, 4094, 4095, 4096, 4097, 4100, 8160, 8190, 8192, 8193, 12000} {
v.PaddingCases = append(v.PaddingCases, testkit.LocatorPaddingCase{Base: base, Total: locator.PlaintextLength(base)})
}
for _, c := range []struct {
uri string
ok bool
}{
{"https://ejemplo.org/a.bin", true}, {"https://ejemplo.org:8443/x?y=1", true}, {"ipfs://" + cid, true}, {"ipfs://" + cid + "/ruta", true},
{"", false}, {"http://ejemplo.org/a", false}, {"file:///etc/passwd", false}, {"ftp://x/y", false}, {"https://user:pass@ejemplo.org/", false},
{"https://", false}, {"ipfs://notacid", false}, {"https://ejemplo.org/ñ", false}, {"https://ejemplo.org/a b", false},
{"https://%D0%B0pple.com/x", false}, {"https://ejemplo.org%E2%80%AEtxt.exe/", false}, {"https://127.0.0.1/", false}, {"https://[::1]/", false},
{"https://0x7f000001/", false}, {"https://a.com:99999999/", false}, {"https://a.com:0/", false}, {"https://xn--pple-43d.com/", true},
} {
if (locator.CheckURI(c.uri) == nil) != c.ok {
return nil, fmt.Errorf("the address %q: accepted %v, want %v", c.uri, !c.ok, c.ok)
}
v.URICases = append(v.URICases, testkit.LocatorURICase{URI: c.uri, OK: c.ok})
}
return v, nil
}
func mustRoundTime(p *profile.Profile, round uint64) time.Time {
t, err := datekey.RoundTime(p, round)
if err != nil {
panic(err)
}
return t
}

@ -16,6 +16,7 @@ import (
"g.activething.com/go/DateKeys/capsule"
"g.activething.com/go/DateKeys/codec"
"g.activething.com/go/DateKeys/codec/bech32"
"g.activething.com/go/DateKeys/internal/cms"
"g.activething.com/go/DateKeys/internal/testkit"
)
@ -163,50 +164,118 @@ func record3(f *testkit.DKCFixture, body []byte, verdicts *capsule.Verdicts) err
if f.Signature != nil {
f.Verdicts.AuthorKey = f.Signature.AuthorKey
}
return nil
f.Seal, err = recordSeal(f, security, hb, v)
return err
}
// recordSignature returns what the record of a fixture says about its
// signature of alg 1: the seed of its test key, which the generator wrote and
// this keeps, and the commitments and the message, which it computes from the
// control of the fixture, its head and its security (spec v0.11, §29.8). Nil
// when the fixture has no valid signature.
func recordSignature(f *testkit.DKCFixture, security, head []byte, v capsule.Verdicts) (*testkit.FixtureSignature, error) {
if v.Signature != capsule.VerdictSignedOther && v.Signature != capsule.VerdictSignedSaved {
return nil, nil
}
if f.Signature == nil {
return nil, errors.New("the fixture has a valid signature and its record no seed of the key")
}
// commitmentsOf returns control_commit and head_digest of the fixture f, from
// its control and its head (spec v0.11, §29.8).
func commitmentsOf(f *testkit.DKCFixture, head []byte) (cc, hd [32]byte, err error) {
cb, err := hex.DecodeString(f.ControlCBOR)
if err != nil {
return nil, err
return cc, hd, err
}
c, err := capsule.DecodeControl(cb, capsule.Format(f.Format))
if err != nil {
return nil, err
return cc, hd, err
}
defer clear(c.PayloadIdentity[:])
cc, err := capsule.ControlCommit(c, capsule.Format(f.Format))
if cc, err = capsule.ControlCommit(c, capsule.Format(f.Format)); err != nil {
return cc, hd, err
}
return cc, capsule.HeadDigest(head), nil
}
func signerResults(lines []capsule.SignerLine) []testkit.FixtureSignerResult {
var out []testkit.FixtureSignerResult
for _, l := range lines {
r := testkit.FixtureSignerResult{Holder: l.Holder, Issuer: l.Issuer, Result: l.Result, Before: l.Before}
if !l.SealTime.IsZero() {
r.SealTime = l.SealTime.UTC().Format(time.RFC3339)
}
out = append(out, r)
}
return out
}
// recordSignature returns what the record of a fixture says about its
// signature: with alg 1, the seed of its test key, which the generator wrote
// and this keeps, and the commitments and the message, which it computes from
// the control of the fixture, its head and its security (spec v0.11, §29.8);
// with alg 2, the same without a seed, and the certificates, SIGNERS and the
// result of each signer that Open gave. Nil when the fixture has no valid
// signature of alg 1 and no signature of alg 2 that Open judged.
func recordSignature(f *testkit.DKCFixture, security, head []byte, v capsule.Verdicts) (*testkit.FixtureSignature, error) {
content, value, err := capsule.SecurityKey2(security)
if err != nil {
return nil, nil // no signature
}
alg, key, _, err := capsule.DecodeAuthorSignature(content)
if err != nil {
return nil, nil
}
switch {
case alg == capsule.AlgEd25519 && (v.Signature == capsule.VerdictSignedOther || v.Signature == capsule.VerdictSignedSaved):
if f.Signature == nil {
return nil, errors.New("the fixture has a valid signature and its record no seed of the key")
}
case alg == capsule.AlgCMS && v.Detail != nil:
default:
return nil, nil
}
cc, hd, err := commitmentsOf(f, head)
if err != nil {
return nil, err
}
hd := capsule.HeadDigest(head)
sd := capsule.SignersDigest(capsule.AlgEd25519, nil)
var sd [32]byte
sig := &testkit.FixtureSignature{Alg: int(alg), SignatureValue: hex.EncodeToString(value), SecurityKey2: hex.EncodeToString(content)}
if alg == capsule.AlgEd25519 {
sd = capsule.SignersDigest(capsule.AlgEd25519, nil)
sig.SecretSeed = f.Signature.SecretSeed
if sig.AuthorKey, err = bech32.Encode("dkauthor", v.AuthorKey[:]); err != nil {
return nil, err
}
} else {
sd = capsule.SignersDigest(capsule.AlgCMS, key)
sig.Signers = hex.EncodeToString(key)
parsed, err := cms.ParseSignature(value)
if err != nil {
return nil, err
}
for _, c := range parsed.Certs {
sig.Certificates = append(sig.Certificates, hex.EncodeToString(c.Raw))
}
sig.Results, sig.Foreign = signerResults(v.Detail.Signers), signerResults(v.Detail.Foreign)
}
msg := capsule.AuthorMessage(cc, hd, sd)
content, value, err := capsule.SecurityKey2(security)
sig.ControlCommit, sig.HeadDigest, sig.SignersDigest = hex.EncodeToString(cc[:]), hex.EncodeToString(hd[:]), hex.EncodeToString(sd[:])
sig.AuthorMessage, sig.AuthorCode = string(msg), capsule.AuthorCode(msg)
return sig, nil
}
// recordSeal returns what the record of a fixture says about its seal of
// seal_type 2 when Open found it valid (S4 or S5): SEAL_SUBJECT, the token
// and the authority and the time that the verdict names.
func recordSeal(f *testkit.DKCFixture, security, head []byte, v capsule.Verdicts) (*testkit.FixtureSeal, error) {
if (v.Seal != capsule.VerdictSealed && v.Seal != capsule.VerdictSealedLate) || v.Detail == nil {
return nil, nil
}
typ, token, err := capsule.SecurityKey3(security)
if err != nil {
return nil, err
}
key, err := bech32.Encode("dkauthor", v.AuthorKey[:])
cc, hd, err := commitmentsOf(f, head)
if err != nil {
return nil, err
}
return &testkit.FixtureSignature{
Alg: capsule.AlgEd25519, SecretSeed: f.Signature.SecretSeed, AuthorKey: key,
ControlCommit: hex.EncodeToString(cc[:]), HeadDigest: hex.EncodeToString(hd[:]), SignersDigest: hex.EncodeToString(sd[:]),
AuthorMessage: string(msg), AuthorCode: capsule.AuthorCode(msg),
SignatureValue: hex.EncodeToString(value), SecurityKey2: hex.EncodeToString(content),
var part []byte
if content, _, err := capsule.SecurityKey2(security); err == nil {
part = content
}
subject := capsule.SealSubject(cc, hd, capsule.SigPart(part))
return &testkit.FixtureSeal{
SealType: int(typ), SealSubject: hex.EncodeToString(subject[:]), Token: hex.EncodeToString(token),
Holder: v.Detail.SealHolder, Time: v.Detail.SealTime.UTC().Format(time.RFC3339),
}, nil
}

@ -188,7 +188,7 @@ func format3Vectors(dir string) error {
return err
}
}
return nil
return frozenVectors(dir)
}
// mutationsName is the -only name that rebuilds the capsules of the
@ -372,7 +372,10 @@ type spec struct {
// signer, when not nil, is the 32-byte seed of the author key that signs
// a format 3 fixture written by EncryptFiles (spec v0.11, §29.9).
signer []byte
body func() ([]byte, error)
// configure, when not nil, sets what only this fixture needs in the
// options of EncryptFiles: a signer with certificates, or a sealer.
configure func(o *capsule.EncryptOptions) error
body func() ([]byte, error)
}
// Extension data of the fixtures (spec §54, §72): the header carries the raw
@ -482,6 +485,8 @@ func specs() []spec {
return body3(capsule.AreaUnit, s, "", "", note)
}},
{name: "format3_signed", format: f3, description: "format 3 time_only capsule with a single file, nota.txt, signed with alg 1 by a test key whose seed the record gives: verdict F4, and the commitments and the message of the signature", round: 1000, policy: capsule.TimeOnly, padding: capsule.Reforzado, files: note, signer: signerSeed},
{name: "format3_signed_cms", format: f3, description: "format 3 time_only capsule with a single file, nota.txt, signed with alg 2 by two test certificates, an ECDSA P-256 one and an RSA 2048 one, each sealed by a test time-stamping authority before the round time: verdict F6, with the certificates, the commitments, SIGNERS and the result of each signer in the record", round: 1000, policy: capsule.TimeOnly, padding: capsule.Reforzado, files: note, configure: configureCMS},
{name: "format3_sealed", format: f3, description: "format 3 time_only capsule with a single file, nota.txt, signed with alg 1 by the test key of format3_signed and sealed with seal_type 2 by a test time-stamping authority before the round time: verdicts F4 and S4, with SEAL_SUBJECT and the token in the record", round: 1000, policy: capsule.TimeOnly, padding: capsule.Reforzado, files: note, signer: signerSeed, configure: configureSeal},
}
}
@ -629,6 +634,11 @@ func write(s spec) (*written, error) {
var res *capsule.Result
if s.format == capsule.Format3 {
opts.Comment, opts.Author = s.comment, s.author
if s.configure != nil {
if err := s.configure(&opts); err != nil {
return nil, err
}
}
if s.signer != nil {
k, err := authorkey.NewFromSeed(s.signer)
if err != nil {

@ -441,6 +441,17 @@ func padFor(n0 int) int {
}
}
// PlaintextLength returns the length of the plaintext of a locator whose CBOR
// without key 6 measures base bytes: base when it already is a multiple of
// Block, and otherwise the least multiple that key 6 can fill exactly.
func PlaintextLength(base int) int {
pad := padFor(base)
if pad < 0 {
return base
}
return base + 1 + bstrHeadLen(pad) + pad
}
// Marshal returns the plaintext of the locator: CBOR with the profile of
// spec §58, completed with zeros in key 6 up to the least multiple of 4096
// bytes that holds it, so that its length does not tell how many addresses

@ -0,0 +1,76 @@
package locator_test
import (
"bytes"
"encoding/hex"
"path/filepath"
"testing"
"g.activething.com/go/DateKeys/extension"
"g.activething.com/go/DateKeys/internal/testkit"
"g.activething.com/go/DateKeys/locator"
"g.activething.com/go/DateKeys/profile"
)
func unhex(t *testing.T, s string) []byte {
t.Helper()
b, err := hex.DecodeString(s)
if err != nil {
t.Fatal(err)
}
return b
}
// testdata/vectors/locator.json is frozen: the extension of the .dkk reads,
// the sealed locator opens with the release of its round, the rest is found
// in the host at its offset, the envelope gives the .dkc back, and the rules
// of the addresses and of the padding give what the file says (spec v0.11,
// §44.1).
func TestLocatorVectors(t *testing.T) {
var v testkit.LocatorVectorFile
if err := testkit.ReadJSON(filepath.Join("..", "testdata", "vectors", "locator.json"), &v); err != nil {
t.Fatal(err)
}
p := profile.Quicknet()
info, err := locator.ParseInfo(extension.Extension{ID: extension.CapsuleID, Version: 1, Data: unhex(t, v.Extension)})
if err != nil || info.Note != v.Note || info.DateKey.Compact() != v.DateKey || info.DateKey.Round != v.Round || !bytes.Equal(info.Sealed, unhex(t, v.Sealed)) {
t.Fatalf("the extension: %+v, %v", info, err)
}
loc, err := locator.Open(p, v.Round, testkit.Release(v.Round), unhex(t, v.Sealed))
if err != nil {
t.Fatal(err)
}
plain, err := loc.Marshal()
if err != nil || !bytes.Equal(plain, unhex(t, v.Plaintext)) || len(plain) != locator.Block {
t.Fatalf("the plaintext of the locator: %d bytes, %v", len(plain), err)
}
if hex.EncodeToString(loc.EnvelopeKey[:]) != v.EnvelopeKey || hex.EncodeToString(loc.RestDigest[:]) != v.RestDigest || loc.RestSize != v.RestSize ||
hex.EncodeToString(loc.CapsuleDigest[:]) != v.CapsuleDigest || hex.EncodeToString(loc.EnvelopeHeader) != v.Header || len(loc.Addresses) != len(v.Addresses) {
t.Fatalf("the fields of the locator: %+v", loc)
}
for i, a := range v.Addresses {
if loc.Addresses[i].URI != a.URI || loc.Addresses[i].Offset != a.Offset || loc.Addresses[i].Host() != a.Host {
t.Errorf("address %d: %+v", i, loc.Addresses[i])
}
}
// The rest, from the host at the offset of the first address, opens the envelope.
rest, err := loc.RestIn(unhex(t, v.Host), v.HostOffset)
if err != nil || !bytes.Equal(rest, unhex(t, v.Rest)) {
t.Fatalf("the rest in the host: %v", err)
}
dkc, err := loc.OpenEnvelope(rest)
if err != nil || !bytes.Equal(dkc, unhex(t, v.DKC)) {
t.Fatalf("the envelope: %v", err)
}
for _, c := range v.PaddingCases {
if got := locator.PlaintextLength(c.Base); got != c.Total || got%locator.Block != 0 {
t.Errorf("padding of %d: %d, want %d", c.Base, got, c.Total)
}
}
for _, c := range v.URICases {
if got := locator.CheckURI(c.URI) == nil; got != c.OK {
t.Errorf("%q: accepted %v, want %v", c.URI, got, c.OK)
}
}
}

95
testdata/README.md vendored

@ -73,7 +73,7 @@ extension and a noncritical CONTROL_CBOR extension. The release that opens each
capsule, a published Quicknet signature, is in its `<name>.json`, so they all
decrypt offline.
Nine are in format 3. Their plaintext file is BODY, L bytes: the frame, the
Twelve are in format 3. Their plaintext file is BODY, L bytes: the frame, the
security area, the head and the files (spec §29.2).
| Fixture | Policy | Files | Comment | L | Padding code | P | Area | Verdicts |
@ -87,14 +87,45 @@ security area, the head and the files (spec §29.2).
| `format3_security_v2` | `time_only` | 1 | — | 659 | 2 | 768 | 512 | X |
| `format3_signature_unsupported` | `time_only` | 1 | — | 659 | 2 | 768 | 512 | F1, S0 |
| `format3_seal_unsupported` | `time_only` | 1 | — | 659 | 2 | 768 | 512 | F1, S1 |
| `format3_signed` | `time_only` | 1, `nota.txt`, with mtime | — | 32915 | 2 | 34816 | 32768 | F4, S0 |
| `format3_signed_cms` | `time_only` | 1, `nota.txt`, with mtime | — | 32915 | 2 | 34816 | 32768 | F6, S0 |
| `format3_sealed` | `time_only` | 1, `nota.txt`, with mtime | — | 32915 | 2 | 34816 | 32768 | F4, S4 |
The first five are what `capsule.EncryptFiles` writes. The other four only a
The first five were written by a writer of v0.10, with the area of 512 bytes;
a writer of v0.11 writes the area of 32768 bytes, as in the last three, which
`capsule.EncryptFiles` writes with a signer and a sealer. The next four only a
generator of test vectors may write (spec §62.1 rule 13): an area larger than
the 512 bytes of this version, which a reader accepts, a security map of
version 2, which a reader of this version cannot read, an author signature of
`alg` 1 with a random key of 32 bytes and a random signature of 64, and that
512 bytes, a security map of version 2, which a reader of this version cannot
read, an author signature of `alg` 4294967295, an `alg` that no version
defines, with a random key of 32 bytes and a random signature of 64, and that
with a seal of `seal_type` 1 and a random token of 32 bytes. None of them has
a verdict that stops the opening. The BODY of `format3_tree` is over two STREAM
a verdict that stops the opening.
The last three are signed and sealed with test keys (spec v0.11, §29.8 to
§29.11), and their record has a `signature` object, and `seal` in the third:
- `format3_signed`: `alg` 1. The seed of the test key, which is not a secret,
is in `secret_seed`: Ed25519 is deterministic, so signing `author_message`
with it gives `signature` again. Opening it gives F4 and the key `author_key`,
`dkauthor1…`; with that key among the saved ones, F3.
- `format3_signed_cms`: `alg` 2, two certificates, an ECDSA P-256 one and an
RSA 2048 one, each with a seal CAdES-T from a test authority, dated before
the round time. The record has `signers` (SIGNERS in hexadecimal),
`certificates` (the DER of each), `signature` (the DER of the CMS signature)
and `signer_results`, one for each required signer in the order of SIGNERS:
holder, issuer that the certificate says, result, seal time, and whether the
seal, with its accuracy, precedes the round time. The private keys are not
kept: ECDSA and RSA-PSS are not deterministic, and a reader only verifies.
- `format3_sealed`: `alg` 1 as the first, and a seal of `seal_type` 2 over
`SEAL_SUBJECT`. The record has `seal`: `seal_subject`, the `token` in
hexadecimal, the `holder` of the authority as §29.7 shows it, and the time.
In the three, the record gives the commitments `control_commit`, `head_digest`
and `signers_digest`, the text `author_message` and its `author_code`, and the
exact content of key 2 of `SECURITY_CBOR`. An implementation checks them from
the control, the head and the security area of the fixture, and the verdicts
from `verdicts`. The certificates and the tokens are random, so these fixtures
are frozen once written like the others. The BODY of `format3_tree` is over two STREAM
chunks, with its head in the first.
## Edited files
@ -381,6 +412,58 @@ signature and the seal are evaluated apart, and the first row of the table of
give F1 with the seal intact, and a seal that breaks its schema gives S2 even
with an unknown `seal_type`, which is read only from a seal that meets it.
## `vectors/security_cms.json`
Security areas with an author signature of `alg` 2, a CMS signature with
certificates, or a time seal of `seal_type` 2, an RFC 3161 token, each with
the context of its capsule and the verdicts of spec v0.11 §29.7, §29.10 and
§29.11. They complete `security.json`, whose areas have no valid signature or
seal. The file is frozen: the certificates and the tokens are made once, with
test keys, so a second implementation reads them and must reach the same
verdicts. Delete the file to make it again.
```json
{ "name": "alg 2: a required signer is absent", "security_cbor": "a4…",
"context": { "control_commit": "…", "head_digest": "…", "round_time": "2030-01-01T00:00:00Z" },
"signature": "F5", "seal": "S0", "signers": [ { "holder": "Ana López", "result": "valid", … }, { "holder": "<sha256>", "result": "absent", … } ] }
```
`context` is what a verdict needs besides `SECURITY_CBOR`; with `no_context`
the area is read as a reader of v0.10 does, without a capsule, and any
signature is F1 and any seal S1. `signers` are the results of the required
signers in the order of SIGNERS, and `foreign_signers` those that are not
required and never count. A valid seal gives `seal_holder` and `seal_time`.
The cases cover F6 with two signers, with a seal after the round time and
with a signer who is not required; F5 for an absent signer, no seal, a seal
from before the certificate was valid and a key 3 beside the signature; F2 in
the context of another head; F1 for SIGNERS out of order or empty, a signature
that is not a CMS, and the lack of a context; and, over an `alg` 1 signature,
the seals S4, S5 (also when the accuracy reaches the round time), S3 (another
subject, an authority expired at its time), S2 (a TSTInfo of version 2, not
DER), S1 (a SHA-384 imprint) and a seal over a capsule without a signature.
## `vectors/locator.json`
The extension `datekeys.capsule` of a `.dkk` and what it points to (spec
v0.11, §44.1): a `.dkc` of patterned bytes in an envelope of age whose header
(`envelope_header`) goes in the locator and whose `rest`, without a mark, is
hidden in a `host` file at `host_offset`; the locator sealed with tlock for
round 1000 (`locator_sealed`), with its plaintext of 4096 bytes
(`locator_plaintext`) and its fields; and the data of the extension
(`extension_data`), with the note `note` and the DateKey `datekey`. A reader
opens the locator with the release of round 1000 (`quicknet_rounds.json`),
finds the rest in the host, checks `rest_size`, `rest_digest` and
`capsule_digest`, and gets the `.dkc` back. The file is frozen: the envelope
and the locator hold randomness.
`padding_cases` give the length of the plaintext of the locator for the length
of its CBOR without the padding of key 6: the least multiple of 4096 that key 6
can fill exactly, which skips a multiple where the CBOR length of key 6 jumps
(a base of 4070 gives 8192). `uri_cases` give the verdict of the rules of §44.1
on an address: the scheme `https` or `ipfs`, the raw ASCII authority with no
percent sign or userinfo, a host of letters, digits and hyphens or a public IP
literal, and a port from 1 to 65535.
## `vectors/ed25519_strict.json`
Ed25519 signatures, in hexadecimal, and whether the strict profile of the

Binary file not shown.

@ -0,0 +1,61 @@
{
"file": "format3_sealed.dkc",
"format": 3,
"capsule_id": "b3d25bb6c74cda4e014c65e3d2f43a69",
"datekey": "dk1_eyJ2ZXJzaW9uIjoxLCJuZXR3b3JrIjoiZGF0ZWtleXM6cXVpY2tuZXQ6djEiLCJyb3VuZCI6MTAwMH0",
"profile": "datekeys:quicknet:v1",
"round": 1000,
"unlock_at": "2023-08-23T15:59:24Z",
"access_policy": "time_only",
"valid": true,
"checks": [
{
"step": 1,
"name": "parse DKC1",
"ok": true,
"detail": "magic DKC1"
},
{
"step": 2,
"name": "prelude",
"ok": true,
"detail": "DKC1 v3, PUBLIC_HEADER_LEN=121, SEALED_CONTROL_LEN=458"
},
{
"step": 3,
"name": "public header",
"ok": true,
"detail": "121 bytes"
},
{
"step": 4,
"name": "header validation",
"ok": true,
"detail": "capsule_id=b3d25bb6c74cda4e014c65e3d2f43a69 datekey=dk1_eyJ2ZXJzaW9uIjoxLCJuZXR3b3JrIjoiZGF0ZWtleXM6cXVpY2tuZXQ6djEiLCJyb3VuZCI6MTAwMH0 policy=time_only profile=datekeys:quicknet:v1"
},
{
"step": 5,
"name": "sealed control structure",
"ok": true,
"detail": "one tlock stanza"
},
{
"step": 6,
"name": "payload structure",
"ok": true,
"detail": "one X25519 stanza"
},
{
"step": 7,
"name": "condition",
"ok": true,
"detail": "round 1000, unlock at 2023-08-23T15:59:24Z"
},
{
"step": 8,
"name": "tlock stanza",
"ok": true,
"detail": "round 1000, chain 52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971"
}
]
}

@ -0,0 +1,173 @@
{
"description": "format 3 time_only capsule with a single file, nota.txt, signed with alg 1 by the test key of format3_signed and sealed with seal_type 2 by a test time-stamping authority before the round time: verdicts F4 and S4, with SEAL_SUBJECT and the token in the record",
"spec": "0.10",
"format": 3,
"file": "format3_sealed.dkc",
"sha256": "dde5a072d8783227d28279d06d3d226a1fb967c766da626f889d1c6fd76ac9c7",
"release": {
"round": 1000,
"signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39"
},
"prelude": "444b43310300000000000079000001ca",
"public_header": "a5006a646174656b657963617001010250b3d25bb6c74cda4e014c65e3d2f43a69037853646b315f65794a325a584a7a61573975496a6f784c434a755a58523362334a72496a6f695a4746305a57746c65584d3663585670593274755a585136646a45694c434a79623356755a4349364d5441774d48300400",
"datekey": "dk1_eyJ2ZXJzaW9uIjoxLCJuZXR3b3JrIjoiZGF0ZWtleXM6cXVpY2tuZXQ6djEiLCJyb3VuZCI6MTAwMH0",
"capsule_id": "b3d25bb6c74cda4e014c65e3d2f43a69",
"access_policy": "time_only",
"structure": "time_only",
"unlock_at": "2023-08-23T15:59:24Z",
"header_binding": "c108d8b34eb0a4237e6aff192364a28d544cb79c84ac5c80d88c26f8bd5560d8",
"outer_stanzas": [
{
"type": "tlock",
"args": [
"1000",
"52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971"
]
}
],
"payload_stanzas": [
{
"type": "X25519",
"args": [
"PQPwIpsXx8ZNHf1Xpr7gnPamVYWLPgD8zIGmXqvSkjY"
]
}
],
"control_cbor": "a60070646174656b6579732d636f6e74726f6c0103025820c108d8b34eb0a4237e6aff192364a28d544cb79c84ac5c80d88c26f8bd5560d803582087351091f87f94f2c1155e5bde726116123e321f6cc2c3f2bb934298e0d02ea8064800000000000080930702",
"payload_identity": "87351091f87f94f2c1155e5bde726116123e321f6cc2c3f2bb934298e0d02ea8",
"payload_length": 32915,
"padding": 2,
"padded_length": 34816,
"plaintext_file": "format3_sealed.plaintext",
"plaintext_sha256": "aea0f5feb40acd81ca3b02dd21ea15510234da3ab52b374322f3206e7632d47b",
"area_len": 32768,
"security_cbor": "a40071646174656b6579732d73656375726974790101025869a3000101582092d3a82b1e2387a860d57cae4cc55091d43904fdd625e4d2b285e3a1b4ba674002584024c8a0fd4ef7338a2607c1489e33eb32b2d2c6a9438e8d01d5beb04911acd5bdf7eb4bd958141293e5fe4074276097cd0df21d8ca12b9c2e6cb5164304666c060359038ba20002015903843082038006092a864886f70d010702a08203713082036d020101310d300b06096086480165030402013065060b2a864886f70d0109100104a0560454305202010106032a0304302f300b060960864801650304020104201ce5a87549e8bed1af2238888c5a4e942b3b3de1fa8d757c70941cd21522638d02012a180f32303233303832333135303932375a3003020101a08201b8308201b430820159a0030201020208074ecca73c287e79300a06082a8648ce3d040302304131163014060355040a130d446174654b6579732074657374312730250603550403131e4175746f72696461642064652053656c6c61646f20646520707275656261301e170d3230303130313030303030305a170d3430303130313030303030305a304131163014060355040a130d446174654b6579732074657374312730250603550403131e4175746f72696461642064652053656c6c61646f206465207072756562613059301306072a8648ce3d020106082a8648ce3d03010703420004aa2eb21e68c810b6271dadcc25575401ceccac2461de40cb2bfde25bceaa384abbbba098880bf14316b62769b3596d701d60c8dd1f1313f257d93c4d5f213a53a33b3039300e0603551d0f0101ff04040302078030270603551d0e0420041e4175746f72696461642064652053656c6c61646f20646520707275656261300a06082a8648ce3d0403020349003046022100a05f22801b8432108d6330dd777a25bcfdd1b32db72bc8feb9be522291559e11022100cfd71b1445e68079aa98af90ac7037ab938eb7e4f11279f1476a93ce0b693aff3182013430820130020101304d304131163014060355040a130d446174654b6579732074657374312730250603550403131e4175746f72696461642064652053656c6c61646f206465207072756562610208074ecca73c287e79300b0609608648016503040201a07a301a06092a864886f70d010903310d060b2a864886f70d0109100104302b060b2a864886f70d010910020c311c301a30183016041426e0465376bf0d652c266e5940c9d01ba8cea5e9302f06092a864886f70d01090431220420545889d11d3289646f8156538dbcc608e8cc543fa052056a56a85eb7356523f1300a06082a8648ce3d04030204473045022100883cfcc51038245652e767fdfecea1b7153b9158df02d95f3fcfc2b58e575cb1022029b9727ead61a3022bcddd62d704ee26f71de738556d97796de3ce1c408c7419",
"head_cbor": "a4006d646174656b6579732d686561640101025820990d6781941d508558b06652f4c4db8fc56da5a443811b723adfe4370ea139710581a600686e6f74612e7478740116020003160458205d596d5f49c179f0ec337f4610155fc237e727f9dee85377a5fce67a3c853510051a6abcf9c0",
"salt": "990d6781941d508558b06652f4c4db8fc56da5a443811b723adfe4370ea13971",
"content_offset": 32893,
"files": [
{
"path": "nota.txt",
"size": 22,
"start": 0,
"end": 22,
"sha256": "5d596d5f49c179f0ec337f4610155fc237e727f9dee85377a5fce67a3c853510",
"mtime": 1790769600
}
],
"verdicts": {
"signature": "F4",
"seal": "S4",
"lines": [
"Firmado con la clave dkauthor1jtf6s2c7ywr6scx40jhye32sj82rjp8a6cj7f54jsh36rd96vaqqcg54xg. No prueba quién la tiene.",
"Según un sello a nombre de Autoridad de Sellado de prueba, existía el 2023-08-23T15:09:27Z, antes de que la cápsula pudiera abrirse. DateKeys no comprueba quién emitió el sello."
],
"author_key": "dkauthor1jtf6s2c7ywr6scx40jhye32sj82rjp8a6cj7f54jsh36rd96vaqqcg54xg"
},
"signature": {
"alg": 1,
"secret_seed": "294b60d256e79fc4c18b4bcc0a156810b44144619969dadedcbf01d3b37c1054",
"author_key": "dkauthor1jtf6s2c7ywr6scx40jhye32sj82rjp8a6cj7f54jsh36rd96vaqqcg54xg",
"control_commit": "1793cc0adaba31bdbf18cb4c97ba86dcff05bec498c8ecf11668a74bfd71718a",
"head_digest": "a5537946c281a8e22ca9d3a4deb1aebcddf0d299abe47bded31c90157dd72d3a",
"signers_digest": "1665c6f3f1afb37b1a87d87e4265f156d7108c3c1762972d2a4241e6e5ab824e",
"author_message": "datekeys:dkc3:author-signature:v1\n111b174e204e0eaa73b87601e1e1d9b3d60799b5a0a5c0e371a9dc65d1d39f38\n",
"author_code": "111b-174e",
"signature": "24c8a0fd4ef7338a2607c1489e33eb32b2d2c6a9438e8d01d5beb04911acd5bdf7eb4bd958141293e5fe4074276097cd0df21d8ca12b9c2e6cb5164304666c06",
"security_key_2": "a3000101582092d3a82b1e2387a860d57cae4cc55091d43904fdd625e4d2b285e3a1b4ba674002584024c8a0fd4ef7338a2607c1489e33eb32b2d2c6a9438e8d01d5beb04911acd5bdf7eb4bd958141293e5fe4074276097cd0df21d8ca12b9c2e6cb5164304666c06"
},
"seal": {
"seal_type": 2,
"seal_subject": "8e60fd12a9475d95f77adacf81772774f4a8f508ffc7edda2e15df9ccebf163e",
"token": "3082038006092a864886f70d010702a08203713082036d020101310d300b06096086480165030402013065060b2a864886f70d0109100104a0560454305202010106032a0304302f300b060960864801650304020104201ce5a87549e8bed1af2238888c5a4e942b3b3de1fa8d757c70941cd21522638d02012a180f32303233303832333135303932375a3003020101a08201b8308201b430820159a0030201020208074ecca73c287e79300a06082a8648ce3d040302304131163014060355040a130d446174654b6579732074657374312730250603550403131e4175746f72696461642064652053656c6c61646f20646520707275656261301e170d3230303130313030303030305a170d3430303130313030303030305a304131163014060355040a130d446174654b6579732074657374312730250603550403131e4175746f72696461642064652053656c6c61646f206465207072756562613059301306072a8648ce3d020106082a8648ce3d03010703420004aa2eb21e68c810b6271dadcc25575401ceccac2461de40cb2bfde25bceaa384abbbba098880bf14316b62769b3596d701d60c8dd1f1313f257d93c4d5f213a53a33b3039300e0603551d0f0101ff04040302078030270603551d0e0420041e4175746f72696461642064652053656c6c61646f20646520707275656261300a06082a8648ce3d0403020349003046022100a05f22801b8432108d6330dd777a25bcfdd1b32db72bc8feb9be522291559e11022100cfd71b1445e68079aa98af90ac7037ab938eb7e4f11279f1476a93ce0b693aff3182013430820130020101304d304131163014060355040a130d446174654b6579732074657374312730250603550403131e4175746f72696461642064652053656c6c61646f206465207072756562610208074ecca73c287e79300b0609608648016503040201a07a301a06092a864886f70d010903310d060b2a864886f70d0109100104302b060b2a864886f70d010910020c311c301a30183016041426e0465376bf0d652c266e5940c9d01ba8cea5e9302f06092a864886f70d01090431220420545889d11d3289646f8156538dbcc608e8cc543fa052056a56a85eb7356523f1300a06082a8648ce3d04030204473045022100883cfcc51038245652e767fdfecea1b7153b9158df02d95f3fcfc2b58e575cb1022029b9727ead61a3022bcddd62d704ee26f71de738556d97796de3ce1c408c7419",
"holder": "Autoridad de Sellado de prueba",
"time": "2023-08-23T15:09:27Z"
},
"stages": [
{
"step": 1,
"name": "parse DKC1",
"ok": true
},
{
"step": 2,
"name": "prelude",
"ok": true
},
{
"step": 3,
"name": "public header",
"ok": true
},
{
"step": 4,
"name": "header validation",
"ok": true
},
{
"step": 5,
"name": "sealed control structure",
"ok": true
},
{
"step": 6,
"name": "payload structure",
"ok": true
},
{
"step": 7,
"name": "condition",
"ok": true
},
{
"step": 8,
"name": "tlock stanza",
"ok": true
},
{
"step": 9,
"name": "release",
"ok": true
},
{
"step": 10,
"name": "release verification",
"ok": true
},
{
"step": 11,
"name": "open sealed control",
"ok": true
},
{
"step": 12,
"name": "policy structure",
"ok": true
},
{
"step": 14,
"name": "control",
"ok": true
},
{
"step": 15,
"name": "header binding",
"ok": true
},
{
"step": 16,
"name": "payload identity",
"ok": true
},
{
"step": 17,
"name": "open payload",
"ok": true
},
{
"step": 18,
"name": "commit",
"ok": true
}
]
}

Binary file not shown.

Binary file not shown.

@ -0,0 +1,61 @@
{
"file": "format3_signed_cms.dkc",
"format": 3,
"capsule_id": "fb1b4917149f996c9c9561997593d37d",
"datekey": "dk1_eyJ2ZXJzaW9uIjoxLCJuZXR3b3JrIjoiZGF0ZWtleXM6cXVpY2tuZXQ6djEiLCJyb3VuZCI6MTAwMH0",
"profile": "datekeys:quicknet:v1",
"round": 1000,
"unlock_at": "2023-08-23T15:59:24Z",
"access_policy": "time_only",
"valid": true,
"checks": [
{
"step": 1,
"name": "parse DKC1",
"ok": true,
"detail": "magic DKC1"
},
{
"step": 2,
"name": "prelude",
"ok": true,
"detail": "DKC1 v3, PUBLIC_HEADER_LEN=121, SEALED_CONTROL_LEN=458"
},
{
"step": 3,
"name": "public header",
"ok": true,
"detail": "121 bytes"
},
{
"step": 4,
"name": "header validation",
"ok": true,
"detail": "capsule_id=fb1b4917149f996c9c9561997593d37d datekey=dk1_eyJ2ZXJzaW9uIjoxLCJuZXR3b3JrIjoiZGF0ZWtleXM6cXVpY2tuZXQ6djEiLCJyb3VuZCI6MTAwMH0 policy=time_only profile=datekeys:quicknet:v1"
},
{
"step": 5,
"name": "sealed control structure",
"ok": true,
"detail": "one tlock stanza"
},
{
"step": 6,
"name": "payload structure",
"ok": true,
"detail": "one X25519 stanza"
},
{
"step": 7,
"name": "condition",
"ok": true,
"detail": "round 1000, unlock at 2023-08-23T15:59:24Z"
},
{
"step": 8,
"name": "tlock stanza",
"ok": true,
"detail": "round 1000, chain 52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971"
}
]
}

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long
Loading…
Cancel
Save

Powered by TurnKey Linux.