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
|
// originally assigned log_index with a fresh inclusion proof against the
|
||||||
// current tree size.
|
// current tree size.
|
||||||
type Memory struct {
|
type Memory struct {
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
leaves [][]byte // ordered list of leaf hashes
|
leaves [][]byte // ordered list of leaf hashes
|
||||||
indexByHash map[string]int64 // contentHash -> log_index (zero-based)
|
indexByHash map[string]int64 // contentHash -> log_index (zero-based)
|
||||||
loggedAtByIndex map[int64]time.Time // original append time per index
|
loggedAtByIndex map[int64]time.Time // original append time per index
|
||||||
now func() time.Time
|
envelopesByIndex map[int64]Envelope // original envelope per index (for Get)
|
||||||
|
now func() time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewMemory returns an empty in-memory backend.
|
// NewMemory returns an empty in-memory backend.
|
||||||
func NewMemory() *Memory {
|
func NewMemory() *Memory {
|
||||||
return &Memory{
|
return &Memory{
|
||||||
indexByHash: map[string]int64{},
|
indexByHash: map[string]int64{},
|
||||||
loggedAtByIndex: map[int64]time.Time{},
|
loggedAtByIndex: map[int64]time.Time{},
|
||||||
now: func() time.Time { return time.Now().UTC() },
|
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.leaves = append(m.leaves, leafHash(canonical))
|
||||||
m.indexByHash[contentHash] = idx
|
m.indexByHash[contentHash] = idx
|
||||||
m.loggedAtByIndex[idx] = m.now()
|
m.loggedAtByIndex[idx] = m.now()
|
||||||
|
m.envelopesByIndex[idx] = env
|
||||||
fresh = true
|
fresh = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -99,6 +102,40 @@ func (m *Memory) Submit(_ context.Context, env Envelope, contentHash string) (Re
|
||||||
}, fresh, nil
|
}, 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
|
// signedTreeHeadRef is a stand-in for the real STH reference. In the
|
||||||
// in-memory backend we deterministically derive it from (tree_size, root),
|
// in-memory backend we deterministically derive it from (tree_size, root),
|
||||||
// so callers get a stable per-head identifier without needing a signer.
|
// so callers get a stable per-head identifier without needing a signer.
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,13 @@ type Trillian struct {
|
||||||
dialErr error
|
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
|
// Submit queues the envelope and returns the inclusion proof against the
|
||||||
// current signed tree head. See the Backend interface for the fresh
|
// current signed tree head. See the Backend interface for the fresh
|
||||||
// semantics.
|
// semantics.
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,27 @@ type InclusionProof struct {
|
||||||
// concurrent use and MUST be idempotent on contentHash: a repeated submit
|
// concurrent use and MUST be idempotent on contentHash: a repeated submit
|
||||||
// with the same contentHash returns the originally assigned log_index and
|
// with the same contentHash returns the originally assigned log_index and
|
||||||
// a valid inclusion proof against the current tree size, and sets fresh=false.
|
// 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 {
|
type Backend interface {
|
||||||
Submit(ctx context.Context, env Envelope, contentHash string) (receipt Receipt, fresh bool, err error)
|
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)
|
LogError func(string, ...any)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handler returns an http.Handler that serves the ADR-0005 routes. Only
|
// Handler returns an http.Handler that serves the ADR-0005 routes plus a
|
||||||
// POST /api/v1/log/entries is implemented; everything else is 404.
|
// liveness check at GET /healthz. Everything else is 404.
|
||||||
func (s *Server) Handler() http.Handler {
|
func (s *Server) Handler() http.Handler {
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
mux.Handle("POST /api/v1/log/entries", http.HandlerFunc(s.handleSubmit))
|
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
|
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 {
|
type submitRequest struct {
|
||||||
Envelope backend.Envelope `json:"envelope"`
|
Envelope backend.Envelope `json:"envelope"`
|
||||||
ContentHash string `json:"content_hash"`
|
ContentHash string `json:"content_hash"`
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue