Skip to main content

nwep/
keypair.rs

1
2use crate::ffi;
3use crate::error::{check, Error};
4use crate::types::{NodeId, Identity};
5
6/// `Keypair` holds an Ed25519 keypair used to authenticate a NWEP node.
7///
8/// The keypair is stored in a heap-allocated [`Box`] so that its memory address remains
9/// stable when the `Keypair` value is moved. The C library keeps a raw pointer to the
10/// keypair for TLS signing during handshakes; if the data were allowed to move, that
11/// pointer would become dangling.
12///
13/// Key material is zeroed on drop via [`nwep_keypair_clear`].
14///
15/// # Examples
16///
17/// ```no_run
18/// use nwep::Keypair;
19/// nwep::init().unwrap();
20/// let kp = Keypair::generate().unwrap();
21/// println!("node id: {}", kp.node_id().unwrap());
22/// ```
23pub struct Keypair {
24    // Heap-allocated so the nwep_keypair address stays stable when Keypair moves.
25    // The C library stores a pointer to the keypair for TLS signing, so the data
26    // must not move after nwep_server_new / nwep_client_new is called.
27    inner: Box<ffi::nwep_keypair>,
28}
29
30impl Keypair {
31    /// `generate` creates a new Ed25519 keypair from OS-provided randomness.
32    ///
33    /// # Errors
34    ///
35    /// Returns an [`Error`] if the underlying C library call fails.
36    pub fn generate() -> Result<Self, Error> {
37        let mut kp = Box::new(ffi::nwep_keypair { pubkey: [0u8; 32], privkey: [0u8; 64] });
38        check(unsafe { ffi::nwep_keypair_generate(kp.as_mut()) })?;
39        Ok(Keypair { inner: kp })
40    }
41
42    /// `from_seed` derives an Ed25519 keypair from a 32-byte seed.
43    ///
44    /// The seed is the first 32 bytes of the standard 64-byte Ed25519 private key
45    /// representation (`seed || pubkey`). Passing the same seed always produces the
46    /// same keypair.
47    ///
48    /// # Errors
49    ///
50    /// Returns an [`Error`] if the C library call fails.
51    pub fn from_seed(seed: &[u8; 32]) -> Result<Self, Error> {
52        let mut kp = Box::new(ffi::nwep_keypair { pubkey: [0u8; 32], privkey: [0u8; 64] });
53        check(unsafe { ffi::nwep_keypair_from_seed(kp.as_mut(), seed.as_ptr()) })?;
54        Ok(Keypair { inner: kp })
55    }
56
57    /// `from_privkey` reconstructs a keypair from a 64-byte Ed25519 private key.
58    ///
59    /// The 64-byte private key is the concatenation `seed || pubkey`. The public key is
60    /// re-derived from the seed by the C library and the provided public-key suffix is
61    /// used for validation.
62    ///
63    /// # Errors
64    ///
65    /// Returns an [`Error`] if the key material is invalid or the C library call fails.
66    pub fn from_privkey(privkey: &[u8; 64]) -> Result<Self, Error> {
67        let mut kp = Box::new(ffi::nwep_keypair { pubkey: [0u8; 32], privkey: [0u8; 64] });
68        check(unsafe { ffi::nwep_keypair_from_privkey(kp.as_mut(), privkey.as_ptr()) })?;
69        Ok(Keypair { inner: kp })
70    }
71
72    /// `public_key` returns the 32-byte Ed25519 public key.
73    pub fn public_key(&self) -> [u8; 32] {
74        self.inner.pubkey
75    }
76
77    /// `seed` returns the 32-byte Ed25519 seed (first half of the private key).
78    ///
79    /// The seed is sufficient to fully reconstruct the keypair via [`Keypair::from_seed`].
80    pub fn seed(&self) -> [u8; 32] {
81        let mut seed = [0u8; 32];
82        seed.copy_from_slice(&self.inner.privkey[..32]);
83        seed
84    }
85
86    /// `private_key` returns the full 64-byte Ed25519 private key (`seed || pubkey`).
87    pub fn private_key(&self) -> [u8; 64] {
88        self.inner.privkey
89    }
90
91    /// `node_id` derives the [`NodeId`] for this keypair by hashing the public key.
92    ///
93    /// The NodeId uniquely identifies a node on the network and is embedded in NWEP
94    /// addresses, ensuring that connection targets cannot be impersonated.
95    ///
96    /// # Errors
97    ///
98    /// Returns an [`Error`] if the C library call fails.
99    pub fn node_id(&self) -> Result<NodeId, Error> {
100        let mut nid = ffi::nwep_nodeid { data: [0u8; 32] };
101        check(unsafe { ffi::nwep_nodeid_from_keypair(&mut nid, self.inner.as_ref()) })?;
102        Ok(NodeId(nid.data))
103    }
104
105    /// `clear` zeroes the key material held by this keypair.
106    ///
107    /// This is called automatically on drop. Call it explicitly when you need to
108    /// erase key material before the value goes out of scope, for example before
109    /// passing ownership elsewhere.
110    pub fn clear(&mut self) {
111        unsafe { ffi::nwep_keypair_clear(self.inner.as_mut()) }
112    }
113
114    pub(crate) fn as_ffi(&self) -> &ffi::nwep_keypair {
115        self.inner.as_ref()
116    }
117
118    pub(crate) fn as_ffi_mut(&mut self) -> &mut ffi::nwep_keypair {
119        self.inner.as_mut()
120    }
121}
122
123// SAFETY: nwep_keypair is plain key-material bytes with no thread-local state.
124// The C library accesses the keypair only through the pointer passed to
125// nwep_client_new / nwep_server_new, so it is safe to send across threads.
126unsafe impl Send for Keypair {}
127
128impl Drop for Keypair {
129    fn drop(&mut self) {
130        self.clear();
131    }
132}
133
134/// `node_id_from_pubkey` derives a [`NodeId`] from a raw 32-byte Ed25519 public key.
135///
136/// Use this when you have a public key but no full [`Keypair`], for example when
137/// verifying the identity of a remote peer.
138///
139/// # Errors
140///
141/// Returns an [`Error`] if the C library call fails.
142pub fn node_id_from_pubkey(pubkey: &[u8; 32]) -> Result<NodeId, Error> {
143    let mut nid = ffi::nwep_nodeid { data: [0u8; 32] };
144    check(unsafe { ffi::nwep_nodeid_from_pubkey(&mut nid, pubkey.as_ptr()) })?;
145    Ok(NodeId(nid.data))
146}
147
148/// `node_id_eq` returns `true` if two [`NodeId`] values are equal.
149///
150/// Delegates to the C library's constant-time comparison to avoid timing side-channels.
151pub fn node_id_eq(a: &NodeId, b: &NodeId) -> bool {
152    let fa = ffi::nwep_nodeid { data: a.0 };
153    let fb = ffi::nwep_nodeid { data: b.0 };
154    unsafe { ffi::nwep_nodeid_eq(&fa, &fb) != 0 }
155}
156
157/// `RecoveryAuthority` holds a separate keypair used to sign key revocations.
158///
159/// Without a recovery authority, a node whose key is compromised cannot be revoked;
160/// the old key would remain trusted indefinitely. The recovery authority keypair should
161/// be stored offline or in a secure enclave separate from the node's operational key.
162///
163/// Key material is zeroed on drop.
164pub struct RecoveryAuthority {
165    inner: ffi::nwep_recovery_authority,
166}
167
168impl RecoveryAuthority {
169    /// `new` generates a fresh recovery authority with a randomly generated keypair.
170    ///
171    /// # Errors
172    ///
173    /// Returns an [`Error`] if the C library call fails.
174    pub fn new() -> Result<Self, Error> {
175        let mut ra = ffi::nwep_recovery_authority {
176            keypair: ffi::nwep_keypair { pubkey: [0u8; 32], privkey: [0u8; 64] },
177            initialized: 0,
178        };
179        check(unsafe { ffi::nwep_recovery_authority_new(&mut ra) })?;
180        Ok(RecoveryAuthority { inner: ra })
181    }
182
183    /// `from_keypair` constructs a recovery authority from an existing [`Keypair`].
184    ///
185    /// Use this to restore a recovery authority from a stored keypair, or to designate
186    /// a specific key as the recovery authority for a node.
187    ///
188    /// # Errors
189    ///
190    /// Returns an [`Error`] if the C library call fails.
191    pub fn from_keypair(kp: &Keypair) -> Result<Self, Error> {
192        let mut ra = ffi::nwep_recovery_authority {
193            keypair: ffi::nwep_keypair { pubkey: [0u8; 32], privkey: [0u8; 64] },
194            initialized: 0,
195        };
196        check(unsafe { ffi::nwep_recovery_authority_from_keypair(&mut ra, kp.as_ffi()) })?;
197        Ok(RecoveryAuthority { inner: ra })
198    }
199
200    /// `public_key` returns the recovery authority's 32-byte public key, if initialized.
201    ///
202    /// Returns `None` when the authority has not been fully initialized by the C library.
203    pub fn public_key(&self) -> Option<[u8; 32]> {
204        let ptr = unsafe { ffi::nwep_recovery_authority_get_pubkey(&self.inner) };
205        if ptr.is_null() {
206            None
207        } else {
208            let mut pk = [0u8; 32];
209            unsafe { pk.copy_from_slice(std::slice::from_raw_parts(ptr, 32)) };
210            Some(pk)
211        }
212    }
213
214    /// `clear` zeroes all key material held by this recovery authority.
215    ///
216    /// Called automatically on drop.
217    pub fn clear(&mut self) {
218        unsafe { ffi::nwep_recovery_authority_clear(&mut self.inner) }
219    }
220
221    pub(crate) fn as_ffi(&self) -> &ffi::nwep_recovery_authority {
222        &self.inner
223    }
224}
225
226impl Drop for RecoveryAuthority {
227    fn drop(&mut self) {
228        self.clear();
229    }
230}
231
232/// `ManagedIdentity` tracks an NWEP node's full key lifecycle, including rotation and revocation.
233///
234/// A managed identity maintains the current active keypair along with a history of previous
235/// keys that are still within their grace period. During a rotation the old key remains
236/// valid for a short time so that in-flight connections are not disrupted. The identity also
237/// records whether the node has been revoked via its [`RecoveryAuthority`].
238///
239/// [`update`](ManagedIdentity::update) must be called periodically so that the C library can
240/// advance its internal rotation schedule and expire stale keys.
241///
242/// Key material is zeroed on drop.
243pub struct ManagedIdentity {
244    inner: ffi::nwep_managed_identity,
245}
246
247impl ManagedIdentity {
248    /// `new` initialises a managed identity from an existing keypair and optional recovery authority.
249    ///
250    /// `now` is the current timestamp used to seed the rotation schedule. Passing `None` for `ra`
251    /// creates an identity that cannot be revoked; pass `Some` to enable revocation support.
252    ///
253    /// # Errors
254    ///
255    /// Returns an [`Error`] if the C library call fails.
256    pub fn new(kp: &Keypair, ra: Option<&RecoveryAuthority>, now: crate::Tstamp) -> Result<Self, Error> {
257        let mut mi = unsafe { std::mem::zeroed::<ffi::nwep_managed_identity>() };
258        let ra_ptr = ra.map_or(std::ptr::null(), |r| r.as_ffi());
259        check(unsafe { ffi::nwep_managed_identity_new(&mut mi, kp.as_ffi(), ra_ptr, now) })?;
260        Ok(ManagedIdentity { inner: mi })
261    }
262
263    /// `rotate` generates a new keypair and adds it as the active key for this identity.
264    ///
265    /// The previous key remains valid for a grace period so that peers with cached credentials
266    /// can complete in-flight operations. The rotation is signed with the old key to form a
267    /// verifiable chain of custody.
268    ///
269    /// # Errors
270    ///
271    /// Returns an [`Error`] if the C library call fails.
272    pub fn rotate(&mut self, now: crate::Tstamp) -> Result<(), Error> {
273        check(unsafe { ffi::nwep_managed_identity_rotate(&mut self.inner, now) })
274    }
275
276    /// `update` advances the identity's internal rotation schedule to the given timestamp.
277    ///
278    /// Call this periodically (e.g. once per second) so that the C library can expire keys
279    /// that have passed their grace period and trigger scheduled rotations.
280    pub fn update(&mut self, now: crate::Tstamp) {
281        unsafe { ffi::nwep_managed_identity_update(&mut self.inner, now) }
282    }
283
284    /// `is_revoked` returns `true` if this identity has been revoked.
285    ///
286    /// A revoked identity should be refused new connections. Revocation is permanent within
287    /// the managed identity structure; re-use the underlying node ID is not possible after
288    /// revocation.
289    pub fn is_revoked(&self) -> bool {
290        unsafe { ffi::nwep_managed_identity_is_revoked(&self.inner) != 0 }
291    }
292
293    /// `revoke` marks this identity as revoked, signing the revocation with the recovery authority.
294    ///
295    /// After calling this, [`is_revoked`](ManagedIdentity::is_revoked) returns `true` and the
296    /// revocation can be published to peers as a [`Revocation`] struct.
297    ///
298    /// # Errors
299    ///
300    /// Returns an [`Error`] if the identity has no recovery authority, or if the C library call
301    /// fails.
302    pub fn revoke(&mut self, ra: &RecoveryAuthority, now: crate::Tstamp) -> Result<(), Error> {
303        check(unsafe { ffi::nwep_managed_identity_revoke(&mut self.inner, ra.as_ffi(), now) })
304    }
305
306    /// `node_id` returns the stable [`NodeId`] for this managed identity.
307    ///
308    /// The node ID is derived from the initial keypair and does not change across rotations.
309    pub fn node_id(&self) -> NodeId {
310        NodeId(self.inner.nodeid.data)
311    }
312
313    /// `active_keypair` returns a copy of the currently active [`Keypair`], if any.
314    ///
315    /// Returns `None` if the identity has been revoked or has no active key. The returned
316    /// keypair is a copy of the C-managed data; it will not be invalidated by subsequent
317    /// rotations.
318    pub fn active_keypair(&self) -> Option<Keypair> {
319        let ptr = unsafe { ffi::nwep_managed_identity_get_active(&self.inner) };
320        if ptr.is_null() {
321            None
322        } else {
323            // Copy the keypair data out of the C-managed storage (matching Go's memcpy approach).
324            Some(Keypair { inner: unsafe { Box::new(*ptr) } })
325        }
326    }
327
328    /// `active_keys` returns copies of all keypairs that are currently within their validity window.
329    ///
330    /// During a rotation grace period both the old and new keypairs are active. Use this when
331    /// you need to validate signatures from any currently trusted key, not just the newest one.
332    pub fn active_keys(&self) -> Vec<Keypair> {
333        let mut ptrs = [std::ptr::null::<ffi::nwep_keypair>(); crate::types::MAX_ACTIVE_KEYS];
334        let n = unsafe {
335            ffi::nwep_managed_identity_get_active_keys(
336                &self.inner,
337                ptrs.as_mut_ptr() as *mut *const ffi::nwep_keypair,
338                crate::types::MAX_ACTIVE_KEYS,
339            )
340        };
341        (0..n).filter_map(|i| {
342            if ptrs[i].is_null() { None } else { Some(Keypair { inner: unsafe { Box::new(*ptrs[i]) } }) }
343        }).collect()
344    }
345
346    /// `active_pubkey` returns the 32-byte public key of the currently active keypair, if any.
347    ///
348    /// This is a cheaper alternative to [`active_keypair`](ManagedIdentity::active_keypair) when
349    /// only the public key is needed.
350    pub fn active_pubkey(&self) -> Option<[u8; 32]> {
351        let ptr = unsafe { ffi::nwep_managed_identity_get_active(&self.inner) };
352        if ptr.is_null() {
353            None
354        } else {
355            Some(unsafe { (*ptr).pubkey })
356        }
357    }
358
359    /// `identity` returns an [`Identity`] snapshot containing the current active public key and node ID.
360    ///
361    /// If there is no active keypair, the public key field in the returned `Identity` is all zeros.
362    pub fn identity(&self) -> Identity {
363        let pk = self.active_pubkey().unwrap_or([0u8; 32]);
364        Identity { pubkey: pk, node_id: self.node_id() }
365    }
366
367    /// `clear` zeroes all key material held by this managed identity.
368    ///
369    /// Called automatically on drop.
370    pub fn clear(&mut self) {
371        unsafe { ffi::nwep_managed_identity_clear(&mut self.inner) }
372    }
373
374    #[allow(dead_code)]
375    pub(crate) fn as_ffi(&self) -> &ffi::nwep_managed_identity {
376        &self.inner
377    }
378}
379
380impl Drop for ManagedIdentity {
381    fn drop(&mut self) {
382        self.clear();
383    }
384}
385
386/// `verify_revocation` checks the cryptographic signature on a [`Revocation`] record.
387///
388/// Returns `Ok(())` if the signature is valid, meaning the revocation was produced by the
389/// recovery authority that originally registered with the node's managed identity.
390///
391/// # Errors
392///
393/// Returns an [`Error`] if the signature is invalid or the C library call fails.
394pub fn verify_revocation(rev: &Revocation) -> Result<(), Error> {
395    let ffi_rev = rev.to_ffi();
396    check(unsafe { ffi::nwep_managed_identity_verify_revocation(&ffi_rev) })
397}
398
399/// `Revocation` is the serialised form of a key revocation record for an NWEP node.
400///
401/// A `Revocation` is produced by [`ManagedIdentity::revoke`] and can be distributed to peers
402/// so they stop trusting the revoked node ID. Recipients call [`verify_revocation`] to confirm
403/// the record is authentic before acting on it.
404#[derive(Clone, Debug)]
405pub struct Revocation {
406    /// The node ID being revoked.
407    pub node_id: NodeId,
408    /// The timestamp at which the revocation was issued.
409    pub timestamp: crate::Tstamp,
410    /// The public key of the recovery authority that signed this revocation.
411    pub recovery_pubkey: [u8; 32],
412    /// Ed25519 signature over the revocation payload, produced by the recovery authority.
413    pub signature: [u8; 64],
414}
415
416impl Revocation {
417    fn to_ffi(&self) -> ffi::nwep_revocation {
418        ffi::nwep_revocation {
419            nodeid: ffi::nwep_nodeid { data: self.node_id.0 },
420            timestamp: self.timestamp,
421            recovery_pubkey: self.recovery_pubkey,
422            signature: self.signature,
423        }
424    }
425}