The same open-standards posture that turned JCR-1 into a standard
(three independent implementations agreeing byte-for-byte across
canonical CBOR + COSE_Sign1), now applied to policy receipts. A
policy receipt is a real spec, not a Rust house format.
## What ships
- conformance/vectors-policy.json — 3 vectors spanning the
decision space: Allow with named rule, Explicit deny with named
rule, and No-matching-allow (deny-by-default). Each carries the
receipt JSON, the canonical signed-core CBOR hex, and the
COSE_Sign1 envelope hex.
- conformance/verify.py — Python verifier (cbor2 + cryptography
only). Re-encodes each canonical core, signs a fresh envelope
under the same seed, asserts byte-equal against committed hex.
- conformance/verify.go — Go verifier (stdlib only, no go.mod).
From-scratch RFC 8949 §4.2.1 length-first CBOR encoder + COSE
reader. Stdlib crypto/ed25519 verifies the envelope.
- conformance/README.md — vector descriptions, regeneration
procedure, scope.
- examples/gen_policy_vectors.rs — Rust regenerator. Run with a
path argument to write the vectors JSON; argument-less prints
to stdout for inspection.
- Embedded POLICY_VECTORS_JSON const + the
policy_vectors_match_committed_conformance regression test in
src/lib.rs — a drifting Rust producer fails BEFORE the Python
and Go verifiers ever run.
## Cross-implementation verification
All three implementations cross-verified locally on the same vectors:
Rust (cargo test policy_vectors_match_committed_conformance): pass
Python (python3 verify.py): pass on 3/3 vectors
Go (go run verify.go): pass on 3/3 vectors
Same seed JCR-1 uses for cascade-receipt vectors, so a third party
can cross-validate both receipt families under a single test key.
## Workspace
102 crates, 2,533 lib tests pass (+1 for the conformance regression),
0 failures.
330 lines
7.5 KiB
Go
330 lines
7.5 KiB
Go
// Independent Go verifier for JouleClaw policy-receipt vectors.
|
|
//
|
|
// Stdlib only — no go.mod required. From-scratch canonical CBOR encoder
|
|
// (RFC 8949 §4.2.1 length-first map ordering), minimal COSE_Sign1
|
|
// reader, crypto/ed25519. Three independent implementations (Rust +
|
|
// Python + Go) MUST produce bit-identical bytes on every vector.
|
|
//
|
|
// Usage: go run verify.go [vectors-policy.json]
|
|
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/ed25519"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"sort"
|
|
"strconv"
|
|
)
|
|
|
|
// ---------- canonical CBOR encoder (subset: uint, text, bstr, array, map) ----
|
|
|
|
func cborUint(buf *bytes.Buffer, major byte, n uint64) {
|
|
mt := major << 5
|
|
switch {
|
|
case n < 24:
|
|
buf.WriteByte(mt | byte(n))
|
|
case n < 1<<8:
|
|
buf.WriteByte(mt | 24)
|
|
buf.WriteByte(byte(n))
|
|
case n < 1<<16:
|
|
buf.WriteByte(mt | 25)
|
|
buf.WriteByte(byte(n >> 8))
|
|
buf.WriteByte(byte(n))
|
|
case n < 1<<32:
|
|
buf.WriteByte(mt | 26)
|
|
for s := 24; s >= 0; s -= 8 {
|
|
buf.WriteByte(byte(n >> uint(s)))
|
|
}
|
|
default:
|
|
buf.WriteByte(mt | 27)
|
|
for s := 56; s >= 0; s -= 8 {
|
|
buf.WriteByte(byte(n >> uint(s)))
|
|
}
|
|
}
|
|
}
|
|
|
|
func cborText(buf *bytes.Buffer, s string) {
|
|
cborUint(buf, 3, uint64(len(s)))
|
|
buf.WriteString(s)
|
|
}
|
|
|
|
func cborBytes(buf *bytes.Buffer, b []byte) {
|
|
cborUint(buf, 2, uint64(len(b)))
|
|
buf.Write(b)
|
|
}
|
|
|
|
func encodeValue(buf *bytes.Buffer, v interface{}) error {
|
|
switch x := v.(type) {
|
|
case string:
|
|
cborText(buf, x)
|
|
case json.Number:
|
|
u, err := strconv.ParseUint(x.String(), 10, 64)
|
|
if err != nil {
|
|
return fmt.Errorf("non-integer in signed core: %s", x.String())
|
|
}
|
|
cborUint(buf, 0, u)
|
|
case bool, float64, nil:
|
|
return fmt.Errorf("disallowed type in signed core: %T", x)
|
|
case []interface{}:
|
|
cborUint(buf, 4, uint64(len(x)))
|
|
for _, e := range x {
|
|
if err := encodeValue(buf, e); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
case map[string]interface{}:
|
|
return encodeMap(buf, x)
|
|
default:
|
|
return fmt.Errorf("unsupported type %T", x)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// RFC 8949 §4.2.1 ordering — length-first, then bytewise on key.
|
|
func encodeMap(buf *bytes.Buffer, m map[string]interface{}) error {
|
|
keys := make([]string, 0, len(m))
|
|
for k := range m {
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Slice(keys, func(i, j int) bool {
|
|
if len(keys[i]) != len(keys[j]) {
|
|
return len(keys[i]) < len(keys[j])
|
|
}
|
|
return keys[i] < keys[j]
|
|
})
|
|
cborUint(buf, 5, uint64(len(keys)))
|
|
for _, k := range keys {
|
|
cborText(buf, k)
|
|
if err := encodeValue(buf, m[k]); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// signedCore: canonical-encode the policy-receipt map verbatim. Unlike
|
|
// JCR-1 v1, there are no excluded fields in policy/1.
|
|
func signedCore(receipt map[string]interface{}) ([]byte, error) {
|
|
var buf bytes.Buffer
|
|
if err := encodeMap(&buf, receipt); err != nil {
|
|
return nil, err
|
|
}
|
|
return buf.Bytes(), nil
|
|
}
|
|
|
|
// ---------- minimal CBOR reader (only what COSE_Sign1 needs) ----------------
|
|
|
|
type reader struct {
|
|
b []byte
|
|
pos int
|
|
}
|
|
|
|
func (r *reader) byte() (byte, error) {
|
|
if r.pos >= len(r.b) {
|
|
return 0, fmt.Errorf("eof")
|
|
}
|
|
c := r.b[r.pos]
|
|
r.pos++
|
|
return c, nil
|
|
}
|
|
|
|
func (r *reader) head() (major byte, arg uint64, err error) {
|
|
c, err := r.byte()
|
|
if err != nil {
|
|
return
|
|
}
|
|
major = c >> 5
|
|
ai := c & 0x1f
|
|
switch {
|
|
case ai < 24:
|
|
arg = uint64(ai)
|
|
case ai == 24:
|
|
var v byte
|
|
v, err = r.byte()
|
|
arg = uint64(v)
|
|
case ai == 25:
|
|
for i := 0; i < 2; i++ {
|
|
var v byte
|
|
if v, err = r.byte(); err != nil {
|
|
return
|
|
}
|
|
arg = arg<<8 | uint64(v)
|
|
}
|
|
case ai == 26:
|
|
for i := 0; i < 4; i++ {
|
|
var v byte
|
|
if v, err = r.byte(); err != nil {
|
|
return
|
|
}
|
|
arg = arg<<8 | uint64(v)
|
|
}
|
|
case ai == 27:
|
|
for i := 0; i < 8; i++ {
|
|
var v byte
|
|
if v, err = r.byte(); err != nil {
|
|
return
|
|
}
|
|
arg = arg<<8 | uint64(v)
|
|
}
|
|
default:
|
|
err = fmt.Errorf("unsupported additional info %d", ai)
|
|
}
|
|
return
|
|
}
|
|
|
|
func (r *reader) bstr() ([]byte, error) {
|
|
major, n, err := r.head()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if major != 2 {
|
|
return nil, fmt.Errorf("expected bstr, got major %d", major)
|
|
}
|
|
if r.pos+int(n) > len(r.b) {
|
|
return nil, fmt.Errorf("bstr overrun")
|
|
}
|
|
out := r.b[r.pos : r.pos+int(n)]
|
|
r.pos += int(n)
|
|
return out, nil
|
|
}
|
|
|
|
func parseCOSESign1(b []byte) (protected, payload, signature []byte, err error) {
|
|
r := &reader{b: b}
|
|
major, tag, err := r.head()
|
|
if err != nil {
|
|
return
|
|
}
|
|
if major == 6 {
|
|
if tag != 18 {
|
|
err = fmt.Errorf("unexpected tag %d", tag)
|
|
return
|
|
}
|
|
major, tag, err = r.head()
|
|
if err != nil {
|
|
return
|
|
}
|
|
}
|
|
if major != 4 || tag != 4 {
|
|
err = fmt.Errorf("expected 4-element array, got major %d len %d", major, tag)
|
|
return
|
|
}
|
|
if protected, err = r.bstr(); err != nil {
|
|
return
|
|
}
|
|
if m, a, e := r.head(); e != nil || m != 5 {
|
|
err = fmt.Errorf("expected map for unprotected header (major %d)", m)
|
|
_ = a
|
|
return
|
|
}
|
|
if payload, err = r.bstr(); err != nil {
|
|
return
|
|
}
|
|
signature, err = r.bstr()
|
|
return
|
|
}
|
|
|
|
// ---------- verification --------------------------------------------------
|
|
|
|
type TestKey struct {
|
|
SeedHex string `json:"ed25519_seed_hex"`
|
|
PubHex string `json:"ed25519_public_raw_hex"`
|
|
Kid string `json:"steward_kid"`
|
|
}
|
|
|
|
type Vectors struct {
|
|
SchemaID string `json:"schema_id"`
|
|
VectorCount int `json:"vector_count"`
|
|
Envelope string `json:"envelope"`
|
|
TestKey TestKey `json:"test_key"`
|
|
Vectors []struct {
|
|
N int `json:"n"`
|
|
Description string `json:"description"`
|
|
Receipt map[string]interface{} `json:"receipt"`
|
|
CoreHex string `json:"signed_core_cbor_hex"`
|
|
CoseHex string `json:"cose_sign1_hex"`
|
|
} `json:"vectors"`
|
|
}
|
|
|
|
func verify(rec map[string]interface{}, coreHex, coseHex string, pub ed25519.PublicKey) []string {
|
|
var errs []string
|
|
|
|
core, err := signedCore(rec)
|
|
if err != nil {
|
|
errs = append(errs, "encode error: "+err.Error())
|
|
}
|
|
want, _ := hex.DecodeString(coreHex)
|
|
if !bytes.Equal(core, want) {
|
|
errs = append(errs, "signed core re-encode mismatch")
|
|
}
|
|
|
|
coseBytes, _ := hex.DecodeString(coseHex)
|
|
protected, payload, signature, perr := parseCOSESign1(coseBytes)
|
|
if perr != nil {
|
|
errs = append(errs, "cose parse: "+perr.Error())
|
|
} else {
|
|
if !bytes.Equal(payload, want) {
|
|
errs = append(errs, "envelope payload != signed core")
|
|
}
|
|
var sig bytes.Buffer
|
|
sig.WriteByte(0x84) // array(4)
|
|
cborText(&sig, "Signature1")
|
|
cborBytes(&sig, protected)
|
|
cborBytes(&sig, []byte{})
|
|
cborBytes(&sig, payload)
|
|
if !ed25519.Verify(pub, sig.Bytes(), signature) {
|
|
errs = append(errs, "ed25519 verification failed")
|
|
}
|
|
}
|
|
return errs
|
|
}
|
|
|
|
func mustDecodeJSON(path string, v interface{}) {
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
defer f.Close()
|
|
dec := json.NewDecoder(f)
|
|
dec.UseNumber()
|
|
if err := dec.Decode(v); err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
|
|
func main() {
|
|
vecPath := "vectors-policy.json"
|
|
if len(os.Args) > 1 {
|
|
vecPath = os.Args[1]
|
|
}
|
|
var vs Vectors
|
|
mustDecodeJSON(vecPath, &vs)
|
|
|
|
pubBytes, _ := hex.DecodeString(vs.TestKey.PubHex)
|
|
pub := ed25519.PublicKey(pubBytes)
|
|
|
|
fmt.Printf("JouleClaw policy-receipt independent Go verifier — %d vectors — schema %s — %s\n\n",
|
|
vs.VectorCount, vs.SchemaID, vs.Envelope)
|
|
pass := 0
|
|
for _, v := range vs.Vectors {
|
|
errs := verify(v.Receipt, v.CoreHex, v.CoseHex, pub)
|
|
ok := len(errs) == 0
|
|
if ok {
|
|
pass++
|
|
}
|
|
status := "PASS"
|
|
if !ok {
|
|
status = "FAIL"
|
|
}
|
|
fmt.Printf(" [%s] vector %2d: %s\n", status, v.N, v.Description)
|
|
for _, e := range errs {
|
|
fmt.Printf(" - %s\n", e)
|
|
}
|
|
}
|
|
fmt.Printf("\n%d/%d vectors verified by the independent Go implementation\n", pass, vs.VectorCount)
|
|
if pass != vs.VectorCount {
|
|
os.Exit(1)
|
|
}
|
|
}
|