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}