Files
admin 47dd0bd244 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>
2026-06-10 07:37:03 +02:00

57 lines
2.0 KiB
Go

//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()
}