slice 7: PBS recovery-code escrow creation (agent, Phase B) (v0.9.0 wip)

internal/escrow: zero-knowledge escrow creation. R = 10 EFF-wordlist words
(crypto/rand, ~129 bits); wrap K under R via PBS-native key change-passphrase
driven over a stdlib pty (x/sys/unix; output discarded so R can't leak, F-A2);
self-verify the blob recovers K (fingerprint match) before shipping. Opt-in (b)
R-wrapped offline copy + (a) raw paperkey. Live K is byte-unchanged (operates on
a copy). --selftest=escrow-create (-storage/-paperkey/-offline/-upload). Posture
config field (zero_knowledge default). PBSEncKeyPath helper. Grounded by the
escrow spike findings.

Tests: R entropy>=128/format/uniqueness; integration round-trip (wrap->unwrap
fingerprint match, wrong-R fails, K byte-unchanged) guarded to linux+pbc.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-10 07:37:03 +02:00
parent 6e036e4c75
commit 47dd0bd244
8 changed files with 8449 additions and 6 deletions
+24 -4
View File
@@ -28,9 +28,20 @@ type Config struct {
Hub HubConfig `json:"hub"`
Storage StorageConfig `json:"storage"`
Backup BackupConfig `json:"backup"`
Escrow EscrowConfig `json:"escrow"`
LogLevel string `json:"log_level"` // debug|info|warn|error (default info)
}
// EscrowConfig configures PBS recovery-code escrow creation (slice 7, doc 03 §8a). Enrollment-time
// only (not the steady-state daemon). The default posture is zero-knowledge (Felhom holds the
// opaque blob, the customer holds the recovery code).
type EscrowConfig struct {
// Posture is the key-custody posture; "" → zero_knowledge (the only one implemented this slice).
Posture string `json:"posture"`
// PBSStorageID is the pbs storage whose client encryption key is escrowed (e.g. "felhom-pbs").
PBSStorageID string `json:"pbs_storage_id"`
}
// BackupConfig tunes the slice-6 backup + self-restore-test layer. The restore-test runs on
// an agent-internal cadence (no hub policy needed — it's self-validation); the backup
// schedule/retention/target-selection policy is hub-manifest-owned and unfed until slice 10.
@@ -91,11 +102,20 @@ func (b BackupConfig) PBSVerifyCadence() time.Duration {
// PBSSecretPath returns the path to a pbs storage's token-secret file.
func (b BackupConfig) PBSSecretPath(storageID string) string {
dir := b.PBSSecretDir
if dir == "" {
dir = "/etc/pve/priv/storage"
return b.pbsSecretDir() + "/" + storageID + ".pw"
}
// PBSEncKeyPath returns the path to a pbs storage's CLIENT ENCRYPTION KEY (K) file — the key the
// escrow wraps (slice 7). PVE stores it alongside the token secret as <id>.enc, 0600 root.
func (b BackupConfig) PBSEncKeyPath(storageID string) string {
return b.pbsSecretDir() + "/" + storageID + ".enc"
}
func (b BackupConfig) pbsSecretDir() string {
if b.PBSSecretDir == "" {
return "/etc/pve/priv/storage"
}
return dir + "/" + storageID + ".pw"
return b.PBSSecretDir
}
// ScratchBand returns the effective [min,max] scratch VMID band (defaults applied).