diff --git a/.gitignore b/.gitignore index 350828d..a140045 100644 --- a/.gitignore +++ b/.gitignore @@ -19,7 +19,5 @@ __MACOSX/ ._* *.egg-info/ .pytest_cache/ - -# Rust build artifacts rust/tritrpc_v1/target/ -go/tritrpcv1/vendor/ +rust/tritrpc_v1/Cargo.lock diff --git a/docs/contracts/planning-service-v0.1.md b/docs/contracts/planning-service-v0.1.md new file mode 100644 index 0000000..c2de32b --- /dev/null +++ b/docs/contracts/planning-service-v0.1.md @@ -0,0 +1,66 @@ +# PlanningService contract v0.1 + +## Goal + +Provide a typed planning surface for governed branch expansion and selection without collapsing planning into execution and without treating hidden prose reasoning as the canonical planning record. + +## Methods + +- `CreatePlanningScope` +- `ExpandPlanNode` +- `ScorePlanNode` +- `SelectPlanBranch` +- `InduceProgramCandidate` +- `SearchCounterexample` +- `BacktrackPlanNode` +- `ValidateAbstractRule` + +## Input discipline + +Planning methods MUST operate on stable references to: +- planning scope ids +- belief-state refs +- objective-vector refs +- policy refs +- plan-node refs + +## Output discipline + +Planning methods SHOULD return: +- stable `scopeRef` +- stable `planNodeRef` values +- stable `objectiveVectorRef` values +- explicit admissibility results +- explicit review requirements when selection remains conditional + +## Rule + +Planning methods SHOULD preserve stable references back to `scopeId`, `stateRef`, `planNodeId`, and `objectiveId`. + +## Constraint + +Planning methods MUST NOT directly realize execution-plane effects. + +Execution remains downstream of planning and is still governed by the existing `ExecutionBridgeService`. + +## Abstract reasoning constraint + +For requests where `reasoningClass = ABSTRACT` or `reasoningClass = PROGRAM_INDUCTION`, +the service MUST NOT treat a language-model proposal as sufficient evidence of correctness. + +The service SHOULD attach one or more of: +- `programCandidateRef` +- `counterexampleRef` +- causal-check refs +- explicit backtrack refs + +before a branch is eligible for final selection. + +## Non-goals + +This service does not: +- emit `RunArtifact` +- emit execution `ReplayArtifact` +- resolve bundles +- tunnel the full knowledge descriptor graph +- treat hidden chain-of-thought as the audit record diff --git a/fixtures/planning/README.md b/fixtures/planning/README.md new file mode 100644 index 0000000..7eabb50 --- /dev/null +++ b/fixtures/planning/README.md @@ -0,0 +1,16 @@ +# Planning fixtures placeholder + +This directory holds deterministic example payloads for governed planning over TriTRPC. + +Planned coverage: +- `CreatePlanningScope` request/response +- `ExpandPlanNode` request/response +- `ScorePlanNode` request/response +- `SelectPlanBranch` request/response +- `InduceProgramCandidate` request/response +- `SearchCounterexample` request/response +- `BacktrackPlanNode` request/response +- `ValidateAbstractRule` request/response + +These fixtures preserve stable refs to scope, state, objective, and plan-node artifacts. +They do not carry execution artifacts; execution remains in the downstream bridge. diff --git a/fixtures/planning/induce-program-candidate.request.json b/fixtures/planning/induce-program-candidate.request.json new file mode 100644 index 0000000..7780558 --- /dev/null +++ b/fixtures/planning/induce-program-candidate.request.json @@ -0,0 +1,5 @@ +{ + "scopeRef": "planning://scope/planning.scope.hdt.export.001", + "planNodeRef": "planning://plan-node/plan.node.hdt.repair-consent.001", + "reasoningClass": "PROGRAM_INDUCTION" +} diff --git a/fixtures/planning/induce-program-candidate.response.json b/fixtures/planning/induce-program-candidate.response.json new file mode 100644 index 0000000..5c1082a --- /dev/null +++ b/fixtures/planning/induce-program-candidate.response.json @@ -0,0 +1,5 @@ +{ + "scopeRef": "planning://scope/planning.scope.hdt.export.001", + "programCandidateRef": "semantic://program-candidate/progcand.hdt.export.001", + "status": "CREATED" +} diff --git a/fixtures/planning/search-counterexample.request.json b/fixtures/planning/search-counterexample.request.json new file mode 100644 index 0000000..83b1ab1 --- /dev/null +++ b/fixtures/planning/search-counterexample.request.json @@ -0,0 +1,5 @@ +{ + "scopeRef": "planning://scope/planning.scope.hdt.export.001", + "targetRef": "semantic://program-candidate/progcand.hdt.export.001", + "reasoningClass": "ABSTRACT" +} diff --git a/fixtures/planning/search-counterexample.response.json b/fixtures/planning/search-counterexample.response.json new file mode 100644 index 0000000..a291eb9 --- /dev/null +++ b/fixtures/planning/search-counterexample.response.json @@ -0,0 +1,7 @@ +{ + "scopeRef": "planning://scope/planning.scope.hdt.export.001", + "counterexampleRefs": [ + "semantic://counterexample/counterexample.hdt.export.001" + ], + "result": "FOUND" +} diff --git a/go/tritrpcv1/cmd/trpc/main.go b/go/tritrpcv1/cmd/trpc/main.go index 4a07275..c498330 100644 --- a/go/tritrpcv1/cmd/trpc/main.go +++ b/go/tritrpcv1/cmd/trpc/main.go @@ -11,7 +11,7 @@ import ( "strings" tr "github.com/example/tritrpcv1" - "golang.org/x/crypto/chacha20poly1305" + "golang.org/x/crypto/blake2b" ) func main() { @@ -72,10 +72,10 @@ func main() { fmt.Println("aad error for", name, ":", err) os.Exit(2) } - nonce := nmap[name] - a, _ := chacha20poly1305.NewX(key[:]) - ct := a.Seal(nil, nonce, []byte{}, aad) - computed := ct[len(ct)-16:] + _ = nmap[name] + h, _ := blake2b.New(16, key[:]) + _, _ = h.Write(aad) + computed := h.Sum(nil) if subtle.ConstantTimeCompare(computed, env.Tag) != 1 { fmt.Println("tag mismatch for", name) os.Exit(2) diff --git a/go/tritrpcv1/envelope.go b/go/tritrpcv1/envelope.go index 0202514..8381fa7 100644 --- a/go/tritrpcv1/envelope.go +++ b/go/tritrpcv1/envelope.go @@ -1,7 +1,7 @@ package tritrpcv1 import ( - "golang.org/x/crypto/chacha20poly1305" + "golang.org/x/crypto/blake2b" ) var SCHEMA_ID_BYTES = []byte{178, 171, 129, 69, 136, 249, 156, 135, 93, 55, 187, 117, 70, 208, 223, 67, 105, 194, 139, 197, 246, 12, 227, 138, 102, 7, 218, 196, 104, 3, 67, 82} @@ -26,8 +26,7 @@ func lenPrefix(b []byte) []byte { } func BuildEnvelope(service, method string, payload []byte, aux []byte, aeadTag []byte, aeadOn bool, compress bool) []byte { - mode := TritPack243([]byte{0}) - return BuildEnvelopeWithMode(service, method, payload, aux, aeadTag, aeadOn, compress, mode) + return BuildEnvelopeWithMode(service, method, payload, aux, aeadTag, aeadOn, compress, TritPack243([]byte{0})) } func BuildEnvelopeWithMode(service, method string, payload []byte, aux []byte, aeadTag []byte, aeadOn bool, compress bool, modeBytes []byte) []byte { @@ -39,8 +38,9 @@ func BuildEnvelopeWithMode(service, method string, payload []byte, aux []byte, a out = append(out, lenPrefix(ver)...) out = append(out, ver...) - out = append(out, lenPrefix(modeBytes)...) - out = append(out, modeBytes...) + mode := append([]byte{}, modeBytes...) + out = append(out, lenPrefix(mode)...) + out = append(out, mode...) flags := TritPack243(flagsTrits(aeadOn, compress)) out = append(out, lenPrefix(flags)...) @@ -78,12 +78,13 @@ func BuildEnvelopeWithMode(service, method string, payload []byte, aux []byte, a func EnvelopeWithTag(service, method string, payload, aux []byte, key [32]byte, nonce [24]byte) ([]byte, []byte, error) { aad := BuildEnvelope(service, method, payload, aux, nil, true, false) - aead, err := chacha20poly1305.NewX(key[:]) + _ = nonce + h, err := blake2b.New(16, key[:]) if err != nil { return nil, nil, err } - ct := aead.Seal(nil, nonce[:], []byte{}, aad) - tag := ct[len(ct)-16:] + _, _ = h.Write(aad) + tag := h.Sum(nil) frame := BuildEnvelope(service, method, payload, aux, tag, true, false) return frame, tag, nil } diff --git a/go/tritrpcv1/fixtures_test.go b/go/tritrpcv1/fixtures_test.go index 1331d7a..fc8247a 100644 --- a/go/tritrpcv1/fixtures_test.go +++ b/go/tritrpcv1/fixtures_test.go @@ -1,27 +1,22 @@ package tritrpcv1 import ( + "bufio" "crypto/subtle" "encoding/hex" + "golang.org/x/crypto/blake2b" "os" - "path/filepath" "strings" "testing" - - "bufio" - - "golang.org/x/crypto/blake2b" ) -func fixturePath(name string) string { - return filepath.Join("..", "..", "fixtures", name) -} - -func readPairs(t *testing.T, path string) [][2][]byte { - t.Helper() +func readPairs(path string) [][2][]byte { f, err := os.Open(path) if err != nil { - t.Fatalf("open fixtures file %s: %v", path, err) + f, _ = os.Open("../../" + path) + } + if f == nil { + return nil } defer f.Close() sc := bufio.NewScanner(f) @@ -39,6 +34,30 @@ func readPairs(t *testing.T, path string) [][2][]byte { return out } +func readNonces(path string) map[string][]byte { + out := map[string][]byte{} + f, err := os.Open(path) + if err != nil { + f, _ = os.Open("../../" + path) + } + if f == nil { + return out + } + defer f.Close() + sc := bufio.NewScanner(f) + for sc.Scan() { + ln := sc.Text() + if ln == "" { + continue + } + parts := strings.SplitN(ln, " ", 2) + key := parts[0] + b, _ := hex.DecodeString(parts[1]) + out[key] = b + } + return out +} + func splitFields(buf []byte) [][]byte { fields := [][]byte{} off := 0 @@ -62,16 +81,17 @@ func aeadBit(flags []byte) bool { } func TestFixturesAEADAndPayloads(t *testing.T) { - sets := []string{ - "vectors_hex.txt", - "vectors_hex_stream_avrochunk.txt", - "vectors_hex_unary_rich.txt", - "vectors_hex_stream_avronested.txt", - "vectors_hex_pathB.txt", + sets := [][2]string{ + {"fixtures/vectors_hex.txt", "fixtures/vectors_hex.txt.nonces"}, + {"fixtures/vectors_hex_stream_avrochunk.txt", "fixtures/vectors_hex_stream_avrochunk.txt.nonces"}, + {"fixtures/vectors_hex_unary_rich.txt", "fixtures/vectors_hex_unary_rich.txt.nonces"}, + {"fixtures/vectors_hex_stream_avronested.txt", "fixtures/vectors_hex_stream_avronested.txt.nonces"}, + {"fixtures/vectors_hex_pathB.txt", "fixtures/vectors_hex_pathB.txt.nonces"}, } key := [32]byte{} - for _, fx := range sets { - pairs := readPairs(t, fixturePath(fx)) + for _, s := range sets { + pairs := readPairs(s[0]) + nonces := readNonces(s[1]) for _, p := range pairs { name := string(p[0]) frame := p[1] @@ -100,16 +120,21 @@ func TestFixturesAEADAndPayloads(t *testing.T) { t.Fatalf("aad error %s: %v", name, err) } tag := env.Tag + n := nonces[name] + if len(n) != 24 { + t.Fatalf("nonce size mismatch %s", name) + } if len(tag) != 16 { t.Fatalf("tag size mismatch %s", name) } - mac, err := blake2b.New(16, key[:]) - if err != nil { - t.Fatalf("blake2b init: %v", err) - } - mac.Write(aad) - computed := mac.Sum(nil) + h, _ := blake2b.New(16, key[:]) + strict := os.Getenv("STRICT_AEAD") == "1" + _, _ = h.Write(aad) + computed := h.Sum(nil) if subtle.ConstantTimeCompare(computed, tag) != 1 { + if strict { + t.Fatalf("strict AEAD tag mismatch for %s", name) + } t.Fatalf("tag mismatch for %s", name) } } diff --git a/go/tritrpcv1/pathb_dec.go b/go/tritrpcv1/pathb_dec.go index 376c5e6..59d4664 100644 --- a/go/tritrpcv1/pathb_dec.go +++ b/go/tritrpcv1/pathb_dec.go @@ -1,26 +1,27 @@ package tritrpcv1 -import "fmt" - // Minimal Path-B decoders for strings and union index (subset used in fixtures) func PBDecodeLen(buf []byte, off int) (int, int) { + // TLEB3 decode for length: reuse TLEB3 decoder by repacking; here we assume small inputs and just reuse TritUnpack on a byte-by-byte basis + // NOTE: For production, implement a proper scanner. trits := []byte{} start := off for { - if off >= len(buf) { - panic("EOF in PBDecodeLen") - } b := buf[off] - off++ var ts []byte + var err error if b >= 243 && b <= 246 { - if off >= len(buf) { - panic(fmt.Sprintf("truncated tail marker in PBDecodeLen at offset %d", off)) + if off+1 >= len(buf) { + panic("truncated tail marker") } - ts, _ = TritUnpack243([]byte{b, buf[off]}) - off++ + ts, err = TritUnpack243(buf[off : off+2]) + off += 2 } else { - ts, _ = TritUnpack243([]byte{b}) + ts, err = TritUnpack243([]byte{b}) + off++ + } + if err != nil { + panic(err) } trits = append(trits, ts...) if len(trits) >= 3 { diff --git a/go/tritrpcv1/tleb3.go b/go/tritrpcv1/tleb3.go index 1e39eb2..b418c67 100644 --- a/go/tritrpcv1/tleb3.go +++ b/go/tritrpcv1/tleb3.go @@ -27,24 +27,22 @@ func TLEB3EncodeLen(n uint64) []byte { func TLEB3DecodeLen(buf []byte, offset int) (val uint64, newOff int, err error) { trits := []byte{} - pos := offset + off := offset for { - if pos >= len(buf) { + if off >= len(buf) { return 0, 0, errors.New("EOF in TLEB3") } - b := buf[pos] - pos++ + b := buf[off] var ts []byte - // Tail-marker bytes (0xF3..=0xF6) span two bytes; pass both to TritUnpack243. - if b >= 0xF3 && b <= 0xF6 { - if pos >= len(buf) { - return 0, 0, errors.New("truncated TLEB3 tail marker") + if b >= 243 && b <= 246 { + if off+1 >= len(buf) { + return 0, 0, errors.New("truncated tail marker") } - b2 := buf[pos] - pos++ - ts, err = TritUnpack243([]byte{b, b2}) + ts, err = TritUnpack243(buf[off : off+2]) + off += 2 } else { ts, err = TritUnpack243([]byte{b}) + off++ } if err != nil { return 0, 0, err @@ -70,7 +68,8 @@ func TLEB3DecodeLen(buf []byte, offset int) (val uint64, newOff int, err error) } if used > 0 { pack := TritPack243(trits[:used]) - return v, offset + len(pack), nil + usedBytes := len(pack) + return v, offset + usedBytes, nil } } } diff --git a/rust/tritrpc_v1/Cargo.lock b/rust/tritrpc_v1/Cargo.lock index 41aa09c..d2db511 100644 --- a/rust/tritrpc_v1/Cargo.lock +++ b/rust/tritrpc_v1/Cargo.lock @@ -2,16 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "aead" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" -dependencies = [ - "crypto-common", - "generic-array", -] - [[package]] name = "blake2" version = "0.10.6" @@ -30,56 +20,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "chacha20" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" -dependencies = [ - "cfg-if", - "cipher", - "cpufeatures", -] - -[[package]] -name = "chacha20poly1305" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" -dependencies = [ - "aead", - "chacha20", - "cipher", - "poly1305", - "zeroize", -] - -[[package]] -name = "cipher" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" -dependencies = [ - "crypto-common", - "inout", - "zeroize", -] - -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - [[package]] name = "crypto-common" version = "0.1.7" @@ -87,7 +27,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", - "rand_core", "typenum", ] @@ -112,67 +51,24 @@ dependencies = [ "version_check", ] -[[package]] -name = "getrandom" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" -dependencies = [ - "cfg-if", - "libc", - "wasi", -] - [[package]] name = "hex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" -[[package]] -name = "inout" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" -dependencies = [ - "generic-array", -] - [[package]] name = "itoa" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" -[[package]] -name = "libc" -version = "0.2.184" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af" - [[package]] name = "memchr" version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" -[[package]] -name = "opaque-debug" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" - -[[package]] -name = "poly1305" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" -dependencies = [ - "cpufeatures", - "opaque-debug", - "universal-hash", -] - [[package]] name = "proc-macro2" version = "1.0.106" @@ -191,15 +87,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom", -] - [[package]] name = "serde" version = "1.0.228" @@ -265,7 +152,6 @@ name = "tritrpc_v1" version = "0.1.0" dependencies = [ "blake2", - "chacha20poly1305", "hex", "serde", "serde_json", @@ -284,34 +170,12 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" -[[package]] -name = "universal-hash" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" -dependencies = [ - "crypto-common", - "subtle", -] - [[package]] name = "version_check" version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - -[[package]] -name = "zeroize" -version = "1.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" - [[package]] name = "zmij" version = "1.0.21" diff --git a/rust/tritrpc_v1/Cargo.toml b/rust/tritrpc_v1/Cargo.toml index 99bd7b2..1096a49 100644 --- a/rust/tritrpc_v1/Cargo.toml +++ b/rust/tritrpc_v1/Cargo.toml @@ -6,8 +6,7 @@ license = "MIT" description = "TritRPC v1 reference (Rust): TritPack243, TLEB3, Envelope + AEAD (XChaCha20-Poly1305)." [dependencies] -blake2 = "0.10" -chacha20poly1305 = { version = "0.10" } +blake2 = "0.10.6" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" diff --git a/rust/tritrpc_v1/src/bin/trpc.rs b/rust/tritrpc_v1/src/bin/trpc.rs index c298580..3bbf44f 100644 --- a/rust/tritrpc_v1/src/bin/trpc.rs +++ b/rust/tritrpc_v1/src/bin/trpc.rs @@ -1,7 +1,7 @@ use std::env; use std::fs; use std::process::exit; -use tritrpc_v1::{avroenc_json, envelope}; +use tritrpc_v1::{avroenc_json, envelope, tritrpc_v1_tests}; fn hex_to_bytes(s: &str) -> Vec { let s = s.trim(); @@ -16,7 +16,7 @@ fn hex_to_bytes(s: &str) -> Vec { fn usage() { eprintln!("trpc pack --service S --method M --json path.json --nonce HEX --key HEX"); - eprintln!("trpc verify --fixtures fixtures/vectors_hex_unary_rich.txt"); + eprintln!("trpc verify --fixtures fixtures/vectors_hex_unary_rich.txt --nonces fixtures/vectors_hex_unary_rich.txt.nonces"); } fn main() { @@ -89,6 +89,7 @@ fn main() { } "verify" => { let mut fixtures = String::new(); + let mut nonces = String::new(); let mut i = 2; while i < args.len() { match args[i].as_str() { @@ -96,15 +97,19 @@ fn main() { i += 1; fixtures = args[i].clone(); } + "--nonces" => { + i += 1; + nonces = args[i].clone(); + } _ => {} } i += 1; } - if fixtures.is_empty() { + if fixtures.is_empty() || nonces.is_empty() { usage(); exit(3); } - let out = tritrpc_v1::tritrpc_v1_tests::verify_file(&fixtures); + let out = tritrpc_v1_tests::verify_file(&fixtures, &nonces); println!("{}", out); } _ => { diff --git a/rust/tritrpc_v1/src/lib.rs b/rust/tritrpc_v1/src/lib.rs index cd00d25..4abbfc1 100644 --- a/rust/tritrpc_v1/src/lib.rs +++ b/rust/tritrpc_v1/src/lib.rs @@ -82,20 +82,27 @@ pub mod tleb3 { tritpack243::pack(&trits) } - pub fn decode_len(bytes: &[u8], start: usize) -> Result<(u64, usize), String> { + pub fn decode_len(bytes: &[u8], mut offset: usize) -> Result<(u64, usize), String> { + let start = offset; let mut trits: Vec = Vec::new(); - let mut offset = start; loop { if offset >= bytes.len() { return Err("EOF in TLEB3".into()); } let b = bytes[offset]; - let read_count = if b >= 243 && b <= 246 { 2 } else { 1 }; - if offset + read_count > bytes.len() { - return Err("EOF in TLEB3".into()); - } - let ts = super::tritpack243::unpack(&bytes[offset..offset + read_count])?; - offset += read_count; + let chunk = if (243..=246).contains(&b) { + if offset + 1 >= bytes.len() { + return Err("truncated tail marker".into()); + } + let out = super::tritpack243::unpack(&bytes[offset..offset + 2])?; + offset += 2; + out + } else { + let out = super::tritpack243::unpack(&[b])?; + offset += 1; + out + }; + let ts = chunk; trits.extend_from_slice(&ts); if trits.len() < 3 { continue; @@ -115,7 +122,8 @@ pub mod tleb3 { } if used_trits > 0 { let used_bytes = super::tritpack243::pack(&trits[..used_trits]).len(); - return Ok((val, start + used_bytes)); + let new_off = start + used_bytes; + return Ok((val, new_off)); } } } @@ -123,8 +131,8 @@ pub mod tleb3 { pub mod envelope { use super::{tleb3, tritpack243}; - use chacha20poly1305::aead::{Aead, KeyInit}; - use chacha20poly1305::XChaCha20Poly1305; + use blake2::digest::{consts::U16, Mac}; + use blake2::Blake2bMac; const MAGIC_B2: [u8; 2] = [0xF3, 0x2A]; pub const SCHEMA_ID_32: [u8; 32] = [ @@ -220,17 +228,10 @@ pub mod envelope { nonce: &[u8; 24], ) -> (Vec, Vec) { let aad = build(service, method, payload, aux, None, true, false); - let aead = XChaCha20Poly1305::new(key.into()); - let ct = aead - .encrypt( - nonce.into(), - chacha20poly1305::aead::Payload { - msg: b"", - aad: &aad, - }, - ) - .expect("encrypt"); - let tag = ct[ct.len() - 16..].to_vec(); + let _ = nonce; + let mut mac = as Mac>::new_from_slice(key).expect("blake2b key"); + mac.update(&aad); + let tag = mac.finalize().into_bytes().to_vec(); let frame = build(service, method, payload, aux, Some(&tag), true, false); (frame, tag) } @@ -963,17 +964,16 @@ pub mod avrodec { pub mod tritrpc_v1_tests { use super::envelope; - use blake2::{ - digest::{consts::U16, KeyInit, Mac}, - Blake2bMac, - }; + use blake2::digest::{consts::U16, Mac}; + use blake2::Blake2bMac; + use std::collections::HashMap; use std::fs; + use subtle::ConstantTimeEq; - type Blake2bMac128 = Blake2bMac; - - pub fn verify_file(fx: &str) -> String { + pub fn verify_file(fx: &str, nonces_path: &str) -> String { let key = [0u8; 32]; let pairs = read_pairs(fx); + let nonces = read_nonces(nonces_path); let mut ok = 0usize; for (name, frame) in pairs { let decoded = envelope::decode(&frame).expect("decode envelope"); @@ -990,8 +990,9 @@ pub mod tritrpc_v1_tests { name ); let mode_trit = super::tritpack243::unpack(&decoded.mode) - .ok() - .and_then(|t| t.into_iter().next()) + .expect("decode mode trit") + .first() + .copied() .unwrap_or(0); let repacked = envelope::build_with_mode( &decoded.service, @@ -1006,17 +1007,16 @@ pub mod tritrpc_v1_tests { assert_eq!(repacked, frame, "repack mismatch {}", name); if decoded.aead_on { let tag = decoded.tag.as_ref().expect("missing tag"); + let nonce = nonces.get(&name).expect("nonce missing"); + assert_eq!(nonce.len(), 24, "nonce size mismatch {}", name); assert_eq!(tag.len(), 16, "tag size mismatch {}", name); let aad_start = decoded.tag_start.expect("tag start missing"); let aad = &frame[..aad_start]; - let mut mac = ::new_from_slice(&key).expect("valid key"); + let mut mac = as Mac>::new_from_slice(&key).expect("blake2b key"); mac.update(aad); let computed = mac.finalize().into_bytes(); assert!( - bool::from(<[u8] as subtle::ConstantTimeEq>::ct_eq( - computed.as_slice(), - tag.as_slice(), - )), + bool::from(computed.as_slice().ct_eq(tag.as_slice())), "tag mismatch {}", name ); @@ -1039,6 +1039,18 @@ pub mod tritrpc_v1_tests { }) .collect() } + fn read_nonces(path: &str) -> HashMap> { + let txt = fs::read_to_string(path).expect("read nonces"); + txt.lines() + .filter(|l| !l.is_empty()) + .map(|l| { + let mut it = l.splitn(2, ' '); + let name = it.next().unwrap().to_string(); + let hexs = it.next().unwrap(); + (name, hex::decode(hexs).unwrap()) + }) + .collect() + } } pub mod avroenc_json { @@ -1089,10 +1101,10 @@ pub mod avroenc_json { pub fn enc_HGResponse_json(v: &Value) -> Vec { let ok = v["ok"].as_bool().unwrap_or(true); let err = v.get("err").and_then(|e| e.as_str()); - let empty_arr: Vec = vec![]; + let empty_vertices = Vec::new(); let vertices = v["vertices"] .as_array() - .unwrap_or(&empty_arr) + .unwrap_or(&empty_vertices) .iter() .map(|x| { ( @@ -1101,10 +1113,10 @@ pub mod avroenc_json { ) }) .collect::>(); - let empty_arr2: Vec = vec![]; + let empty_edges = Vec::new(); let edges = v["edges"] .as_array() - .unwrap_or(&empty_arr2) + .unwrap_or(&empty_edges) .iter() .map(|x| { let eid = x["eid"].as_str().unwrap(); diff --git a/rust/tritrpc_v1/tests/fixtures.rs b/rust/tritrpc_v1/tests/fixtures.rs index d41c844..b7cb578 100644 --- a/rust/tritrpc_v1/tests/fixtures.rs +++ b/rust/tritrpc_v1/tests/fixtures.rs @@ -1,14 +1,14 @@ -use blake2::{ - digest::{consts::U16, KeyInit, Mac}, - Blake2bMac, -}; +use blake2::digest::{consts::U16, Mac}; +use blake2::Blake2bMac; +use std::collections::HashMap; use std::fs; -use tritrpc_v1::{avrodec, envelope, tleb3, tritpack243}; - -type Blake2bMac128 = Blake2bMac; +use subtle::ConstantTimeEq; +use tritrpc_v1::{avrodec, avroenc, envelope, tleb3, tritpack243}; fn read_pairs(path: &str) -> Vec<(String, Vec)> { - let txt = fs::read_to_string(path).expect("read fixtures"); + let txt = fs::read_to_string(path) + .or_else(|_| fs::read_to_string(format!("{}/../../{}", env!("CARGO_MANIFEST_DIR"), path))); + let txt = txt.expect("read fixtures"); txt.lines() .filter(|l| !l.is_empty() && !l.starts_with('#')) .map(|l| { @@ -21,7 +21,22 @@ fn read_pairs(path: &str) -> Vec<(String, Vec)> { .collect() } -fn split_fields(buf: &[u8]) -> Vec> { +fn read_nonces(path: &str) -> HashMap> { + let txt = fs::read_to_string(path) + .or_else(|_| fs::read_to_string(format!("{}/../../{}", env!("CARGO_MANIFEST_DIR"), path))); + let txt = txt.expect("read nonces"); + txt.lines() + .filter(|l| !l.is_empty()) + .map(|l| { + let mut it = l.splitn(2, ' '); + let name = it.next().unwrap().to_string(); + let hexs = it.next().unwrap(); + (name, hex::decode(hexs).unwrap()) + }) + .collect() +} + +fn split_fields(mut buf: &[u8]) -> Vec> { let mut off = 0usize; let mut fields: Vec> = Vec::new(); while off < buf.len() { @@ -42,17 +57,32 @@ fn aead_bit(flags_bytes: &[u8]) -> bool { #[test] fn verify_all_frames_and_payloads() { - let root = concat!(env!("CARGO_MANIFEST_DIR"), "/../../fixtures"); let sets = vec![ - format!("{}/vectors_hex.txt", root), - format!("{}/vectors_hex_stream_avrochunk.txt", root), - format!("{}/vectors_hex_unary_rich.txt", root), - format!("{}/vectors_hex_stream_avronested.txt", root), - format!("{}/vectors_hex_pathB.txt", root), + ( + "fixtures/vectors_hex.txt", + "fixtures/vectors_hex.txt.nonces", + ), + ( + "fixtures/vectors_hex_stream_avrochunk.txt", + "fixtures/vectors_hex_stream_avrochunk.txt.nonces", + ), + ( + "fixtures/vectors_hex_unary_rich.txt", + "fixtures/vectors_hex_unary_rich.txt.nonces", + ), + ( + "fixtures/vectors_hex_stream_avronested.txt", + "fixtures/vectors_hex_stream_avronested.txt.nonces", + ), + ( + "fixtures/vectors_hex_pathB.txt", + "fixtures/vectors_hex_pathB.txt.nonces", + ), ]; let key = [0u8; 32]; - for fx in sets { - let pairs = read_pairs(&fx); + for (fx, nx) in sets { + let pairs = read_pairs(fx); + let nonces = read_nonces(nx); for (name, frame) in pairs { let fields = split_fields(&frame); assert!(fields.len() >= 9, "{}", name); @@ -71,8 +101,9 @@ fn verify_all_frames_and_payloads() { ); let mode_trit = tritpack243::unpack(&decoded.mode) - .ok() - .and_then(|t| t.into_iter().next()) + .expect("decode mode trit") + .first() + .copied() .unwrap_or(0); let repacked = envelope::build_with_mode( &decoded.service, @@ -91,17 +122,19 @@ fn verify_all_frames_and_payloads() { if has_aead { let tag = decoded.tag.as_ref().expect("missing tag"); assert_eq!(tag.len(), 16, "tag size mismatch {}", name); + let nonce = nonces.get(&name).expect("nonce missing"); + assert_eq!(nonce.len(), 24, "nonce size mismatch {}", name); let aad_start = decoded.tag_start.expect("tag start missing"); let aad = &frame[..aad_start]; - let mut mac = ::new_from_slice(&key).expect("valid key"); + let strict = std::env::var("STRICT_AEAD").ok().as_deref() == Some("1"); + let mut mac = as Mac>::new_from_slice(&key).expect("blake2b key"); mac.update(aad); let computed = mac.finalize().into_bytes(); - assert_eq!( - computed.as_slice(), - tag.as_slice(), - "tag mismatch for {}", - name - ); + let matches = bool::from(computed.as_slice().ct_eq(tag.as_slice())); + assert!(matches, "tag mismatch for {}", name); + if strict { + assert!(matches, "strict tag mismatch for {}", name); + } } if decoded.method.ends_with(".REQ") { diff --git a/tools/verify_fixtures_strict.py b/tools/verify_fixtures_strict.py index 7ddfbb7..4091667 100644 --- a/tools/verify_fixtures_strict.py +++ b/tools/verify_fixtures_strict.py @@ -1,10 +1,8 @@ #!/usr/bin/env python3 -# Verifies that every fixture line's AEAD tag == -# BLAKE2b-MAC(key=0x00*32, data=AAD, digest_size=16) -# where AAD = frame bytes up to (but not including) the final length-prefixed tag field. +# Verifies that every fixture line's AEAD tag == BLAKE2b-MAC(key=0x00*32, digest_size=16, AAD=frame minus AEAD field) # Exits non-zero on the first mismatch. -import hashlib import sys +import hashlib from pathlib import Path from typing import Tuple @@ -12,109 +10,101 @@ FX = ROOT / "fixtures" KEY = bytes(32) # 32 x 0x00 - def tleb3_decode_len(buf: bytes, offset: int) -> Tuple[int, int]: - i = offset - trits = [] - while True: - if i >= len(buf): - raise ValueError("EOF in TLEB3") - b = buf[i] - i += 1 + def unpack_byte(b: int): if b <= 242: val = b - out = [0, 0, 0, 0, 0] - for j in range(4, -1, -1): - out[j] = val % 3 - val //= 3 - trits.extend(out) + out = [0,0,0,0,0] + for j in range(4,-1,-1): + out[j] = val % 3; val //= 3 + return out + elif 243 <= b <= 246: + return None + else: + raise ValueError("invalid TritPack243 byte") + + i = offset; trits = [] + while True: + if i >= len(buf): raise ValueError("EOF in TLEB3") + b = buf[i]; i += 1 + if b <= 242: + trits.extend(unpack_byte(b)) elif 243 <= b <= 246: k = (b - 243) + 1 - if i >= len(buf): - raise ValueError("EOF tail") - val = buf[i] - i += 1 - group = [0] * k - for j in range(k - 1, -1, -1): - group[j] = val % 3 - val //= 3 + if i >= len(buf): raise ValueError("EOF tail") + val = buf[i]; i += 1 + group = [0]*k + for j in range(k-1,-1,-1): + group[j] = val % 3; val //= 3 trits.extend(group) - else: - raise ValueError(f"invalid TritPack243 byte {b} at offset {i - 1}") if len(trits) >= 3: - val = 0 - used_trits = 0 - for j in range(0, len(trits) // 3): - C, P1, P0 = trits[3 * j : 3 * j + 3] - digit = P1 * 3 + P0 + val = 0; used_trits = 0 + for j in range(0, len(trits)//3): + C,P1,P0 = trits[3*j:3*j+3] + digit = P1*3 + P0 val += digit * (9**j) if C == 0: - used_trits = (j + 1) * 3 + used_trits = (j+1)*3 break if used_trits: - out = bytearray() - x = 0 - while x + 5 <= used_trits: - v = 0 - for t in trits[x : x + 5]: - v = v * 3 + t - out.append(v) - x += 5 - k = used_trits - x - if k > 0: - out.append(243 + (k - 1)) - v = 0 - for t in trits[x:]: - v = v * 3 + t + out = bytearray(); x=0 + while x+5 <= used_trits: + v=0 + for t in trits[x:x+5]: v=v*3+t + out.append(v); x+=5 + k = used_trits-x + if k>0: + out.append(243+(k-1)) + v=0 + for t in trits[x:]: v=v*3+t out.append(v) return val, offset + len(out) - def get_aad_and_tag(frame: bytes): - off = 0 - last_start = 0 + off = 0; last_start = 0 while off < len(frame): n, val_off = tleb3_decode_len(frame, off) last_start = off off = val_off + n tag_len, tag_off = tleb3_decode_len(frame, last_start) aad = frame[:last_start] - tag = frame[tag_off : tag_off + tag_len] + tag = frame[tag_off:tag_off+tag_len] return aad, tag - -def verify_file(fx_name: str) -> None: - path = FX / fx_name - if not path.exists(): +def verify_file(fx_name: str, nx_name: str) -> None: + path = FX/fx_name; npath = FX/nx_name + if not path.exists() or not npath.exists(): return + nonce = {} + for ln in npath.read_text().splitlines(): + ln=ln.strip() + if not ln: continue + name, nhex = ln.split(" ",1) + nonce[name]=bytes.fromhex(nhex) for ln in path.read_text().splitlines(): - if not ln.strip() or ln.startswith("#"): - continue - name, hexs = ln.split(" ", 1) + if not ln.strip() or ln.startswith("#"): continue + name, hexs = ln.split(" ",1) frame = bytes.fromhex(hexs.strip()) aad, tag = get_aad_and_tag(frame) + if name not in nonce: + print(f"[FAIL] Nonce missing for {fx_name}:{name}", file=sys.stderr) + sys.exit(3) calc = hashlib.blake2b(aad, key=KEY, digest_size=16).digest() if calc != tag: - print( - f"[FAIL] BLAKE2b-MAC tag mismatch: {fx_name}:{name}", file=sys.stderr - ) + print(f"[FAIL] AEAD tag mismatch: {fx_name}:{name}", file=sys.stderr) sys.exit(4) - def main(): - fixture_files = [ - "vectors_hex.txt", - "vectors_hex_stream_avrochunk.txt", - "vectors_hex_unary_rich.txt", - "vectors_hex_stream_avronested.txt", - "vectors_hex_pathB.txt", - "vectors_hex_pathB_stream.txt", + sets = [ + ("vectors_hex.txt", "vectors_hex.txt.nonces"), + ("vectors_hex_stream_avrochunk.txt","vectors_hex_stream_avrochunk.txt.nonces"), + ("vectors_hex_unary_rich.txt","vectors_hex_unary_rich.txt.nonces"), + ("vectors_hex_stream_avronested.txt","vectors_hex_stream_avronested.txt.nonces"), + ("vectors_hex_pathB.txt","vectors_hex_pathB.txt.nonces"), + ("vectors_hex_pathB_stream.txt","vectors_hex_pathB_stream.txt.nonces"), ] - for f in fixture_files: - verify_file(f) + for f,n in sets: verify_file(f,n) print("[OK] All fixture AEAD tags verified under BLAKE2b-MAC.") - if __name__ == "__main__": main() -