nwep-rs 0.1.8

Rust bindings for the NWEP (WEB/1) protocol library
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425

use crate::ffi;
use crate::error::{check, Error};
use crate::types::{NodeId, Identity};

/// `Keypair` holds an Ed25519 keypair used to authenticate a NWEP node.
///
/// The keypair is stored in a heap-allocated [`Box`] so that its memory address remains
/// stable when the `Keypair` value is moved. The C library keeps a raw pointer to the
/// keypair for TLS signing during handshakes; if the data were allowed to move, that
/// pointer would become dangling.
///
/// Key material is zeroed on drop via [`nwep_keypair_clear`].
///
/// # Examples
///
/// ```no_run
/// use nwep::Keypair;
/// nwep::init().unwrap();
/// let kp = Keypair::generate().unwrap();
/// println!("node id: {}", kp.node_id().unwrap());
/// ```
pub struct Keypair {
    // Heap-allocated so the nwep_keypair address stays stable when Keypair moves.
    // The C library stores a pointer to the keypair for TLS signing, so the data
    // must not move after nwep_server_new / nwep_client_new is called.
    inner: Box<ffi::nwep_keypair>,
}

impl Keypair {
    /// `generate` creates a new Ed25519 keypair from OS-provided randomness.
    ///
    /// # Errors
    ///
    /// Returns an [`Error`] if the underlying C library call fails.
    pub fn generate() -> Result<Self, Error> {
        let mut kp = Box::new(ffi::nwep_keypair { pubkey: [0u8; 32], privkey: [0u8; 64] });
        check(unsafe { ffi::nwep_keypair_generate(kp.as_mut()) })?;
        Ok(Keypair { inner: kp })
    }

    /// `from_seed` derives an Ed25519 keypair from a 32-byte seed.
    ///
    /// The seed is the first 32 bytes of the standard 64-byte Ed25519 private key
    /// representation (`seed || pubkey`). Passing the same seed always produces the
    /// same keypair.
    ///
    /// # Errors
    ///
    /// Returns an [`Error`] if the C library call fails.
    pub fn from_seed(seed: &[u8; 32]) -> Result<Self, Error> {
        let mut kp = Box::new(ffi::nwep_keypair { pubkey: [0u8; 32], privkey: [0u8; 64] });
        check(unsafe { ffi::nwep_keypair_from_seed(kp.as_mut(), seed.as_ptr()) })?;
        Ok(Keypair { inner: kp })
    }

    /// `from_privkey` reconstructs a keypair from a 64-byte Ed25519 private key.
    ///
    /// The 64-byte private key is the concatenation `seed || pubkey`. The public key is
    /// re-derived from the seed by the C library and the provided public-key suffix is
    /// used for validation.
    ///
    /// # Errors
    ///
    /// Returns an [`Error`] if the key material is invalid or the C library call fails.
    pub fn from_privkey(privkey: &[u8; 64]) -> Result<Self, Error> {
        let mut kp = Box::new(ffi::nwep_keypair { pubkey: [0u8; 32], privkey: [0u8; 64] });
        check(unsafe { ffi::nwep_keypair_from_privkey(kp.as_mut(), privkey.as_ptr()) })?;
        Ok(Keypair { inner: kp })
    }

    /// `public_key` returns the 32-byte Ed25519 public key.
    pub fn public_key(&self) -> [u8; 32] {
        self.inner.pubkey
    }

    /// `seed` returns the 32-byte Ed25519 seed (first half of the private key).
    ///
    /// The seed is sufficient to fully reconstruct the keypair via [`Keypair::from_seed`].
    pub fn seed(&self) -> [u8; 32] {
        let mut seed = [0u8; 32];
        seed.copy_from_slice(&self.inner.privkey[..32]);
        seed
    }

    /// `private_key` returns the full 64-byte Ed25519 private key (`seed || pubkey`).
    pub fn private_key(&self) -> [u8; 64] {
        self.inner.privkey
    }

    /// `node_id` derives the [`NodeId`] for this keypair by hashing the public key.
    ///
    /// The NodeId uniquely identifies a node on the network and is embedded in NWEP
    /// addresses, ensuring that connection targets cannot be impersonated.
    ///
    /// # Errors
    ///
    /// Returns an [`Error`] if the C library call fails.
    pub fn node_id(&self) -> Result<NodeId, Error> {
        let mut nid = ffi::nwep_nodeid { data: [0u8; 32] };
        check(unsafe { ffi::nwep_nodeid_from_keypair(&mut nid, self.inner.as_ref()) })?;
        Ok(NodeId(nid.data))
    }

    /// `clear` zeroes the key material held by this keypair.
    ///
    /// This is called automatically on drop. Call it explicitly when you need to
    /// erase key material before the value goes out of scope, for example before
    /// passing ownership elsewhere.
    pub fn clear(&mut self) {
        unsafe { ffi::nwep_keypair_clear(self.inner.as_mut()) }
    }

    pub(crate) fn as_ffi(&self) -> &ffi::nwep_keypair {
        self.inner.as_ref()
    }

    pub(crate) fn as_ffi_mut(&mut self) -> &mut ffi::nwep_keypair {
        self.inner.as_mut()
    }
}

// SAFETY: nwep_keypair is plain key-material bytes with no thread-local state.
// The C library accesses the keypair only through the pointer passed to
// nwep_client_new / nwep_server_new, so it is safe to send across threads.
unsafe impl Send for Keypair {}

impl Drop for Keypair {
    fn drop(&mut self) {
        self.clear();
    }
}

/// `node_id_from_pubkey` derives a [`NodeId`] from a raw 32-byte Ed25519 public key.
///
/// Use this when you have a public key but no full [`Keypair`], for example when
/// verifying the identity of a remote peer.
///
/// # Errors
///
/// Returns an [`Error`] if the C library call fails.
pub fn node_id_from_pubkey(pubkey: &[u8; 32]) -> Result<NodeId, Error> {
    let mut nid = ffi::nwep_nodeid { data: [0u8; 32] };
    check(unsafe { ffi::nwep_nodeid_from_pubkey(&mut nid, pubkey.as_ptr()) })?;
    Ok(NodeId(nid.data))
}

/// `node_id_eq` returns `true` if two [`NodeId`] values are equal.
///
/// Delegates to the C library's constant-time comparison to avoid timing side-channels.
pub fn node_id_eq(a: &NodeId, b: &NodeId) -> bool {
    let fa = ffi::nwep_nodeid { data: a.0 };
    let fb = ffi::nwep_nodeid { data: b.0 };
    unsafe { ffi::nwep_nodeid_eq(&fa, &fb) != 0 }
}

/// `RecoveryAuthority` holds a separate keypair used to sign key revocations.
///
/// Without a recovery authority, a node whose key is compromised cannot be revoked;
/// the old key would remain trusted indefinitely. The recovery authority keypair should
/// be stored offline or in a secure enclave separate from the node's operational key.
///
/// Key material is zeroed on drop.
pub struct RecoveryAuthority {
    inner: ffi::nwep_recovery_authority,
}

impl RecoveryAuthority {
    /// `new` generates a fresh recovery authority with a randomly generated keypair.
    ///
    /// # Errors
    ///
    /// Returns an [`Error`] if the C library call fails.
    pub fn new() -> Result<Self, Error> {
        let mut ra = ffi::nwep_recovery_authority {
            keypair: ffi::nwep_keypair { pubkey: [0u8; 32], privkey: [0u8; 64] },
            initialized: 0,
        };
        check(unsafe { ffi::nwep_recovery_authority_new(&mut ra) })?;
        Ok(RecoveryAuthority { inner: ra })
    }

    /// `from_keypair` constructs a recovery authority from an existing [`Keypair`].
    ///
    /// Use this to restore a recovery authority from a stored keypair, or to designate
    /// a specific key as the recovery authority for a node.
    ///
    /// # Errors
    ///
    /// Returns an [`Error`] if the C library call fails.
    pub fn from_keypair(kp: &Keypair) -> Result<Self, Error> {
        let mut ra = ffi::nwep_recovery_authority {
            keypair: ffi::nwep_keypair { pubkey: [0u8; 32], privkey: [0u8; 64] },
            initialized: 0,
        };
        check(unsafe { ffi::nwep_recovery_authority_from_keypair(&mut ra, kp.as_ffi()) })?;
        Ok(RecoveryAuthority { inner: ra })
    }

    /// `public_key` returns the recovery authority's 32-byte public key, if initialized.
    ///
    /// Returns `None` when the authority has not been fully initialized by the C library.
    pub fn public_key(&self) -> Option<[u8; 32]> {
        let ptr = unsafe { ffi::nwep_recovery_authority_get_pubkey(&self.inner) };
        if ptr.is_null() {
            None
        } else {
            let mut pk = [0u8; 32];
            unsafe { pk.copy_from_slice(std::slice::from_raw_parts(ptr, 32)) };
            Some(pk)
        }
    }

    /// `clear` zeroes all key material held by this recovery authority.
    ///
    /// Called automatically on drop.
    pub fn clear(&mut self) {
        unsafe { ffi::nwep_recovery_authority_clear(&mut self.inner) }
    }

    pub(crate) fn as_ffi(&self) -> &ffi::nwep_recovery_authority {
        &self.inner
    }
}

impl Drop for RecoveryAuthority {
    fn drop(&mut self) {
        self.clear();
    }
}

/// `ManagedIdentity` tracks an NWEP node's full key lifecycle, including rotation and revocation.
///
/// A managed identity maintains the current active keypair along with a history of previous
/// keys that are still within their grace period. During a rotation the old key remains
/// valid for a short time so that in-flight connections are not disrupted. The identity also
/// records whether the node has been revoked via its [`RecoveryAuthority`].
///
/// [`update`](ManagedIdentity::update) must be called periodically so that the C library can
/// advance its internal rotation schedule and expire stale keys.
///
/// Key material is zeroed on drop.
pub struct ManagedIdentity {
    inner: ffi::nwep_managed_identity,
}

impl ManagedIdentity {
    /// `new` initialises a managed identity from an existing keypair and optional recovery authority.
    ///
    /// `now` is the current timestamp used to seed the rotation schedule. Passing `None` for `ra`
    /// creates an identity that cannot be revoked; pass `Some` to enable revocation support.
    ///
    /// # Errors
    ///
    /// Returns an [`Error`] if the C library call fails.
    pub fn new(kp: &Keypair, ra: Option<&RecoveryAuthority>, now: crate::Tstamp) -> Result<Self, Error> {
        let mut mi = unsafe { std::mem::zeroed::<ffi::nwep_managed_identity>() };
        let ra_ptr = ra.map_or(std::ptr::null(), |r| r.as_ffi());
        check(unsafe { ffi::nwep_managed_identity_new(&mut mi, kp.as_ffi(), ra_ptr, now) })?;
        Ok(ManagedIdentity { inner: mi })
    }

    /// `rotate` generates a new keypair and adds it as the active key for this identity.
    ///
    /// The previous key remains valid for a grace period so that peers with cached credentials
    /// can complete in-flight operations. The rotation is signed with the old key to form a
    /// verifiable chain of custody.
    ///
    /// # Errors
    ///
    /// Returns an [`Error`] if the C library call fails.
    pub fn rotate(&mut self, now: crate::Tstamp) -> Result<(), Error> {
        check(unsafe { ffi::nwep_managed_identity_rotate(&mut self.inner, now) })
    }

    /// `update` advances the identity's internal rotation schedule to the given timestamp.
    ///
    /// Call this periodically (e.g. once per second) so that the C library can expire keys
    /// that have passed their grace period and trigger scheduled rotations.
    pub fn update(&mut self, now: crate::Tstamp) {
        unsafe { ffi::nwep_managed_identity_update(&mut self.inner, now) }
    }

    /// `is_revoked` returns `true` if this identity has been revoked.
    ///
    /// A revoked identity should be refused new connections. Revocation is permanent within
    /// the managed identity structure; re-use the underlying node ID is not possible after
    /// revocation.
    pub fn is_revoked(&self) -> bool {
        unsafe { ffi::nwep_managed_identity_is_revoked(&self.inner) != 0 }
    }

    /// `revoke` marks this identity as revoked, signing the revocation with the recovery authority.
    ///
    /// After calling this, [`is_revoked`](ManagedIdentity::is_revoked) returns `true` and the
    /// revocation can be published to peers as a [`Revocation`] struct.
    ///
    /// # Errors
    ///
    /// Returns an [`Error`] if the identity has no recovery authority, or if the C library call
    /// fails.
    pub fn revoke(&mut self, ra: &RecoveryAuthority, now: crate::Tstamp) -> Result<(), Error> {
        check(unsafe { ffi::nwep_managed_identity_revoke(&mut self.inner, ra.as_ffi(), now) })
    }

    /// `node_id` returns the stable [`NodeId`] for this managed identity.
    ///
    /// The node ID is derived from the initial keypair and does not change across rotations.
    pub fn node_id(&self) -> NodeId {
        NodeId(self.inner.nodeid.data)
    }

    /// `active_keypair` returns a copy of the currently active [`Keypair`], if any.
    ///
    /// Returns `None` if the identity has been revoked or has no active key. The returned
    /// keypair is a copy of the C-managed data; it will not be invalidated by subsequent
    /// rotations.
    pub fn active_keypair(&self) -> Option<Keypair> {
        let ptr = unsafe { ffi::nwep_managed_identity_get_active(&self.inner) };
        if ptr.is_null() {
            None
        } else {
            // Copy the keypair data out of the C-managed storage (matching Go's memcpy approach).
            Some(Keypair { inner: unsafe { Box::new(*ptr) } })
        }
    }

    /// `active_keys` returns copies of all keypairs that are currently within their validity window.
    ///
    /// During a rotation grace period both the old and new keypairs are active. Use this when
    /// you need to validate signatures from any currently trusted key, not just the newest one.
    pub fn active_keys(&self) -> Vec<Keypair> {
        let mut ptrs = [std::ptr::null::<ffi::nwep_keypair>(); crate::types::MAX_ACTIVE_KEYS];
        let n = unsafe {
            ffi::nwep_managed_identity_get_active_keys(
                &self.inner,
                ptrs.as_mut_ptr() as *mut *const ffi::nwep_keypair,
                crate::types::MAX_ACTIVE_KEYS,
            )
        };
        (0..n).filter_map(|i| {
            if ptrs[i].is_null() { None } else { Some(Keypair { inner: unsafe { Box::new(*ptrs[i]) } }) }
        }).collect()
    }

    /// `active_pubkey` returns the 32-byte public key of the currently active keypair, if any.
    ///
    /// This is a cheaper alternative to [`active_keypair`](ManagedIdentity::active_keypair) when
    /// only the public key is needed.
    pub fn active_pubkey(&self) -> Option<[u8; 32]> {
        let ptr = unsafe { ffi::nwep_managed_identity_get_active(&self.inner) };
        if ptr.is_null() {
            None
        } else {
            Some(unsafe { (*ptr).pubkey })
        }
    }

    /// `identity` returns an [`Identity`] snapshot containing the current active public key and node ID.
    ///
    /// If there is no active keypair, the public key field in the returned `Identity` is all zeros.
    pub fn identity(&self) -> Identity {
        let pk = self.active_pubkey().unwrap_or([0u8; 32]);
        Identity { pubkey: pk, node_id: self.node_id() }
    }

    /// `clear` zeroes all key material held by this managed identity.
    ///
    /// Called automatically on drop.
    pub fn clear(&mut self) {
        unsafe { ffi::nwep_managed_identity_clear(&mut self.inner) }
    }

    #[allow(dead_code)]
    pub(crate) fn as_ffi(&self) -> &ffi::nwep_managed_identity {
        &self.inner
    }
}

impl Drop for ManagedIdentity {
    fn drop(&mut self) {
        self.clear();
    }
}

/// `verify_revocation` checks the cryptographic signature on a [`Revocation`] record.
///
/// Returns `Ok(())` if the signature is valid, meaning the revocation was produced by the
/// recovery authority that originally registered with the node's managed identity.
///
/// # Errors
///
/// Returns an [`Error`] if the signature is invalid or the C library call fails.
pub fn verify_revocation(rev: &Revocation) -> Result<(), Error> {
    let ffi_rev = rev.to_ffi();
    check(unsafe { ffi::nwep_managed_identity_verify_revocation(&ffi_rev) })
}

/// `Revocation` is the serialised form of a key revocation record for an NWEP node.
///
/// A `Revocation` is produced by [`ManagedIdentity::revoke`] and can be distributed to peers
/// so they stop trusting the revoked node ID. Recipients call [`verify_revocation`] to confirm
/// the record is authentic before acting on it.
#[derive(Clone, Debug)]
pub struct Revocation {
    /// The node ID being revoked.
    pub node_id: NodeId,
    /// The timestamp at which the revocation was issued.
    pub timestamp: crate::Tstamp,
    /// The public key of the recovery authority that signed this revocation.
    pub recovery_pubkey: [u8; 32],
    /// Ed25519 signature over the revocation payload, produced by the recovery authority.
    pub signature: [u8; 64],
}

impl Revocation {
    fn to_ffi(&self) -> ffi::nwep_revocation {
        ffi::nwep_revocation {
            nodeid: ffi::nwep_nodeid { data: self.node_id.0 },
            timestamp: self.timestamp,
            recovery_pubkey: self.recovery_pubkey,
            signature: self.signature,
        }
    }
}