Skip to main content

ts_keys/
keystate.rs

1use core::fmt::{Debug, Display, Formatter};
2
3use crate::{
4    DiscoKeyPair, MachineKeyPair, MachinePrivateKey, NetworkLockKeyPair, NetworkLockPrivateKey,
5    NodeKeyPair, NodePrivateKey, NodePublicKey,
6};
7
8/// The portion of the key state that should be retained between runs of the same device.
9///
10/// Disco keys are ephemeral and should be generated anew each time a device runs, so are
11/// excluded from this state.
12///
13/// # At-rest protection is the embedder's responsibility
14///
15/// The secret-bearing fields here are zeroized in memory on drop (the dedicated key types and the
16/// [`Zeroizing`](zeroize::Zeroizing)-wrapped ACME account key), but that is an in-process hygiene
17/// measure only. Protecting this state **at rest** — restrictive file permissions (e.g. `0o600`),
18/// full-disk or filesystem encryption, secure-enclave/keyring storage — is entirely the
19/// responsibility of the embedding application that serializes and writes it to durable storage.
20/// This crate neither reads nor writes files and makes no at-rest guarantee (see `SECURITY.md`).
21#[derive(Clone, Debug)]
22#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
23pub struct PersistState {
24    /// The [`MachinePrivateKey`] for the hardware this Tailnet peer runs on.
25    pub machine_key: MachinePrivateKey,
26
27    /// The [`NetworkLockPrivateKey`] for this Tailnet peer, for use with Tailnet Lock.
28    pub network_lock_key: NetworkLockPrivateKey,
29
30    /// The [`NodePrivateKey`] for this Tailnet peer.
31    pub node_key: NodePrivateKey,
32
33    /// The node's PREVIOUS node public key, recorded during a node-key rotation so the next
34    /// registration sends it as `RegisterRequest.OldNodeKey` for key continuity (Go's `regen` flow).
35    /// `None` outside a rotation (the default). Reactive / embedder-driven — matching Go, this fork
36    /// does NOT auto-rotate the node key before expiry (Go deliberately doesn't either; key expiry is
37    /// a human-re-auth control). See [`PersistState::rotate_node_key`].
38    #[cfg_attr(feature = "serde", serde(default))]
39    pub old_node_key: Option<NodePublicKey>,
40
41    /// The persisted ACME account key (PKCS#8 DER of an ECDSA P-256 key), or `None` if no ACME
42    /// account has been provisioned for this node. The `acme` cert-issuance path loads this to keep
43    /// the same Let's Encrypt account identity across renewals; absent, the runtime generates an
44    /// ephemeral per-call key (a new ACME account each issuance). `#[serde(default)]` so key files
45    /// written before this field load as `None` (mirrors [`old_node_key`](PersistState::old_node_key)).
46    ///
47    /// Wrapped in [`Zeroizing`](zeroize::Zeroizing) so the DER private-key bytes are wiped from
48    /// memory on drop. `Zeroizing<Vec<u8>>` serializes transparently via its inner `Vec`, so the
49    /// persisted JSON shape is identical to a bare `Vec<u8>` (a byte array).
50    #[cfg_attr(feature = "serde", serde(default))]
51    pub acme_account_key: Option<zeroize::Zeroizing<alloc::vec::Vec<u8>>>,
52}
53
54impl PersistState {
55    /// Rotate the node key for re-registration, mirroring Go's `regen` flow: record the current
56    /// node public key as [`old_node_key`](PersistState::old_node_key) and replace the node key with
57    /// a freshly-generated one. The next registration that uses this state will send the prior key
58    /// as `RegisterRequest.OldNodeKey`, so control links the new node key to the node's existing
59    /// identity instead of treating it as a brand-new node.
60    ///
61    /// This is the embedder-driven rotation primitive (re-create the device with the returned state).
62    /// It is reactive, NOT a pre-expiry auto-rotator: Go has no such timer, because node-key expiry
63    /// is a deliberate periodic human/IdP re-attestation control. Re-registration still requires a
64    /// valid auth credential, exactly as a fresh registration does.
65    ///
66    // TODO(TKA): on a tailnet-lock-enabled tailnet, a node-key rotation must also re-sign the node
67    // key with the network-lock key and send the new `RegisterRequest.NodeKeySignature`. This
68    // primitive covers the non-TKA path; TKA re-sign is a separate follow-up.
69    pub fn rotate_node_key(&mut self) {
70        self.old_node_key = Some(self.node_key.public_key());
71        self.node_key = NodePrivateKey::random();
72    }
73}
74
75impl From<&NodeState> for PersistState {
76    fn from(value: &NodeState) -> Self {
77        Self {
78            // `.clone()` (not a bare field read): the private keys are no longer `Copy`, so copying
79            // them out of a `&NodeState` is now an explicit clone. The original keys stay owned by
80            // `value` and zeroize on their own drop.
81            node_key: value.node_keys.private.clone(),
82            machine_key: value.machine_keys.private.clone(),
83            network_lock_key: value.network_lock_keys.private.clone(),
84            old_node_key: value.old_node_key,
85            acme_account_key: value.acme_account_key.clone(),
86        }
87    }
88}
89
90impl From<NodeState> for PersistState {
91    fn from(value: NodeState) -> Self {
92        Self::from(&value)
93    }
94}
95
96impl Default for PersistState {
97    fn default() -> Self {
98        Self {
99            machine_key: MachinePrivateKey::random(),
100            network_lock_key: NetworkLockPrivateKey::random(),
101            node_key: NodePrivateKey::random(),
102            old_node_key: None,
103            acme_account_key: None,
104        }
105    }
106}
107
108/// The complete runtime key state for a Tailscale node.
109#[derive(Clone, Default)]
110#[cfg_attr(feature = "serde", derive(serde::Deserialize))]
111pub struct NodeState {
112    /// The [`DiscoKeyPair`] this Tailnet peer uses for the Disco protocol.
113    ///
114    /// These should be randomly generated for each run of a Tailscale device.
115    pub disco_keys: DiscoKeyPair,
116
117    /// The [`MachineKeyPair`] for the hardware this Tailnet peer runs on.
118    pub machine_keys: MachineKeyPair,
119
120    /// The [`NetworkLockKeyPair`] for this Tailnet peer, for use with Tailnet Lock.
121    pub network_lock_keys: NetworkLockKeyPair,
122
123    /// The [`NodeKeyPair`] for this Tailnet peer.
124    pub node_keys: NodeKeyPair,
125
126    /// The node's previous node public key during a rotation (see
127    /// [`PersistState::old_node_key`]). Threaded to registration as `RegisterRequest.OldNodeKey`.
128    #[cfg_attr(feature = "serde", serde(default))]
129    pub old_node_key: Option<NodePublicKey>,
130
131    /// The persisted ACME account key (PKCS#8 DER), threaded from
132    /// [`PersistState::acme_account_key`]. The `acme` cert-issuance path reads this to reuse the
133    /// same Let's Encrypt account across renewals. `None` when no ACME account is provisioned.
134    ///
135    /// Wrapped in [`Zeroizing`](zeroize::Zeroizing) so the DER private-key bytes are wiped from
136    /// memory on drop; serializes transparently via the inner `Vec` (unchanged JSON shape).
137    #[cfg_attr(feature = "serde", serde(default))]
138    pub acme_account_key: Option<zeroize::Zeroizing<alloc::vec::Vec<u8>>>,
139}
140
141impl Debug for NodeState {
142    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
143        f.debug_tuple("NodeState")
144            .field(&self.machine_keys.public)
145            .field(&self.node_keys.public)
146            .field(&self.disco_keys.public)
147            .field(&self.network_lock_keys.public)
148            .finish()
149    }
150}
151
152impl Display for NodeState {
153    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
154        Debug::fmt(self, f)
155    }
156}
157
158impl NodeState {
159    /// Generate a new [`NodeState`]. All keys get random values.
160    pub fn generate() -> Self {
161        Default::default()
162    }
163
164    /// Rotate the node key for re-registration, the runtime twin of
165    /// [`PersistState::rotate_node_key`]: record the current node public key as
166    /// [`old_node_key`](NodeState::old_node_key) and replace the [`node_keys`](NodeState::node_keys)
167    /// pair with a freshly-generated one. The next registration built from this state sends the prior
168    /// key as `RegisterRequest.OldNodeKey`, so control links the new node key to the node's existing
169    /// identity instead of treating it as a brand-new node (Go's `regen`/`doLogin` flow).
170    ///
171    /// Only the **node key** rotates. The disco and machine keys are deliberately left untouched: the
172    /// data plane (magicsock/WireGuard sessions, disco) keys on those and on per-peer keys, never on
173    /// the self node key, so a node-key rotation does not re-key or flap an established tunnel. The
174    /// node key is a control-plane identity. (The disco ping packet does carry the self node key as a
175    /// claimed-sender identity — a caller that rotates at runtime should refresh magicsock's copy so
176    /// outbound pings advertise the new key, but that is a magicsock concern, not a key-state one.)
177    ///
178    /// **`old_node_key` anchor lifecycle.** This records the prior key as `old_node_key` **only if no
179    /// rotation anchor is already held** ([`old_node_key`](NodeState::old_node_key) is currently
180    /// `None`). A second rotation *before* a successful re-register therefore preserves the
181    /// **original** pre-expiry key as the anchor, rather than overwriting it with the intermediate
182    /// rotated key — control links a rotation against the key it already knows, so the anchor must
183    /// stay pinned to the last *registered* key, not a transient one. The anchor is released back to
184    /// `None` by [`clear_old_node_key`](NodeState::clear_old_node_key), which the re-register path
185    /// calls on a **successful** register: once control has accepted the rotated key, that key is now
186    /// the node's known identity, so a subsequent genuine expiry cycle correctly captures it as the
187    /// new anchor. (A runtime caller that drives repeated rotations is additionally expected not to
188    /// rotate again while a re-register is unconfirmed; this guard is the belt-and-suspenders.)
189    ///
190    // TODO(TKA): on a tailnet-lock-enabled tailnet, a node-key rotation must also re-sign the new node
191    // key with the network-lock key and send the new `RegisterRequest.NodeKeySignature`. This
192    // primitive covers the non-TKA path; the TKA re-sign is a separate follow-up, so an auto-reauth
193    // caller must gate rotation OFF while lock enforcement is active (else the node locks itself out
194    // of locked peers with an unsigned key).
195    pub fn rotate_node_key(&mut self) {
196        // Preserve an existing anchor: only capture the current key as `old_node_key` when none is
197        // held, so two rotations before a successful register keep the ORIGINAL pre-expiry key as the
198        // linkage anchor control expects (see the lifecycle note above).
199        if self.old_node_key.is_none() {
200            self.old_node_key = Some(self.node_keys.public);
201        }
202        self.node_keys = NodeKeyPair::new();
203    }
204
205    /// Release the rotation anchor: drop any [`old_node_key`](NodeState::old_node_key) back to `None`.
206    ///
207    /// Called by the re-register path on a **successful** register. After control accepts a rotated
208    /// node key, that key is the node's known identity, so the next genuine node-key rotation should
209    /// anchor on *it* (capture it fresh as `old_node_key`) rather than re-sending a now-stale prior
210    /// key. Paired with [`rotate_node_key`](NodeState::rotate_node_key)'s preserve-if-`Some` guard:
211    /// the anchor is captured once per rotation episode and cleared once the episode is confirmed.
212    /// A no-op when no anchor is held (the steady-state case — every successful map-poll re-register
213    /// calls this and it harmlessly does nothing while `old_node_key` is already `None`).
214    pub fn clear_old_node_key(&mut self) {
215        self.old_node_key = None;
216    }
217}
218
219impl From<&PersistState> for NodeState {
220    fn from(value: &PersistState) -> Self {
221        Self {
222            disco_keys: Default::default(),
223            // `.clone().into()`: building each keypair consumes a private key, which can no longer
224            // be `Copy`-d out of the `&PersistState`. Clone the stored key, then derive the pair
225            // (the pair's public half is computed from a borrow inside `From<$private>`).
226            node_keys: value.node_key.clone().into(),
227            machine_keys: value.machine_key.clone().into(),
228            network_lock_keys: value.network_lock_key.clone().into(),
229            old_node_key: value.old_node_key,
230            acme_account_key: value.acme_account_key.clone(),
231        }
232    }
233}
234
235impl From<PersistState> for NodeState {
236    fn from(value: PersistState) -> Self {
237        Self::from(&value)
238    }
239}
240
241#[cfg(test)]
242mod tests {
243    use super::*;
244
245    #[test]
246    fn rotate_node_key_sets_old_and_fresh() {
247        let mut state = PersistState::default();
248        let before_pub = state.node_key.public_key();
249
250        state.rotate_node_key();
251
252        assert_eq!(state.old_node_key, Some(before_pub));
253        assert_ne!(state.node_key.public_key(), before_pub);
254    }
255
256    #[test]
257    fn node_state_rotate_node_key_sets_old_and_fresh() {
258        let mut state = NodeState::generate();
259        let before_pub = state.node_keys.public;
260        // The other key roles must be preserved across a node-key rotation (only the node key
261        // rotates — disco/machine/network-lock are unchanged, which is what keeps tunnels from
262        // flapping and keeps a locked-tailnet re-sign possible).
263        let disco_before = state.disco_keys.public;
264        let machine_before = state.machine_keys.public;
265        let lock_before = state.network_lock_keys.public;
266
267        state.rotate_node_key();
268
269        // Old key recorded, node key replaced with a fresh one.
270        assert_eq!(state.old_node_key, Some(before_pub));
271        assert_ne!(state.node_keys.public, before_pub);
272        // The fresh keypair's public matches its private (it's a real, consistent pair).
273        assert_eq!(
274            state.node_keys.public,
275            NodePublicKey::from(&state.node_keys.private)
276        );
277        // Every other key role is untouched.
278        assert_eq!(state.disco_keys.public, disco_before);
279        assert_eq!(state.machine_keys.public, machine_before);
280        assert_eq!(state.network_lock_keys.public, lock_before);
281    }
282
283    #[test]
284    fn node_state_two_rotations_without_success_preserve_original_anchor() {
285        // Two rotations WITHOUT an intervening successful register (no `clear_old_node_key`) must keep
286        // the ORIGINAL pre-expiry key as `old_node_key`, never overwrite it with the intermediate
287        // rotated key. Control links a rotation against the key it already knows (the last registered
288        // one), so a second rotation before recovery must not lose that anchor — the core of the
289        // "preserve the original OldNodeKey anchor" fix.
290        let mut state = NodeState::generate();
291        let original = state.node_keys.public;
292
293        state.rotate_node_key();
294        let intermediate = state.node_keys.public;
295        assert_eq!(
296            state.old_node_key,
297            Some(original),
298            "first rotation anchors on the original key"
299        );
300        assert_ne!(intermediate, original);
301
302        state.rotate_node_key();
303        assert_eq!(
304            state.old_node_key,
305            Some(original),
306            "a SECOND rotation before a successful register must preserve the ORIGINAL anchor, not \
307             overwrite it with the intermediate key"
308        );
309        assert_ne!(
310            state.node_keys.public, intermediate,
311            "the node key still rotates fresh on the second rotation"
312        );
313        assert_ne!(state.node_keys.public, original);
314    }
315
316    #[test]
317    fn node_state_rotation_after_clear_captures_new_current_key() {
318        // After a successful register clears the anchor (`clear_old_node_key`), the NEXT genuine
319        // rotation must capture the now-current (already-registered) key as the fresh anchor — not the
320        // long-gone original. This is the other half of the anchor lifecycle: clear-on-success lets a
321        // later expiry cycle re-anchor correctly.
322        let mut state = NodeState::generate();
323        let original = state.node_keys.public;
324
325        state.rotate_node_key();
326        let current = state.node_keys.public;
327        assert_eq!(state.old_node_key, Some(original));
328
329        // A successful register confirms `current` as the node's known identity at control.
330        state.clear_old_node_key();
331        assert_eq!(
332            state.old_node_key, None,
333            "a successful register releases the anchor"
334        );
335
336        // The next genuine expiry cycle rotates again — now anchoring on `current`, not `original`.
337        state.rotate_node_key();
338        assert_eq!(
339            state.old_node_key,
340            Some(current),
341            "a rotation AFTER a success captures the new current (registered) key as the anchor"
342        );
343        assert_ne!(state.node_keys.public, current);
344    }
345
346    #[test]
347    fn node_state_clear_old_node_key_is_noop_when_none() {
348        // The steady-state call: every successful map-poll re-register calls `clear_old_node_key`, and
349        // it must harmlessly do nothing while no anchor is held (and leave the live keys untouched).
350        let mut state = NodeState::generate();
351        let node_before = state.node_keys.public;
352        assert_eq!(state.old_node_key, None);
353
354        state.clear_old_node_key();
355
356        assert_eq!(state.old_node_key, None);
357        assert_eq!(
358            state.node_keys.public, node_before,
359            "clearing the anchor must not touch live keys"
360        );
361    }
362
363    #[test]
364    fn node_state_rotate_threads_to_persist_old_node_key() {
365        // After a runtime rotation, converting back to a PersistState carries the prior public key as
366        // old_node_key (so an embedder persisting the rotated state keeps the OldNodeKey linkage).
367        let mut state = NodeState::generate();
368        let before_pub = state.node_keys.public;
369        state.rotate_node_key();
370        let persist = PersistState::from(&state);
371        assert_eq!(persist.old_node_key, Some(before_pub));
372        assert_eq!(persist.node_key.public_key(), state.node_keys.public);
373    }
374
375    #[test]
376    fn node_state_threads_old_node_key() {
377        let mut persist = PersistState::default();
378        let some_pub = NodePrivateKey::random().public_key();
379        persist.old_node_key = Some(some_pub);
380
381        let node_state = NodeState::from(&persist);
382        assert_eq!(node_state.old_node_key, Some(some_pub));
383
384        let round_trip = PersistState::from(&node_state);
385        assert_eq!(round_trip.old_node_key, Some(some_pub));
386    }
387
388    #[test]
389    fn default_persist_state_has_no_old_key() {
390        assert!(PersistState::default().old_node_key.is_none());
391    }
392
393    #[cfg(feature = "serde")]
394    #[test]
395    fn persist_state_old_node_key_serde_default() {
396        // A default PersistState round-trips with no old key.
397        let json = serde_json::to_string(&PersistState::default()).unwrap();
398        let parsed: PersistState = serde_json::from_str(&json).unwrap();
399        assert!(parsed.old_node_key.is_none());
400
401        // A serialized form that OMITS `old_node_key` still deserializes (serde(default) →
402        // backward-compat with pre-rotation persisted state).
403        let mut value: serde_json::Value = serde_json::from_str(&json).unwrap();
404        value
405            .as_object_mut()
406            .unwrap()
407            .remove("old_node_key")
408            .expect("default serializes the field");
409        let parsed: PersistState =
410            serde_json::from_value(value).expect("missing old_node_key deserializes via default");
411        assert!(parsed.old_node_key.is_none());
412    }
413
414    #[cfg(feature = "serde")]
415    #[test]
416    fn persist_state_acme_account_key_serde_default_and_round_trip() {
417        use alloc::vec;
418
419        // An old key file that OMITS `acme_account_key` still deserializes (serde(default) → None).
420        let json = serde_json::to_string(&PersistState::default()).unwrap();
421        let mut value: serde_json::Value = serde_json::from_str(&json).unwrap();
422        value
423            .as_object_mut()
424            .unwrap()
425            .remove("acme_account_key")
426            .expect("default serializes the field");
427        let parsed: PersistState = serde_json::from_value(value)
428            .expect("missing acme_account_key deserializes via default");
429        assert!(parsed.acme_account_key.is_none());
430
431        // A `Some(der)` value round-trips through serde and across the NodeState conversions.
432        // The `Zeroizing` wrapper must NOT change the on-wire JSON: it serializes as the inner
433        // byte `Vec`, so the rendered JSON is identical to a bare `Vec<u8>`.
434        let state = PersistState {
435            acme_account_key: Some(zeroize::Zeroizing::new(vec![1u8, 2, 3, 4])),
436            ..Default::default()
437        };
438        let json = serde_json::to_string(&state).unwrap();
439        assert!(
440            json.contains("\"acme_account_key\":[1,2,3,4]"),
441            "Zeroizing must serialize as the bare byte array (unchanged JSON shape): {json}"
442        );
443        let parsed: PersistState = serde_json::from_str(&json).unwrap();
444        assert_eq!(
445            parsed.acme_account_key.as_deref().map(|v| v.as_slice()),
446            Some(&[1u8, 2, 3, 4][..])
447        );
448
449        let node_state = NodeState::from(&state);
450        assert_eq!(
451            node_state.acme_account_key.as_deref().map(|v| v.as_slice()),
452            Some(&[1u8, 2, 3, 4][..])
453        );
454        let round_trip = PersistState::from(&node_state);
455        assert_eq!(
456            round_trip.acme_account_key.as_deref().map(|v| v.as_slice()),
457            Some(&[1u8, 2, 3, 4][..])
458        );
459    }
460}