transparency-log: add GET /api/v1/log/entries/{hash} + /healthz (DEV-613)
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>
This commit is contained in:
parent
0ae29ae342
commit
47a7373ef2
4 changed files with 105 additions and 10 deletions
|
|
@ -18,19 +18,21 @@ import (
|
|||
// 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
|
||||
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{},
|
||||
now: func() time.Time { return time.Now().UTC() },
|
||||
indexByHash: map[string]int64{},
|
||||
loggedAtByIndex: map[int64]time.Time{},
|
||||
envelopesByIndex: map[int64]Envelope{},
|
||||
now: func() time.Time { return time.Now().UTC() },
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -73,6 +75,7 @@ func (m *Memory) Submit(_ context.Context, env Envelope, contentHash string) (Re
|
|||
m.leaves = append(m.leaves, leafHash(canonical))
|
||||
m.indexByHash[contentHash] = idx
|
||||
m.loggedAtByIndex[idx] = m.now()
|
||||
m.envelopesByIndex[idx] = env
|
||||
fresh = true
|
||||
}
|
||||
|
||||
|
|
@ -99,6 +102,40 @@ func (m *Memory) Submit(_ context.Context, env Envelope, contentHash string) (Re
|
|||
}, 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.
|
||||
|
|
|
|||
|
|
@ -58,6 +58,13 @@ type Trillian struct {
|
|||
dialErr error
|
||||
}
|
||||
|
||||
// Get is not yet wired against Trillian; the production read path is a
|
||||
// future subtask. The handler short-circuits to 503 when the client is
|
||||
// absent, so this stub only has to satisfy the interface.
|
||||
func (t *Trillian) Get(_ context.Context, _ string) (Entry, error) {
|
||||
return Entry{}, ErrTrillianUnavailable
|
||||
}
|
||||
|
||||
// Submit queues the envelope and returns the inclusion proof against the
|
||||
// current signed tree head. See the Backend interface for the fresh
|
||||
// semantics.
|
||||
|
|
|
|||
|
|
@ -47,6 +47,27 @@ type InclusionProof struct {
|
|||
// concurrent use and MUST be idempotent on contentHash: a repeated submit
|
||||
// with the same contentHash returns the originally assigned log_index and
|
||||
// a valid inclusion proof against the current tree size, and sets fresh=false.
|
||||
//
|
||||
// Get returns the stored envelope + a fresh inclusion proof against the
|
||||
// current tree size. Verifiers rely on this to independently recompute the
|
||||
// Merkle tree root (ADR-0005 §Read side).
|
||||
type Backend interface {
|
||||
Submit(ctx context.Context, env Envelope, contentHash string) (receipt Receipt, fresh bool, err error)
|
||||
Get(ctx context.Context, contentHash string) (Entry, error)
|
||||
}
|
||||
|
||||
// Entry is the shape returned by GET /api/v1/log/entries/{content_hash}.
|
||||
type Entry struct {
|
||||
ContentHash string `json:"content_hash"`
|
||||
Envelope Envelope `json:"envelope"`
|
||||
LogIndex int64 `json:"log_index"`
|
||||
LoggedAt time.Time `json:"logged_at"`
|
||||
InclusionProof InclusionProof `json:"inclusion_proof"`
|
||||
}
|
||||
|
||||
// ErrEntryNotFound is returned by Get when contentHash is not in the log.
|
||||
var ErrEntryNotFound = errorString("transparency-log: entry not found")
|
||||
|
||||
type errorString string
|
||||
|
||||
func (e errorString) Error() string { return string(e) }
|
||||
|
|
|
|||
|
|
@ -22,14 +22,44 @@ type Server struct {
|
|||
LogError func(string, ...any)
|
||||
}
|
||||
|
||||
// Handler returns an http.Handler that serves the ADR-0005 routes. Only
|
||||
// POST /api/v1/log/entries is implemented; everything else is 404.
|
||||
// Handler returns an http.Handler that serves the ADR-0005 routes plus a
|
||||
// liveness check at GET /healthz. Everything else is 404.
|
||||
func (s *Server) Handler() http.Handler {
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle("POST /api/v1/log/entries", http.HandlerFunc(s.handleSubmit))
|
||||
mux.Handle("GET /api/v1/log/entries/{hash}", http.HandlerFunc(s.handleGetEntry))
|
||||
mux.Handle("GET /healthz", http.HandlerFunc(s.handleHealth))
|
||||
return mux
|
||||
}
|
||||
|
||||
func (s *Server) handleHealth(w http.ResponseWriter, _ *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte(`{"status":"ok"}`))
|
||||
}
|
||||
|
||||
func (s *Server) handleGetEntry(w http.ResponseWriter, r *http.Request) {
|
||||
hash := r.PathValue("hash")
|
||||
if hash == "" {
|
||||
writeError(w, http.StatusBadRequest, "content_hash is required")
|
||||
return
|
||||
}
|
||||
entry, err := s.Backend.Get(r.Context(), hash)
|
||||
switch {
|
||||
case errors.Is(err, backend.ErrEntryNotFound):
|
||||
writeError(w, http.StatusNotFound, "entry not found")
|
||||
return
|
||||
case errors.Is(err, backend.ErrTrillianUnavailable):
|
||||
writeError(w, http.StatusServiceUnavailable, err.Error())
|
||||
return
|
||||
case err != nil:
|
||||
s.logErr("transparency-log get: %v", err)
|
||||
writeError(w, http.StatusInternalServerError, "internal error")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, entry)
|
||||
}
|
||||
|
||||
type submitRequest struct {
|
||||
Envelope backend.Envelope `json:"envelope"`
|
||||
ContentHash string `json:"content_hash"`
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue