hub: opaque PBS recovery-code escrow storage (v0.8.0) + doc 03 §8a posture model
Slice-7 close-out (hub half). PUT /api/v1/hosts/{host_id}/escrow (per-host key)
stores the agent's OPAQUE R-wrapped blob verbatim against the host; the hub never
decrypts it (no recovery code, no decrypt path). host_escrow table + Save/GetHostEscrow.
Tests: verbatim store, rotation last-write-wins, 401/403/400 auth+body, wire contract.
doc 03 §8a rewritten into the key-custody posture model: separation principle,
topology matrix, default + anti-lockout ladder, SSH-vs-key, breach/legal, integrity
caveat. Corrected: hub opaque storage is slice 7 (this task); serving is slice 10.
Slice table + §13 updated.
No secrets committed (R/K never appear; spike findings + docs use placeholders).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -269,6 +269,19 @@ func (s *Store) migrate() error {
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_host_reports_host ON host_reports(host_id, received_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_host_reports_customer ON host_reports(customer_id, received_at DESC);
|
||||
|
||||
-- host_escrow (slice 7, doc 03 §8a): the OPAQUE R-wrapped PBS-key escrow blob. The hub
|
||||
-- stores the ciphertext bytes against the host and NEVER decrypts them (it has no recovery
|
||||
-- code). One row per host; a re-upload (rotation) is last-write-wins. Restore-mode serving
|
||||
-- (handing the blob back to a re-enrolling box) is slice 10.
|
||||
CREATE TABLE IF NOT EXISTS host_escrow (
|
||||
host_id TEXT PRIMARY KEY,
|
||||
blob BLOB NOT NULL,
|
||||
key_fingerprint TEXT NOT NULL DEFAULT '',
|
||||
posture TEXT NOT NULL DEFAULT '',
|
||||
created_at DATETIME NOT NULL,
|
||||
updated_at DATETIME NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
`)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -1381,6 +1394,51 @@ func (s *Store) UpsertHost(h *Host) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// HostEscrow is the opaque R-wrapped escrow blob stored for a host (doc 03 §8a). Blob is
|
||||
// ciphertext the hub cannot open.
|
||||
type HostEscrow struct {
|
||||
HostID string
|
||||
Blob []byte
|
||||
KeyFingerprint string
|
||||
Posture string
|
||||
CreatedAt string
|
||||
UpdatedAt string
|
||||
}
|
||||
|
||||
// SaveHostEscrow stores (last-write-wins) the OPAQUE escrow blob for a host. The hub keeps the
|
||||
// bytes and NEVER decrypts them — there is no decrypt path. createdAt is the agent's timestamp.
|
||||
func (s *Store) SaveHostEscrow(hostID string, blob []byte, keyFingerprint, posture, createdAt string) error {
|
||||
_, err := s.db.Exec(`
|
||||
INSERT INTO host_escrow (host_id, blob, key_fingerprint, posture, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, datetime('now'))
|
||||
ON CONFLICT(host_id) DO UPDATE SET
|
||||
blob = excluded.blob,
|
||||
key_fingerprint = excluded.key_fingerprint,
|
||||
posture = excluded.posture,
|
||||
created_at = excluded.created_at,
|
||||
updated_at = datetime('now')`,
|
||||
hostID, blob, keyFingerprint, posture, createdAt,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetHostEscrow returns the stored opaque escrow for a host (nil if none). Used by tests and
|
||||
// (future, slice 10) restore-mode serving. The hub returns bytes verbatim; it never decrypts.
|
||||
func (s *Store) GetHostEscrow(hostID string) (*HostEscrow, error) {
|
||||
var e HostEscrow
|
||||
err := s.db.QueryRow(`
|
||||
SELECT host_id, blob, key_fingerprint, posture, created_at, updated_at
|
||||
FROM host_escrow WHERE host_id = ?`, hostID).
|
||||
Scan(&e.HostID, &e.Blob, &e.KeyFingerprint, &e.Posture, &e.CreatedAt, &e.UpdatedAt)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &e, nil
|
||||
}
|
||||
|
||||
// SaveHostReport inserts a host_reports row and bumps the host's reality columns
|
||||
// (agent_version/last_report_at/updated_at) — never the inert intent columns.
|
||||
func (s *Store) SaveHostReport(hostID, customerID string, reportJSON []byte, d HostReportDenorm) error {
|
||||
|
||||
Reference in New Issue
Block a user