47dd0bd244
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>
57 lines
2.0 KiB
Go
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()
|
|
}
|