bd4bced771
wgtunnel.InstallRecoveredKey: write an escrow-recovered WG private key (create-
only, refuse-overwrite) so the tunnel re-establishes with the same identity/pubkey
(same /32), no keygen. Wired into identity-consume -install-wg-key (opt-in;
pre-S3 blob → logged fresh-keygen fallback). Value never logged.
internal/dr (new): consume the host_loss restore_directive (was logged-ignored)
into an inspectable RestorePlan via the AddConsumer raw seam — per guest
{vmid,archive,target,sizing} + per drive {durable_id→mount} + offsite PBS coord.
DERIVE-AND-SURFACE only; the Consumer has no restore/destroy dependency (execute-
nothing is structural). guest_loss/absent → no plan.
Tests + red-proofs (WG create-only overwrite; plan mode-gate). No secrets on
argv/stdout/logs. The destructive in-place restore is a separate operator-present
STOP-gated drill.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
196 lines
6.6 KiB
Go
196 lines
6.6 KiB
Go
package wgtunnel
|
|
|
|
// Group A — keygen (S3 Part 1). The fixed vector is the red-proof-(c) anchor: remove the clamp
|
|
// and the derived public key diverges from the real `wg pubkey`.
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// TestInstallRecoveredKey_SameIdentityCreateOnly (S5): a recovered WG key installs into a fresh
|
|
// state dir, EnsureKey LOADS it (no keygen) yielding the SAME pubkey (→ same /32); a second install
|
|
// REFUSES (create-only, never overwrite); an invalid key errors and writes nothing.
|
|
func TestInstallRecoveredKey_SameIdentityCreateOnly(t *testing.T) {
|
|
src := t.TempDir()
|
|
srcPub, created, err := EnsureKey(src)
|
|
if err != nil || !created {
|
|
t.Fatalf("seed EnsureKey: created=%v err=%v", created, err)
|
|
}
|
|
recovered, err := readPrivateKeyB64(src) // the base64 the escrow bundle carries
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
dst := t.TempDir()
|
|
if err := InstallRecoveredKey(dst, recovered); err != nil {
|
|
t.Fatalf("InstallRecoveredKey: %v", err)
|
|
}
|
|
pub, created, err := EnsureKey(dst) // must LOAD, not generate
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if created {
|
|
t.Error("EnsureKey generated a fresh key instead of loading the installed one (no-keygen negative)")
|
|
}
|
|
if pub != srcPub {
|
|
t.Errorf("recovered pubkey = %q, want same as source %q (same /32)", pub, srcPub)
|
|
}
|
|
|
|
// Create-only: a second install REFUSES (a present key may be a live identity).
|
|
if err := InstallRecoveredKey(dst, recovered); err == nil || !strings.Contains(err.Error(), "refusing to overwrite") {
|
|
t.Errorf("second install must refuse (create-only), got %v", err)
|
|
}
|
|
|
|
// Invalid recovered key → error, nothing written.
|
|
bad := t.TempDir()
|
|
if err := InstallRecoveredKey(bad, "not-base64!!"); err == nil {
|
|
t.Error("invalid recovered key accepted")
|
|
}
|
|
if _, serr := os.Stat(KeyFilePath(bad)); !os.IsNotExist(serr) {
|
|
t.Error("a key file was written despite an invalid recovered key")
|
|
}
|
|
}
|
|
|
|
// Fixed test vector. PROVENANCE: public key generated ONCE with the real `wg pubkey` (
|
|
// wireguard-tools on felhom-hetzner, 2026-07-04) from the spec's published test private key —
|
|
// this private key is a PUBLISHED test constant, not a secret.
|
|
const (
|
|
vectorPrivB64 = "YAn1SdYWjNSVZBM4CxJGQ738mUhSMuZ1yA6ZNZ1XwFg="
|
|
vectorPubB64 = "3F+nlkwVVl5OoVY+/vfWH6PDf0H7LCqcHExfJ6ypZj4="
|
|
)
|
|
|
|
func TestDerivePublic_MatchesWgPubkeyVector(t *testing.T) {
|
|
priv, err := base64.StdEncoding.DecodeString(vectorPrivB64)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// The vector private key is already clamped the way `wg genkey` emits; clamp anyway — it
|
|
// must be a no-op for an already-clamped key and is required for raw random bytes.
|
|
clampPrivateKey(priv)
|
|
pub, err := derivePublic(priv)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if pub != vectorPubB64 {
|
|
t.Fatalf("derived pubkey = %s, want %s (the real `wg pubkey` output)", pub, vectorPubB64)
|
|
}
|
|
}
|
|
|
|
// The UNCLAMPED vector is the red-proof-(c) anchor proper: all-0xFF private bytes are invalid
|
|
// until clamped; `wg pubkey` clamps internally, so its output equals our clamp+derive. Remove
|
|
// the clamp and THIS test fails (the primary vector above is already-clamped `wg genkey` output,
|
|
// which cannot detect a missing clamp).
|
|
// PROVENANCE: public generated ONCE with the real `wg pubkey` on felhom-hetzner, 2026-07-04.
|
|
const (
|
|
unclampedPrivB64 = "//////////////////////////////////////////8="
|
|
unclampedPubB64 = "hHwNLDdSNPNl5mCVUYejc1oPdhPRYJ06ak2MU66qWiI="
|
|
)
|
|
|
|
func TestDerivePublic_ClampRequiredForRawBytes(t *testing.T) {
|
|
priv, err := base64.StdEncoding.DecodeString(unclampedPrivB64)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
clampPrivateKey(priv)
|
|
pub, err := derivePublic(priv)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if pub != unclampedPubB64 {
|
|
t.Fatalf("clamped derivation = %s, want %s (real `wg pubkey` on the raw bytes)", pub, unclampedPubB64)
|
|
}
|
|
}
|
|
|
|
func TestEnsureKey_CreateOnceThenReload(t *testing.T) {
|
|
dir := t.TempDir()
|
|
pub1, created, err := EnsureKey(dir)
|
|
if err != nil || !created {
|
|
t.Fatalf("first EnsureKey: pub=%q created=%v err=%v", pub1, created, err)
|
|
}
|
|
if len(pub1) != 44 {
|
|
t.Fatalf("pubkey %q is not 44 base64 chars", pub1)
|
|
}
|
|
fi, err := os.Stat(KeyFilePath(dir))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// POSIX modes are not representable on Windows (Go maps everything to 666/777) — assert on
|
|
// Linux only; the live bring-up (§13) verifies the real 0600/0700 on the box.
|
|
if runtime.GOOS != "windows" {
|
|
if fi.Mode().Perm() != 0o600 {
|
|
t.Errorf("key file mode = %o, want 0600", fi.Mode().Perm())
|
|
}
|
|
if di, _ := os.Stat(filepath.Join(dir, "wg")); di.Mode().Perm() != 0o700 {
|
|
t.Errorf("wg dir mode = %o, want 0700", di.Mode().Perm())
|
|
}
|
|
}
|
|
// Reload: same key, not recreated.
|
|
pub2, created, err := EnsureKey(dir)
|
|
if err != nil || created {
|
|
t.Fatalf("second EnsureKey: created=%v err=%v", created, err)
|
|
}
|
|
if pub2 != pub1 {
|
|
t.Errorf("reload derived a different pubkey: %s vs %s", pub2, pub1)
|
|
}
|
|
}
|
|
|
|
// The stored key file must hold CANONICAL CLAMPED bytes: x/crypto X25519 clamps internally (so
|
|
// derivation is clamp-invariant — a missing clamp is invisible to the pubkey tests above), but
|
|
// an unclamped stored key would still be a non-canonical secret whose bits differ from what
|
|
// every WireGuard tool considers the effective key. This is red-proof (c)'s real anchor.
|
|
func TestEnsureKey_StoredKeyIsClamped(t *testing.T) {
|
|
for i := 0; i < 8; i++ { // several fresh keys — random bytes are unclamped ~7/8 of the time
|
|
dir := t.TempDir()
|
|
if _, _, err := EnsureKey(dir); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
raw, err := os.ReadFile(KeyFilePath(dir))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
priv, err := decodeKey(raw)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if priv[0]&7 != 0 || priv[31]&128 != 0 || priv[31]&64 != 64 {
|
|
t.Fatalf("stored key is not clamped: byte0=%08b byte31=%08b", priv[0], priv[31])
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestEnsureKey_CorruptFileRefused(t *testing.T) {
|
|
dir := t.TempDir()
|
|
os.MkdirAll(filepath.Join(dir, "wg"), 0o700)
|
|
if err := os.WriteFile(KeyFilePath(dir), []byte("not-a-key\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_, _, err := EnsureKey(dir)
|
|
if err == nil {
|
|
t.Fatal("corrupt key file accepted (or overwritten) — must be refused")
|
|
}
|
|
// The corrupt file must still be there — never overwritten.
|
|
raw, _ := os.ReadFile(KeyFilePath(dir))
|
|
if string(raw) != "not-a-key\n" {
|
|
t.Errorf("corrupt key file was modified: %q", raw)
|
|
}
|
|
}
|
|
|
|
func TestReadPrivateKeyB64_RoundTrip(t *testing.T) {
|
|
dir := t.TempDir()
|
|
if _, _, err := EnsureKey(dir); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
b64, err := readPrivateKeyB64(dir)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if raw, _ := base64.StdEncoding.DecodeString(b64); len(raw) != 32 {
|
|
t.Errorf("private key b64 decodes to %d bytes", len(raw))
|
|
}
|
|
}
|