ProofedMesh/services/verification-service/internal/chain/helpers_test.go
CTO (Paperclip) 0ae29ae342
Some checks failed
validate / Rego (opa fmt + opa test) (push) Successful in 16s
validate / JSON Schema (ajv) (push) Successful in 20s
build-images / signing-service image (push) Successful in 24s
validate / DB (migrate up/down dry-run) (push) Failing after 26s
validate / Go (vet + test) (push) Successful in 1m16s
verification-service: Phase-1 two-stage chain verifier (approval -> transform) (DEV-612)
Adds services/verification-service exposing POST /api/v1/verify/chain
for sink:dummy-sink. The handler runs the six hard-coded chain checks
described in DEV-612 and reserves decision_id in a new
`verification_decisions` table so a replayed sink delivery is refused
without recomputing signatures.

- DSSE signature verification against configurable approval + transform
  signer key_ids, loaded from VERIFICATION_SIGNER_KEYS (pem/hex/b64/b64u)
- RFC 6962 inclusion-proof verifier (shared leaf/node hashing with
  services/transparency-log/pkg/backend)
- Content binding: subject.digest.sha256 must match predicate.content_hash
  (approval) and request.payload_hash (transform)
- Hard-coded coverage of allowed_transformations / allowed_sinks /
  expires_at predicates (OPA deferred to Phase 2 per the subtask)
- Replay guard via new db/trust/migrations/0005_verification_decisions
  (in-memory store ships; Postgres skeleton mirrors sibling pattern)
- Dockerfile mirrors signing-service pattern (distroless static + nonroot)
- README documents endpoint, reason-code taxonomy, required predicate
  fields, and the Phase-2 non-scope

Tests: happy path + 7 negatives (bad signature, missing inclusion proof,
hash drift, sink not allowed, expired, replay, operation not allowed).

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-25 20:35:35 +00:00

102 lines
2.4 KiB
Go

package chain
import (
"crypto/sha256"
"encoding/hex"
"basicstack.de/proofedmesh/services/verification-service/internal/canonical"
"basicstack.de/proofedmesh/services/verification-service/internal/dsse"
"basicstack.de/proofedmesh/services/verification-service/internal/tlog"
)
// memTree is a test-only Merkle tree mirroring the transparency-log's
// in-memory backend. It lives in the chain tests (not under internal/tlog)
// so the proof verifier under test does not share code with the proof
// producer.
type memTree struct {
leaves [][]byte
}
func (t *memTree) add(env dsse.Envelope) tlog.Entry {
leafBytes, _ := canonical.Marshal(env)
t.leaves = append(t.leaves, leafHash(leafBytes))
idx := int64(len(t.leaves) - 1)
hash := "sha256:" + canonical.SHA256Hex(leafBytes)
return tlog.Entry{
ContentHash: hash,
Envelope: env,
LogIndex: idx,
InclusionProof: tlog.InclusionProof{},
}
}
func (t *memTree) refreshProofs(entries ...*tlog.Entry) {
root := merkleTreeHashT(t.leaves)
rootStr := "sha256:" + hex.EncodeToString(root)
for _, e := range entries {
path := auditPathT(t.leaves, int(e.LogIndex))
hashes := make([]string, 0, len(path))
for _, h := range path {
hashes = append(hashes, "sha256:"+hex.EncodeToString(h))
}
e.InclusionProof = tlog.InclusionProof{
TreeSize: int64(len(t.leaves)),
TreeRootHash: rootStr,
Hashes: hashes,
}
}
}
func leafHash(data []byte) []byte {
h := sha256.New()
h.Write([]byte{0x00})
h.Write(data)
return h.Sum(nil)
}
func nodeHashT(left, right []byte) []byte {
h := sha256.New()
h.Write([]byte{0x01})
h.Write(left)
h.Write(right)
return h.Sum(nil)
}
func merkleTreeHashT(leaves [][]byte) []byte {
switch len(leaves) {
case 0:
s := sha256.Sum256(nil)
return s[:]
case 1:
return append([]byte(nil), leaves[0]...)
}
k := largestPow2LessThanT(len(leaves))
return nodeHashT(merkleTreeHashT(leaves[:k]), merkleTreeHashT(leaves[k:]))
}
func auditPathT(leaves [][]byte, m int) [][]byte {
n := len(leaves)
if n <= 1 {
return nil
}
k := largestPow2LessThanT(n)
if m < k {
return append(auditPathT(leaves[:k], m), merkleTreeHashT(leaves[k:]))
}
return append(auditPathT(leaves[k:], m-k), merkleTreeHashT(leaves[:k]))
}
func largestPow2LessThanT(n int) int {
k := 1
for k < n {
k <<= 1
}
return k >> 1
}
func seedBytes(s string) []byte { return []byte(s) }
func sha(b []byte) []byte {
s := sha256.Sum256(b)
return s[:]
}