diff --git a/capsule/conformance_test.go b/capsule/conformance_test.go index 081c87c..45cad18 100644 --- a/capsule/conformance_test.go +++ b/capsule/conformance_test.go @@ -18,6 +18,7 @@ import ( "g.activething.com/go/DateKeys/accesskey" "g.activething.com/go/DateKeys/agewrap" + "g.activething.com/go/DateKeys/authorkey" "g.activething.com/go/DateKeys/capsule" "g.activething.com/go/DateKeys/extension" "g.activething.com/go/DateKeys/internal/testkit" @@ -35,6 +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", } type fixture struct { @@ -301,7 +303,8 @@ func TestConformanceFixtures(t *testing.T) { } v := opened.Verdicts if string(v.Signature) != f.Verdicts.Signature || string(v.Seal) != f.Verdicts.Seal || opened.AreaLen != f.AreaLen || - opened.Head.Comment != f.Comment || opened.Head.Author != f.Author || hex.EncodeToString(opened.Head.Salt[:]) != f.Salt { + opened.Head.Comment != f.Comment || opened.Head.Author != f.Author || hex.EncodeToString(opened.Head.Salt[:]) != f.Salt || + (f.Verdicts.AuthorKey != "") != (v.AuthorKey != [32]byte{}) { t.Fatalf("Open reports another head or verdicts: %+v", v) } } else if !bytes.Equal(out.Bytes(), f.plaintext) { @@ -325,6 +328,55 @@ 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 +// 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) + } + c, err := capsule.DecodeControl(cb, f.format()) + if err != nil { + t.Fatal(err) + } + cc, err := capsule.ControlCommit(c, f.format()) + 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) + if err != nil { + t.Fatal(err) + } + seed, err := hex.DecodeString(s.SecretSeed) + if err != nil { + t.Fatal(err) + } + key, err := authorkey.NewFromSeed(seed) + 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) + } + unlock, err := time.Parse(time.RFC3339, f.UnlockAt) + if err != nil { + t.Fatal(err) + } + return capsule.EvaluateSecurityIn(security, &capsule.SecurityContext{ControlCommit: cc, HeadDigest: hd, RoundTime: unlock}) +} + // checkBody3 checks BODY, the plaintext file of a format 3 fixture, against // its record: the frame, the area, security and its verdicts, the head and // each file with its SHA-256 (spec §29.2 to §29.7). @@ -347,6 +399,9 @@ 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 { + 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) { t.Fatalf("verdicts %+v, recorded %+v", v, f.Verdicts) } diff --git a/capsule/signature.go b/capsule/signature.go index a1282df..dd04664 100644 --- a/capsule/signature.go +++ b/capsule/signature.go @@ -4,6 +4,7 @@ import ( "crypto/sha256" "encoding/binary" "encoding/hex" + "errors" "time" "g.activething.com/go/DateKeys/codec/bech32" @@ -123,6 +124,22 @@ func SealSubject(controlCommit, headDigest [32]byte, sigPart []byte) [32]byte { return domainHash(sealSubjectPrefix, controlCommit[:], headDigest[:], sigPart) } +// SecurityKey2 returns the exact content of key 2 of SECURITY_CBOR and, when +// it is an author-signature that decodes, the signature value it holds: what +// a generator of test vectors records about a signature. It fails when +// security has no key 2 or its content does not decode. +func SecurityKey2(security []byte) (content, value []byte, err error) { + w, ok := decodeSecurity(security) + if !ok || w.signature == nil { + return nil, nil, errors.New("capsule: SECURITY_CBOR holds no author-signature") + } + a, err := decodeAuthorSignature(w.signature) + if err != nil { + return nil, nil, err + } + return w.signature, 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 diff --git a/capsule/signed_test.go b/capsule/signed_test.go index 7e04ca3..ba4025a 100644 --- a/capsule/signed_test.go +++ b/capsule/signed_test.go @@ -3,6 +3,7 @@ package capsule_test import ( "bytes" "context" + "encoding/hex" "strings" "testing" @@ -96,3 +97,85 @@ func TestEncryptFilesSignatureChecked(t *testing.T) { } } } + +// Spec v0.11 §29.7, §29.8: a signature of the fixture format3_signed holds +// in the context of its capsule and in no other: a bit of the signature, of +// a commitment or of the message changes the verdict to F2; the same message +// signed by another key is another key's F4; and a security area without it +// is F0. +func TestSignedFixtureVerdicts(t *testing.T) { + f := loadFixture(t, "format3_signed") + body := f.plaintext + frame, err := capsule.ParseBodyFrame(body[:capsule.BodyFrameSize], uint64(len(body))) + if err != nil { + t.Fatal(err) + } + security := body[capsule.BodyFrameSize : capsule.BodyFrameSize+frame.SecurityLen] + cb, _ := hex.DecodeString(f.ControlCBOR) + c, err := capsule.DecodeControl(cb, f.format()) + if err != nil { + t.Fatal(err) + } + cc, err := capsule.ControlCommit(c, f.format()) + if err != nil { + t.Fatal(err) + } + hb := body[capsule.BodyFrameSize+frame.AreaLen : capsule.BodyFrameSize+frame.AreaLen+frame.HeadLen] + ctx := func() *capsule.SecurityContext { + return &capsule.SecurityContext{ControlCommit: cc, HeadDigest: capsule.HeadDigest(hb)} + } + if v := capsule.EvaluateSecurityIn(security, ctx()); v.Signature != capsule.VerdictSignedOther || v.Seal != capsule.VerdictNoSeal { + t.Fatalf("the signature of the fixture: %+v", v) + } + + // Another capsule: another control commitment, or another head. + other := ctx() + other.ControlCommit[0] ^= 1 + if v := capsule.EvaluateSecurityIn(security, other); v.Signature != capsule.VerdictSignatureInvalid { + t.Errorf("another control: %+v", v) + } + other = ctx() + other.HeadDigest[31] ^= 1 + if v := capsule.EvaluateSecurityIn(security, other); v.Signature != capsule.VerdictSignatureInvalid { + t.Errorf("another head: %+v", v) + } + + // A bit of the signature, or of the key. + _, value, err := capsule.SecurityKey2(security) + if err != nil { + t.Fatal(err) + } + pub, _ := authorkey.ParsePublic(f.Signature.AuthorKey) + for name, mutate := range map[string]func(sig, key []byte){ + "signature": func(sig, key []byte) { sig[63] ^= 1 }, + "key": func(sig, key []byte) { key[0] ^= 1 }, + } { + sig, key := bytes.Clone(value), bytes.Clone(pub) + mutate(sig, key) + x, err := capsule.EncodeAuthorSignature(capsule.AlgEd25519, key, sig) + if err != nil { + t.Fatal(err) + } + s, err := capsule.EncodeSecurityWith(x, nil) + if err != nil { + t.Fatal(err) + } + if v := capsule.EvaluateSecurityIn(s, ctx()); v.Signature != capsule.VerdictSignatureInvalid { + t.Errorf("a bit of the %s: %+v", name, v) + } + } + + // The same message signed by another key: F4 with that key. + msg := capsule.AuthorMessage(cc, capsule.HeadDigest(hb), capsule.SignersDigest(capsule.AlgEd25519, nil)) + k, _ := authorkey.Generate() + x, _ := capsule.EncodeAuthorSignature(capsule.AlgEd25519, k.Public(), k.Sign(msg)) + s, _ := capsule.EncodeSecurityWith(x, nil) + if v := capsule.EvaluateSecurityIn(s, ctx()); v.Signature != capsule.VerdictSignedOther || v.AuthorKey != [32]byte(k.Public()) { + t.Errorf("another key: %+v", v) + } + + // Removed: F0. + if v := capsule.EvaluateSecurityIn(capsule.EncodeSecurity(), ctx()); v.Signature != capsule.VerdictNoSignature { + t.Errorf("removed: %+v", v) + } +} diff --git a/cmd/datekeys/main_test.go b/cmd/datekeys/main_test.go index 86e7f72..e6e34b0 100644 --- a/cmd/datekeys/main_test.go +++ b/cmd/datekeys/main_test.go @@ -133,6 +133,7 @@ func TestDecryptFormat3Fixtures(t *testing.T) { {"format3_comment_only", ""}, {"format3_time_and_key_portable", "format3_time_and_key_portable.dkk"}, {"format3_seal_unsupported", ""}, + {"format3_signed", ""}, } { t.Run(tc.name, func(t *testing.T) { var f testkit.DKCFixture @@ -475,8 +476,8 @@ func TestInspectJSONGoldens(t *testing.T) { if err != nil { t.Fatal(err) } - if len(names) != len(dkcs) || len(dkcs) != 21 { - t.Fatalf("%d frozen inspect outputs, want one per official .dkc (%d, 21)", len(names), len(dkcs)) + 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)) } t.Chdir(fixtures) for _, path := range names { diff --git a/internal/testkit/fixture.go b/internal/testkit/fixture.go index 164c861..4c52377 100644 --- a/internal/testkit/fixture.go +++ b/internal/testkit/fixture.go @@ -84,7 +84,11 @@ type DKCFixture struct { ContentOffset uint64 `json:"content_offset,omitempty"` Files []FixtureFile `json:"files,omitempty"` Verdicts *FixtureVerdicts `json:"verdicts,omitempty"` - Stages []FixtureStage `json:"stages"` + // Signature is, in a fixture signed with alg 1, what a second + // 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"` } // FixtureFile is a file of a format 3 fixture: its entry of the head. Its @@ -104,6 +108,28 @@ type FixtureVerdicts struct { Signature string `json:"signature"` Seal string `json:"seal"` Lines []string `json:"lines"` + // AuthorKey is the dkauthor1… key of a valid signature (F3, F4). + AuthorKey string `json:"author_key,omitempty"` +} + +// FixtureSignature describes the signature of alg 1 of a format 3 fixture. +// SecretSeed is the 32-byte seed of a test key made for it: Ed25519 is +// deterministic, so signing AuthorMessage with it gives SignatureValue +// again. ControlCommit, HeadDigest and SignersDigest are the commitments of +// spec §29.8, in hexadecimal; AuthorMessage is the ASCII text that is +// 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"` + SignatureValue string `json:"signature"` + SecurityKey2 string `json:"security_key_2"` } // FixtureExt is an extension in a fixture. diff --git a/internal/testkit/genfixtures/format3.go b/internal/testkit/genfixtures/format3.go index 9547627..a5e6f32 100644 --- a/internal/testkit/genfixtures/format3.go +++ b/internal/testkit/genfixtures/format3.go @@ -15,6 +15,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/testkit" ) @@ -107,7 +108,7 @@ func patterned(seed string, n int) []byte { // record3 fills the fields of format 3 of f from BODY, the first L bytes of // the plaintext of PAYLOAD_AGE, checking it with the rules of the reader. -func record3(f *testkit.DKCFixture, body []byte) error { +func record3(f *testkit.DKCFixture, body []byte, verdicts *capsule.Verdicts) error { l := uint64(len(body)) if l < capsule.BodyFrameSize { return errors.New("BODY shorter than its frame") @@ -131,6 +132,9 @@ func record3(f *testkit.DKCFixture, body []byte) error { return err } v := capsule.EvaluateSecurity(security) + if verdicts != nil { + v = *verdicts + } f.AreaLen = frame.AreaLen f.Security = hex.EncodeToString(security) f.Head = hex.EncodeToString(hb) @@ -152,9 +156,60 @@ func record3(f *testkit.DKCFixture, body []byte) error { f.Files = append(f.Files, ff) } f.Verdicts = &testkit.FixtureVerdicts{Signature: string(v.Signature), Seal: string(v.Seal), Lines: v.Lines()} + f.Signature, err = recordSignature(f, security, hb, v) + if err != nil { + return err + } + if f.Signature != nil { + f.Verdicts.AuthorKey = f.Signature.AuthorKey + } return nil } +// 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") + } + cb, err := hex.DecodeString(f.ControlCBOR) + if err != nil { + return nil, err + } + c, err := capsule.DecodeControl(cb, capsule.Format(f.Format)) + if err != nil { + return nil, err + } + defer clear(c.PayloadIdentity[:]) + cc, err := capsule.ControlCommit(c, capsule.Format(f.Format)) + if err != nil { + return nil, err + } + hd := capsule.HeadDigest(head) + sd := capsule.SignersDigest(capsule.AlgEd25519, nil) + msg := capsule.AuthorMessage(cc, hd, sd) + content, value, err := capsule.SecurityKey2(security) + if err != nil { + return nil, err + } + key, err := bech32.Encode("dkauthor", v.AuthorKey[:]) + 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), + }, nil +} + // check3 checks what Open delivered for the format 3 fixture f, whose BODY // is body: the files in its sink, the head and the verdicts. func check3(f *testkit.DKCFixture, body []byte, opened *capsule.Opened, sink *testkit.MemorySink) error { @@ -168,6 +223,9 @@ func check3(f *testkit.DKCFixture, body []byte, opened *capsule.Opened, sink *te } } want := &testkit.FixtureVerdicts{Signature: string(opened.Verdicts.Signature), Seal: string(opened.Verdicts.Seal), Lines: opened.Verdicts.Lines()} + if f.Verdicts != nil && f.Verdicts.AuthorKey != "" { + want.AuthorKey, _ = bech32.Encode("dkauthor", opened.Verdicts.AuthorKey[:]) + } if !reflect.DeepEqual(want, f.Verdicts) || opened.AreaLen != f.AreaLen || opened.Head.Comment != f.Comment || opened.Head.Author != f.Author { return errors.New("Open reports another head or other verdicts") } diff --git a/internal/testkit/genfixtures/main.go b/internal/testkit/genfixtures/main.go index 6cbdc93..6cfd4f7 100644 --- a/internal/testkit/genfixtures/main.go +++ b/internal/testkit/genfixtures/main.go @@ -50,6 +50,7 @@ import ( "g.activething.com/go/DateKeys/accesskey" "g.activething.com/go/DateKeys/agewrap" + "g.activething.com/go/DateKeys/authorkey" "g.activething.com/go/DateKeys/capsule" "g.activething.com/go/DateKeys/datekey" "g.activething.com/go/DateKeys/extension" @@ -315,7 +316,7 @@ func refreshRecord(dir, name string) error { } if opened.Format == capsule.Format3 { // The plaintext file holds BODY. - if err := record3(&f, plaintext); err != nil { + if err := record3(&f, plaintext, &opened.Verdicts); err != nil { return err } if err := check3(&f, plaintext, opened, sink); err != nil { @@ -368,7 +369,10 @@ type spec struct { // vectors writes (spec §62.1 rule 13), and testkit.Build writes it. files []file3 comment, author string - body func() ([]byte, error) + // 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) } // Extension data of the fixtures (spec §54, §72): the header carries the raw @@ -379,6 +383,9 @@ var ( headerExtData = []byte("public label") controlExtData = mustHex("a2000701667365616c6564") dkkExtData = mustHex("a1006468616e64") + // signerSeed is the seed of the test key of format3_signed: the SHA-256 + // of a text. It is not a secret: anyone may sign with it. + signerSeed = func() []byte { h := sha256.Sum256([]byte("DateKeys fixture author key 1")); return h[:] }() ) func mustHex(s string) []byte { @@ -474,6 +481,7 @@ 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}, } } @@ -621,6 +629,14 @@ 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.signer != nil { + k, err := authorkey.NewFromSeed(s.signer) + if err != nil { + return nil, err + } + defer k.Clear() + opts.AuthorKey = k + } res, err = capsule.EncryptFiles(&dkc, sources(s.files), opts) } else { opts.Length, opts.TestVectors = int64(len(s.plaintext)), true @@ -749,7 +765,9 @@ func generate(dir string, s spec) error { return errors.New("fixture plaintext mismatch") } case capsule.Format3: - if err := record3(&f, plaintext); err != nil { + // Without the verdicts of Open, which come later: the derived record + // that refreshRecord writes takes them from the opening. + if err := record3(&f, plaintext, nil); err != nil { return err } } @@ -770,6 +788,10 @@ func generate(dir string, s spec) error { return fmt.Errorf("fixture does not open: %w", err) } if format == capsule.Format3 { + // A signature is checked in the context of its capsule, which the + // record above does not have yet: the verdicts are those of Open. + v := opened.Verdicts + f.Verdicts = &testkit.FixtureVerdicts{Signature: string(v.Signature), Seal: string(v.Seal), Lines: v.Lines()} if err := check3(&f, plaintext, opened, sink); err != nil { return err } @@ -800,6 +822,9 @@ func generate(dir string, s spec) error { f.PlaintextSHA256 = hex.EncodeToString(psum[:]) f.HeaderExtensions = exts(false, s.headerExt) f.ControlExt = exts(false, s.controlExt) + if s.signer != nil { + f.Signature = &testkit.FixtureSignature{SecretSeed: hex.EncodeToString(s.signer)} + } for _, c := range opened.Inspection.Checks { f.Stages = append(f.Stages, testkit.FixtureStage{Step: c.Step, Name: c.Name, OK: c.OK, Error: c.Error}) } diff --git a/testdata/fixtures/format3_signed.dkc b/testdata/fixtures/format3_signed.dkc new file mode 100644 index 0000000..14c2d3f Binary files /dev/null and b/testdata/fixtures/format3_signed.dkc differ diff --git a/testdata/fixtures/format3_signed.inspect.json b/testdata/fixtures/format3_signed.inspect.json new file mode 100644 index 0000000..bd2134b --- /dev/null +++ b/testdata/fixtures/format3_signed.inspect.json @@ -0,0 +1,61 @@ +{ + "file": "format3_signed.dkc", + "format": 3, + "capsule_id": "681476c1bfa81dd12fec810ee44fe7f7", + "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=681476c1bfa81dd12fec810ee44fe7f7 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" + } + ] +} diff --git a/testdata/fixtures/format3_signed.json b/testdata/fixtures/format3_signed.json new file mode 100644 index 0000000..3041726 --- /dev/null +++ b/testdata/fixtures/format3_signed.json @@ -0,0 +1,165 @@ +{ + "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", + "spec": "0.10", + "format": 3, + "file": "format3_signed.dkc", + "sha256": "3c7d3c9e24c02853a0c7761b93bea1120b27fce396468d8d0f68e53aeb668c5e", + "release": { + "round": 1000, + "signature": "b44679b9a59af2ec876b1a6b1ad52ea9b1615fc3982b19576350f93447cb1125e342b73a8dd2bacbe47e4b6b63ed5e39" + }, + "prelude": "444b43310300000000000079000001ca", + "public_header": "a5006a646174656b657963617001010250681476c1bfa81dd12fec810ee44fe7f7037853646b315f65794a325a584a7a61573975496a6f784c434a755a58523362334a72496a6f695a4746305a57746c65584d3663585670593274755a585136646a45694c434a79623356755a4349364d5441774d48300400", + "datekey": "dk1_eyJ2ZXJzaW9uIjoxLCJuZXR3b3JrIjoiZGF0ZWtleXM6cXVpY2tuZXQ6djEiLCJyb3VuZCI6MTAwMH0", + "capsule_id": "681476c1bfa81dd12fec810ee44fe7f7", + "access_policy": "time_only", + "structure": "time_only", + "unlock_at": "2023-08-23T15:59:24Z", + "header_binding": "10c269d7fcc8ae1f92c9854b8d01b23781fd6c615474c018b5e75c8620d476eb", + "outer_stanzas": [ + { + "type": "tlock", + "args": [ + "1000", + "52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971" + ] + } + ], + "payload_stanzas": [ + { + "type": "X25519", + "args": [ + "TO8ryXIRUX0S1odrqs9IZJ43q7yYotzn/fMycGebKGQ" + ] + } + ], + "control_cbor": "a60070646174656b6579732d636f6e74726f6c010302582010c269d7fcc8ae1f92c9854b8d01b23781fd6c615474c018b5e75c8620d476eb0358203ffe484f443014980f2b2ee8bb66095f6ea418d4c2fce2ae6ad74c550bbefc21064800000000000080930702", + "payload_identity": "3ffe484f443014980f2b2ee8bb66095f6ea418d4c2fce2ae6ad74c550bbefc21", + "payload_length": 32915, + "padding": 2, + "padded_length": 34816, + "plaintext_file": "format3_signed.plaintext", + "plaintext_sha256": "3de3ccab0ac74f95a76aa45c0f85e1749d4b4a051d87e81828eff6bf24372000", + "area_len": 32768, + "security_cbor": "a30071646174656b6579732d73656375726974790101025869a3000101582092d3a82b1e2387a860d57cae4cc55091d43904fdd625e4d2b285e3a1b4ba67400258406f8efe0dfadaf89bf71295167f8d8cc4890de0441a37a26d1f7d0b1f56b79c43c4010861e11742a5431b072b21490d4a976dc9ff385d36242047ad4cb996270b", + "head_cbor": "a4006d646174656b6579732d68656164010102582092a0fe390ff1063d9e93bbafe3ca75be219d950128e2e5aca6a9c12d5c1f1c9b0581a600686e6f74612e7478740116020003160458205d596d5f49c179f0ec337f4610155fc237e727f9dee85377a5fce67a3c853510051a6abcf9c0", + "salt": "92a0fe390ff1063d9e93bbafe3ca75be219d950128e2e5aca6a9c12d5c1f1c9b", + "content_offset": 32893, + "files": [ + { + "path": "nota.txt", + "size": 22, + "start": 0, + "end": 22, + "sha256": "5d596d5f49c179f0ec337f4610155fc237e727f9dee85377a5fce67a3c853510", + "mtime": 1790769600 + } + ], + "verdicts": { + "signature": "F4", + "seal": "S0", + "lines": [ + "Firmado con la clave dkauthor1jtf6s2c7ywr6scx40jhye32sj82rjp8a6cj7f54jsh36rd96vaqqcg54xg. No prueba quién la tiene." + ], + "author_key": "dkauthor1jtf6s2c7ywr6scx40jhye32sj82rjp8a6cj7f54jsh36rd96vaqqcg54xg" + }, + "signature": { + "alg": 1, + "secret_seed": "294b60d256e79fc4c18b4bcc0a156810b44144619969dadedcbf01d3b37c1054", + "author_key": "dkauthor1jtf6s2c7ywr6scx40jhye32sj82rjp8a6cj7f54jsh36rd96vaqqcg54xg", + "control_commit": "9f296b40049889c944be75c61827b694a0104eb5c5424e17217ff1fd32529efe", + "head_digest": "0111835cdde5ef15710cea96fc600757e1695ddc8f44b2b20e57eead31278b92", + "signers_digest": "1665c6f3f1afb37b1a87d87e4265f156d7108c3c1762972d2a4241e6e5ab824e", + "author_message": "datekeys:dkc3:author-signature:v1\n58aedcded250af5f798eeace817f8a5bbd0c245d05627949970b5eb998bb0413\n", + "author_code": "58ae-dcde", + "signature": "6f8efe0dfadaf89bf71295167f8d8cc4890de0441a37a26d1f7d0b1f56b79c43c4010861e11742a5431b072b21490d4a976dc9ff385d36242047ad4cb996270b", + "security_key_2": "a3000101582092d3a82b1e2387a860d57cae4cc55091d43904fdd625e4d2b285e3a1b4ba67400258406f8efe0dfadaf89bf71295167f8d8cc4890de0441a37a26d1f7d0b1f56b79c43c4010861e11742a5431b072b21490d4a976dc9ff385d36242047ad4cb996270b" + }, + "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 + } + ] +} diff --git a/testdata/fixtures/format3_signed.plaintext b/testdata/fixtures/format3_signed.plaintext new file mode 100644 index 0000000..fc5f258 Binary files /dev/null and b/testdata/fixtures/format3_signed.plaintext differ