The verification-service and testagent-transform clients both call
`GET /api/v1/log/entries/{content_hash}` to read a stored envelope and
inclusion proof back out of the log, but the server previously only
implemented the POST (write) half of ADR-0005. Add:
- `Backend.Get` plus `backend.Entry` and `ErrEntryNotFound` so the
memory backend can serve the read; Trillian stays stubbed and
continues returning 503 (`ErrTrillianUnavailable`) until a later
subtask wires the real read path.
- `GET /api/v1/log/entries/{hash}` on the HTTP server, routing 404 /
503 / 500 with the same error envelope as the POST half.
- `GET /healthz` so the Phase-1 smoke-test harness can wait for the
service to come up instead of racing against its bind.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
156 lines
4.9 KiB
Go
156 lines
4.9 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
|
|
envelopesByIndex map[int64]Envelope // original envelope per index (for Get)
|
|
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{},
|
|
envelopesByIndex: map[int64]Envelope{},
|
|
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()
|
|
m.envelopesByIndex[idx] = env
|
|
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
|
|
}
|
|
|
|
// Get returns the stored envelope + a fresh inclusion proof against the
|
|
// current tree. Suitable for the ADR-0005 read endpoint.
|
|
func (m *Memory) Get(_ context.Context, contentHash string) (Entry, error) {
|
|
if contentHash == "" {
|
|
return Entry{}, errors.New("transparency-log: empty content_hash")
|
|
}
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
|
|
idx, ok := m.indexByHash[contentHash]
|
|
if !ok {
|
|
return Entry{}, ErrEntryNotFound
|
|
}
|
|
env := m.envelopesByIndex[idx]
|
|
root := merkleTreeHash(m.leaves)
|
|
proof := auditPath(m.leaves, int(idx))
|
|
hashes := make([]string, 0, len(proof))
|
|
for _, h := range proof {
|
|
hashes = append(hashes, sha256Hex(h))
|
|
}
|
|
return Entry{
|
|
ContentHash: contentHash,
|
|
Envelope: env,
|
|
LogIndex: idx,
|
|
LoggedAt: m.loggedAtByIndex[idx],
|
|
InclusionProof: InclusionProof{
|
|
TreeSize: int64(len(m.leaves)),
|
|
TreeRootHash: sha256Hex(root),
|
|
SignedTreeHeadRef: signedTreeHeadRef(int64(len(m.leaves)), root),
|
|
Hashes: hashes,
|
|
},
|
|
}, 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")
|