ProofedMesh/services/transparency-log/pkg/backend/memory.go
CTO 69b3f3012f
All checks were successful
validate / Rego (opa fmt + opa test) (push) Successful in 14s
validate / JSON Schema (ajv) (push) Successful in 20s
validate / Go (vet + test) (push) Successful in 20s
services: add transparency-log Go service (ADR-0005 POST /api/v1/log/entries)
- services/transparency-log/pkg/backend: Backend interface with in-memory
  append-log (RFC 6962 Merkle tree) + Trillian wrapper (prod stub).
- Idempotenz-Pflicht: Wiederaufruf mit identischem content_hash liefert
  200 + gleichen log_index + aktuellen inclusion_proof.
- content_hash-Verifikation serverseitig; Mismatch → 409.
- pkg/httpapi: ADR-0005-konformer Handler (201/200/400/404/409/503).
- cmd/transparency-log: minimaler HTTP-Server, memory default,
  --backend trillian als Stub (503).
- Interop-Test in services/admin-api/internal/tlog/interop_test.go
  fährt die echte transparency-log pkg gegen die admin-api
  HTTPSubmitter — Happy-Path, Idempotenz und content_hash-Mismatch
  abgedeckt.
- go test: 10 Tests transparency-log + 2 Interop-Tests admin-api, go
  vet clean in beiden Modulen.

Folge-Subtask: Trillian-Deployment + Keys (siehe ADR-0005 Open Points).

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-10-25 18:08:53 +00:00

119 lines
3.8 KiB
Go

package backend
import (
"context"
"crypto/sha256"
"encoding/hex"
"errors"
"sync"
"time"
)
// Memory is an in-memory append-only Merkle log suitable for Dev + CI. It
// stores every leaf (envelope) in memory and recomputes inclusion proofs on
// each submit, which is fine at the entry counts expected in tests and
// development. Production uses Trillian via the trillian.go wrapper stub.
//
// Idempotency: a repeated submit with the same contentHash returns the
// originally assigned log_index with a fresh inclusion proof against the
// current tree size.
type Memory struct {
mu sync.Mutex
leaves [][]byte // ordered list of leaf hashes
indexByHash map[string]int64 // contentHash -> log_index (zero-based)
loggedAtByIndex map[int64]time.Time // original append time per index
now func() time.Time
}
// NewMemory returns an empty in-memory backend.
func NewMemory() *Memory {
return &Memory{
indexByHash: map[string]int64{},
loggedAtByIndex: map[int64]time.Time{},
now: func() time.Time { return time.Now().UTC() },
}
}
// WithClock replaces the time source. For tests.
func (m *Memory) WithClock(now func() time.Time) *Memory {
m.now = now
return m
}
// Submit appends (env, contentHash) if the hash is new, otherwise returns
// the existing entry. See Backend.Submit for the fresh semantics.
func (m *Memory) Submit(_ context.Context, env Envelope, contentHash string) (Receipt, bool, error) {
if contentHash == "" {
return Receipt{}, false, errors.New("transparency-log: empty content_hash")
}
// Recompute the hash to verify what the client sent matches the envelope.
recomputed, err := EnvelopeContentHash(env)
if err != nil {
return Receipt{}, false, err
}
if recomputed != contentHash {
return Receipt{}, false, ErrContentHashMismatch
}
// Encode the envelope the same way we hashed it, and use those bytes as
// the leaf material. This way the leaf hash is a cryptographic commitment
// to the exact envelope the client stored.
canonical, err := canonicalJSON(env)
if err != nil {
return Receipt{}, false, err
}
m.mu.Lock()
defer m.mu.Unlock()
fresh := false
idx, exists := m.indexByHash[contentHash]
if !exists {
idx = int64(len(m.leaves))
m.leaves = append(m.leaves, leafHash(canonical))
m.indexByHash[contentHash] = idx
m.loggedAtByIndex[idx] = m.now()
fresh = true
}
root := merkleTreeHash(m.leaves)
proof := auditPath(m.leaves, int(idx))
rootHex := sha256Hex(root)
headRef := signedTreeHeadRef(int64(len(m.leaves)), root)
hashes := make([]string, 0, len(proof))
for _, h := range proof {
hashes = append(hashes, sha256Hex(h))
}
return Receipt{
LogIndex: idx,
ContentHash: contentHash,
LoggedAt: m.loggedAtByIndex[idx],
InclusionProof: InclusionProof{
TreeSize: int64(len(m.leaves)),
TreeRootHash: rootHex,
SignedTreeHeadRef: headRef,
Hashes: hashes,
},
}, fresh, nil
}
// signedTreeHeadRef is a stand-in for the real STH reference. In the
// in-memory backend we deterministically derive it from (tree_size, root),
// so callers get a stable per-head identifier without needing a signer.
// The Trillian wrapper will return the actual signed head ref.
func signedTreeHeadRef(size int64, root []byte) string {
h := sha256.New()
// Little-endian uint64 encoding of size, then root bytes.
for i := 0; i < 8; i++ {
h.Write([]byte{byte(size >> (8 * i))})
}
h.Write(root)
return "sha256:" + hex.EncodeToString(h.Sum(nil))
}
// ErrContentHashMismatch is returned when the client-supplied content_hash
// does not match the SHA-256 of the serialised envelope. ADR-0005 says the
// transparency log MUST reject this with 409.
var ErrContentHashMismatch = errors.New("transparency-log: content_hash does not match envelope")