Documentation
¶
Overview ¶
Package idkey is the client side of the tunneled-tier AGENT-HELD identity key. It mints an EC P-256 keypair LOCALLY (mirroring wgtun.GenerateKeypair's WireGuard keypair, one level up the stack), persists it 0600 under ~/.config/whisper/identity/ so it survives across reconnects, and builds the self-signed DANE-EE leaf the routed tunnel serves on :443. The private key NEVER leaves this process: only the raw base64 DER SubjectPublicKeyInfo is ever handed to op:connect (identity_public_key) - the server pins SHA-256 of THAT exact submission, verbatim, and never derives a leaf for a held /128 (the server pins SHA-256 of exactly this submitted SPKI).
Index ¶
Constants ¶
const (
// EnrolmentWindowSeconds is the window quantum, matching the server's.
EnrolmentWindowSeconds = 300
)
The node-enrolment proof: the client half of a challenge the server derives independently. Both sides build the SAME bytes from facts they already share - this /128, the WireGuard public key being enrolled, and a coarse time window - so a routed connect proves possession of the persisted identity key in ONE call, with no challenge round trip and, crucially, without resending identity_public_key.
That last point is not a detail. The server's zone-write classifier treats identity_public_key as re-authoring (it is: a rotated pin rewrites the zone), so a connect carrying it is forwarded to the primary. Resend the key on every reconnect and every Tier-1 tunnel in the fleet terminates on one box. The signature is a different argument for exactly that reason.
Variables ¶
This section is empty.
Functions ¶
func EnrolmentChallenge ¶ added in v0.211.0
EnrolmentChallenge builds the exact bytes both sides sign. The address rides as its 16 RAW BYTES, never as text: Go compresses IPv6 zero runs and Java does not, and a canonicalization disagreement would be indistinguishable from a wrong key. wgPublicKey is sent exactly as the caller will send it (empty when the server mints the tunnel key - a signature cannot cover a key the client has not seen).
func EnrolmentWindow ¶ added in v0.211.0
EnrolmentWindow is the window number for an instant - the same floor division the server does, so a correct clock on both ends needs no search at all.
func PathFor ¶
PathFor maps a persistence handle (an agent id or a /128 - whatever is already known before the FIRST connect that mints this key) to its on-disk PEM path. Colons are not portable in filenames (Windows), so they are flattened; a blank handle collapses to "default" (the connect-first case, before any agent id/address is known).
func SetIdentityDirForTest ¶
func SetIdentityDirForTest(dir string) func()
SetIdentityDirForTest overrides the identity-key directory for the duration of a test - a seam OTHER packages' tests use to isolate persistence without touching the real $HOME/.config. Returns a restore func. Test-only; never called from production code.
Types ¶
type Keypair ¶
type Keypair struct {
Private *ecdsa.PrivateKey
// contains filtered or unexported fields
}
Keypair is a freshly-generated (or loaded) EC P-256 identity keypair. The private key NEVER leaves this process; MarshalSPKIBase64 is the ONLY thing ever sent to the control plane.
func GenerateKeypair ¶
GenerateKeypair mints a fresh EC P-256 identity keypair from the host CSPRNG (crypto/rand). The private key stays in-memory only; the caller persists it via Save if it wants reuse across runs.
func Load ¶ added in v0.210.0
Load returns the ALREADY-persisted identity keypair for handle, or an error when none exists. Unlike LoadOrGenerate it NEVER mints: callers that must present the key whose DANE-EE pin is already published (the summary uplink's mTLS client cert) use this, because a freshly-minted key would present an unpinned - and thus unverifiable - leaf.
func LoadOrGenerate ¶
LoadOrGenerate loads the persisted identity key for handle, or mints + persists a fresh one if none exists yet - so the SAME key (and thus the SAME published DANE-EE pin) is reused across reconnects rather than re-pinning on every `whisper connect`. Best-effort persistence: a write failure still returns the freshly-minted in-memory key (the connect proceeds; it just re-pins next time too).
func (*Keypair) MarshalSPKIBase64 ¶
MarshalSPKIBase64 returns the base64 (standard) encoding of the DER SubjectPublicKeyInfo - the EXACT, verbatim value sent as op:connect's identity_public_key arg. Marshaled once at generation (or load) time; this method never re-encodes, so the submitted bytes are always the same bytes the self-signed leaf's own public key encodes to.
func (*Keypair) SelfSignedLeaf ¶
func (k *Keypair) SelfSignedLeaf(canonicalFqdn, friendlyFqdn string, addr netip.Addr) (tls.Certificate, error)
SelfSignedLeaf mints a self-signed EC P-256 leaf certificate over kp's key, with SANs dNSName=canonicalFqdn, dNSName=friendlyFqdn (when non-blank and distinct), and iPAddress=addr - the SAME SAN set the server's per-agent CA leaf carries. DANE-EE (usage 3, selector 1, matching 1) pins the SPKI directly, so no chain/root is needed; this leaf never needs to be signed by anyone else. The SPKI this leaf carries is BYTE-IDENTICAL to what MarshalSPKIBase64 already submitted (the SANs live in the Extensions, never in the SubjectPublicKeyInfo), so the pin submitted before connect still matches whatever this method mints after connect returns the fqdn/address.
func (*Keypair) SignEnrolment ¶ added in v0.211.0
SignEnrolment returns the base64 (standard) ECDSA-P256/SHA-256 signature, ASN.1 DER encoded, over the derived challenge - the value of op:connect's identity_signature arg. The private key never leaves this process; only this signature does.