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:
@@ -0,0 +1,56 @@
|
||||
//go:build linux
|
||||
|
||||
package escrow
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// runWithPassphrase runs a TTY-requiring command (PBS `key change-passphrase`, F-A1 in the spike
|
||||
// findings) on a pty, feeding `passphrase` `reps` times (once per prompt) and DISCARDING all pty
|
||||
// output so the echoed passphrase can never leak (F-A2). Linux-only — the agent runs on Proxmox
|
||||
// hosts; a non-linux stub returns an error.
|
||||
func runWithPassphrase(ctx context.Context, passphrase string, reps int, name string, args ...string) error {
|
||||
master, err := os.OpenFile("/dev/ptmx", os.O_RDWR, 0)
|
||||
if err != nil {
|
||||
return fmt.Errorf("escrow: open ptmx: %w", err)
|
||||
}
|
||||
defer master.Close()
|
||||
if err := unix.IoctlSetPointerInt(int(master.Fd()), unix.TIOCSPTLCK, 0); err != nil { // unlock
|
||||
return fmt.Errorf("escrow: unlock pty: %w", err)
|
||||
}
|
||||
ptn, err := unix.IoctlGetInt(int(master.Fd()), unix.TIOCGPTN)
|
||||
if err != nil {
|
||||
return fmt.Errorf("escrow: pty number: %w", err)
|
||||
}
|
||||
slave, err := os.OpenFile(fmt.Sprintf("/dev/pts/%d", ptn), os.O_RDWR|syscall.O_NOCTTY, 0)
|
||||
if err != nil {
|
||||
return fmt.Errorf("escrow: open pts: %w", err)
|
||||
}
|
||||
defer slave.Close()
|
||||
|
||||
cmd := exec.CommandContext(ctx, name, args...)
|
||||
cmd.Stdin, cmd.Stdout, cmd.Stderr = slave, slave, slave
|
||||
// New session + the slave (fd 0) becomes the controlling terminal, so the child's tty prompts
|
||||
// read from / write to the pty rather than failing "no tty".
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true, Setctty: true}
|
||||
if err := cmd.Start(); err != nil {
|
||||
return fmt.Errorf("escrow: start %s: %w", name, err)
|
||||
}
|
||||
|
||||
// Feed the passphrase once (the tty line discipline buffers both lines for the sequential
|
||||
// New/Verify prompts), then DISCARD everything the pty emits — the passphrase is echoed back
|
||||
// on the master fd and must never reach a log.
|
||||
go func() { _, _ = master.WriteString(strings.Repeat(passphrase+"\n", reps)) }()
|
||||
go func() { _, _ = io.Copy(io.Discard, master) }()
|
||||
|
||||
return cmd.Wait()
|
||||
}
|
||||
Reference in New Issue
Block a user