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")