During short-code pairing the agent submitted its Ed25519 public key at
enroll time, but the panel never checked that the poller actually held the
matching private key -- identity rested entirely on the bearer
enrollment_secret. Anyone who captured that secret (e.g. from a log or proxy)
could claim the device's credentials.
The agent now signs the enrollment_id with its pairing keypair and sends the
base64 signature as X-Enrollment-Signature on each poll. pairing_service.poll
verifies it against the enrolled hex pubkey (cryptography Ed25519) and rejects
a present-but-invalid or wrong-key signature with InvalidEnrollmentError,
before touching any state. So a stolen enrollment_secret alone no longer
suffices -- the attacker also needs the private key.
Verification is best-effort for backward compatibility: a poll with no
signature still works (older agents), and becomes mandatory once signing
agents have rolled out (tracked in SECURITY.md).
Agent side: KeyPair.Sign(); pairing.Client gains an optional keypair via
SetKeyPair(), wired in at both call sites (cmd/agent/pair.go and
internal/pairdriver/driver.go); Poll attaches the signature header.
Verified: backend 89 passed / 12 skipped (4 new PoP tests: valid sig accepted,
invalid rejected, wrong-key rejected, no-sig backward-compatible); go build
./... + go vet + go test ./... all green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>