wolfhsm 0.1.1

Rust bindings to wolfHSM
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
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
use wolfhsm_sys::{
    wh_Client_NvmAddObject, wh_Client_NvmDestroyObjects, wh_Client_NvmGetAvailable,
    wh_Client_NvmGetMetadata, wh_Client_NvmList, wh_Client_NvmRead,
};

use crate::client::Client;
use crate::error::Error;

// WH_ERROR_NOTFOUND — signals end-of-list in NvmList, not a real error.
const WH_ERROR_NOTFOUND: i32 = -2104;

/// Maximum NVM label length in bytes (wolfHSM `whNvmMetadata.label` field).
const NVM_LABEL_LEN: usize = 24;

/// Truncate `label` to [`NVM_LABEL_LEN`] bytes and copy into a fixed-size
/// mutable buffer.  Returns the buffer and the number of bytes copied.
///
/// Used by `nvm_add`, `nvm_overwrite`, and `cert_add_trusted`, which all
/// share the same label-truncation requirement.
pub(crate) fn truncate_label(label: &[u8]) -> ([u8; NVM_LABEL_LEN], usize) {
    let len = label.len().min(NVM_LABEL_LEN);
    let mut buf = [0u8; NVM_LABEL_LEN];
    buf[..len].copy_from_slice(&label[..len]);
    (buf, len)
}

/// Available and reclaimable NVM space reported by the wolfHSM server.
#[derive(Debug, Clone, Copy)]
pub struct NvmAvailability {
    /// Bytes available for new objects.
    pub avail_size: u32,
    /// Number of object slots available.
    pub avail_objects: u16,
    /// Bytes that can be recovered by compaction.
    pub reclaim_size: u32,
    /// Object slots that can be recovered by compaction.
    pub reclaim_objects: u16,
}

/// A wolfHSM NVM object identifier (wraps `whNvmId` = `u16`).
///
/// Used to identify counters and other NVM objects on the wolfHSM server.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct NvmId(pub(crate) u16);

impl NvmId {
    /// The invalid/unset NVM ID (`WH_NVM_ID_INVALID` = 0).
    pub const INVALID: Self = NvmId(0);

    /// Wrap a raw `whNvmId` value.
    ///
    /// Prefer the [`From<u16>`] impl in non-`const` contexts.
    pub const fn new(id: u16) -> Self {
        Self(id)
    }
}

impl From<u16> for NvmId {
    fn from(v: u16) -> Self {
        NvmId(v)
    }
}

impl From<NvmId> for u16 {
    fn from(n: NvmId) -> Self {
        n.0
    }
}

/// Metadata about an NVM object.
#[derive(Debug, Clone)]
pub struct NvmMetadata {
    pub id: NvmId,
    /// Access control flags. Corresponds to `WH_NVM_ACCESS_*` constants in
    /// `wolfhsm/wh_nvm.h`. Pass `0` for unrestricted access.
    pub access: u16,
    /// Object attribute flags. Corresponds to `WH_NVM_FLAGS_*` constants in
    /// `wolfhsm/wh_nvm.h`. Pass `0` for default attributes.
    pub flags: u16,
    /// Data length in bytes as reported by the wolfHSM server.
    pub len: u16,
    /// Raw label bytes (NUL-padded to 24 bytes). Use [`label_str`][NvmMetadata::label_str] for a `&str` view.
    pub label: [u8; 24],
}

impl NvmMetadata {
    /// Return the label as a UTF-8 string slice, trimming trailing null bytes.
    ///
    /// Returns `None` if the label bytes are not valid UTF-8.
    pub fn label_str(&self) -> Option<&str> {
        let trimmed = match self.label.iter().position(|&b| b == 0) {
            Some(n) => &self.label[..n],
            None => &self.label[..],
        };
        core::str::from_utf8(trimmed).ok()
    }
}

impl Client {
    /// Query available and reclaimable NVM space on the server.
    pub fn nvm_available(&mut self) -> Result<NvmAvailability, Error> {
        let mut out_rc: i32 = 0;
        let mut avail_size: u32 = 0;
        let mut avail_objects: u16 = 0;
        let mut reclaim_size: u32 = 0;
        let mut reclaim_objects: u16 = 0;

        // SAFETY: all output pointers are valid stack allocations; ctx_ptr is valid.
        let rc = unsafe {
            wh_Client_NvmGetAvailable(
                self.ctx_ptr(),
                &mut out_rc,
                &mut avail_size,
                &mut avail_objects,
                &mut reclaim_size,
                &mut reclaim_objects,
            )
        };
        Error::check(rc, "wh_Client_NvmGetAvailable")?;
        Error::check(out_rc, "wh_Client_NvmGetAvailable(server)")?;

        Ok(NvmAvailability {
            avail_size,
            avail_objects,
            reclaim_size,
            reclaim_objects,
        })
    }

    /// List all NVM object IDs stored on the server.
    ///
    /// Calls `wh_Client_NvmList` in a loop.  The `start_id` parameter is the
    /// **cursor**: pass 0 (WH_NVM_ID_INVALID) to start from the beginning, or
    /// pass the last-seen ID to resume.  The server locates that exact ID in
    /// its directory and returns the *next* object after it, together with the
    /// count of remaining objects including the one just returned.  End of
    /// list is signalled by `out_id == 0` (WH_NVM_ID_INVALID) with
    /// `out_rc == WH_ERROR_OK`, or by `out_rc == WH_ERROR_NOTFOUND` on some
    /// backend variants.
    ///
    /// Confirmed against wolfHSM C source (`wh_NvmFlash_List`,
    /// `wh_NvmFlashLog_List`, and the upstream test suite in
    /// `wh_test_clientserver.c`): the cursor must be the last-seen ID, not
    /// last-seen + 1.  Passing last-seen + 1 causes a spurious "not found"
    /// result for non-contiguous ID spaces because the server does an
    /// exact-match lookup, not a lower-bound search.
    pub fn nvm_list(&mut self) -> Result<Vec<NvmId>, Error> {
        let mut ids = Vec::new();
        // 0 == WH_NVM_ID_INVALID: tells the server to start from the first object.
        let mut start_id: u16 = 0;

        loop {
            let mut out_rc: i32 = 0;
            let mut out_count: u16 = 0;
            let mut out_id: u16 = 0;

            // SAFETY: all output pointers are valid stack allocations; ctx_ptr is valid.
            let rc = unsafe {
                wh_Client_NvmList(
                    self.ctx_ptr(),
                    0, // access: any
                    0, // flags: any
                    start_id,
                    &mut out_rc,
                    &mut out_count,
                    &mut out_id,
                )
            };
            Error::check(rc, "wh_Client_NvmList")?;

            // WH_ERROR_NOTFOUND in out_rc is the end-of-list signal on some
            // backend variants.
            if out_rc == WH_ERROR_NOTFOUND {
                break;
            }
            Error::check(out_rc, "wh_Client_NvmList(server)")?;

            // out_id == 0 (WH_NVM_ID_INVALID) with WH_ERROR_OK is the
            // end-of-list signal used by wh_NvmFlash_List and
            // wh_NvmFlashLog_List.
            if out_id == 0 {
                break;
            }

            // out_count is the number of remaining objects including out_id.
            // Use it as a capacity hint on the first successful call.
            if ids.is_empty() {
                ids.reserve(out_count as usize);
            }

            // Guard against a misbehaving server that returns the same ID as
            // the cursor without advancing (would otherwise loop forever).
            if out_id == start_id && start_id != 0 {
                return Err(Error::ProtocolError {
                    msg: "wh_Client_NvmList: server returned out_id == start_id; cursor stuck",
                });
            }

            ids.push(NvmId(out_id));

            // Pass the last-seen ID back as the cursor.  The server does an
            // exact-match on start_id and returns the next object after it, so
            // the correct advance is start_id = out_id, not out_id + 1.
            start_id = out_id;
        }

        Ok(ids)
    }

    /// Retrieve metadata for the NVM object identified by `id`.
    pub fn nvm_metadata(&mut self, id: NvmId) -> Result<NvmMetadata, Error> {
        let mut out_rc: i32 = 0;
        let mut out_id: u16 = 0;
        let mut out_access: u16 = 0;
        let mut out_flags: u16 = 0;
        let mut out_len: u16 = 0;
        let mut label = [0u8; 24];

        // SAFETY: all output pointers are valid stack allocations; ctx_ptr is valid.
        let rc = unsafe {
            wh_Client_NvmGetMetadata(
                self.ctx_ptr(),
                id.0,
                &mut out_rc,
                &mut out_id,
                &mut out_access,
                &mut out_flags,
                &mut out_len,
                label.len() as u16,
                label.as_mut_ptr(),
            )
        };
        Error::check(rc, "wh_Client_NvmGetMetadata")?;
        Error::check(out_rc, "wh_Client_NvmGetMetadata(server)")?;

        Ok(NvmMetadata {
            id: NvmId(out_id),
            access: out_access,
            flags: out_flags,
            len: out_len,
            label,
        })
    }

    /// Read the contents of the NVM object identified by `id`.
    ///
    /// Fetches the object length via `nvm_metadata` first, then reads the
    /// bytes from `offset` to the end of the object.  Returns `Ok(vec![])`
    /// when `offset >= meta.len` (nothing left to read).
    pub fn nvm_read(&mut self, id: NvmId, offset: u16) -> Result<Vec<u8>, Error> {
        let meta = self.nvm_metadata(id)?;
        // Request only the bytes that remain after `offset`.  Without this,
        // `data_len` would exceed the object length, causing the server to
        // return an error.
        let data_len = meta.len.saturating_sub(offset);
        if data_len == 0 {
            return Ok(vec![]);
        }

        let mut out_rc: i32 = 0;
        let mut out_len: u16 = 0;
        let mut data = vec![0u8; data_len as usize];

        // SAFETY: `data` is a valid heap allocation of `data_len` bytes; ctx_ptr is valid.
        let rc = unsafe {
            wh_Client_NvmRead(
                self.ctx_ptr(),
                id.0,
                offset,
                data_len,
                &mut out_rc,
                &mut out_len,
                data.as_mut_ptr(),
            )
        };
        Error::check(rc, "wh_Client_NvmRead")?;
        Error::check(out_rc, "wh_Client_NvmRead(server)")?;
        if out_len as usize > data.len() {
            return Err(Error::ProtocolError {
                msg: "wh_Client_NvmRead: server reported out_len > requested length",
            });
        }
        data.truncate(out_len as usize);
        Ok(data)
    }

    /// Read exactly `len` bytes from NVM object `id` starting at `offset`,
    /// without issuing a prior metadata round-trip.
    ///
    /// Use this when you already know the object length and want to avoid
    /// the extra [`nvm_metadata`][Self::nvm_metadata] call that
    /// [`nvm_read`][Self::nvm_read] issues unconditionally.  The server
    /// returns an error if `offset + len` exceeds the object length.
    pub fn nvm_read_raw(
        &mut self,
        id: NvmId,
        offset: u16,
        len: u16,
    ) -> Result<Vec<u8>, Error> {
        if len == 0 {
            return Ok(vec![]);
        }
        let mut out_rc: i32 = 0;
        let mut out_len: u16 = 0;
        let mut data = vec![0u8; len as usize];
        // SAFETY: `data` is a valid heap allocation of `len` bytes; ctx_ptr is valid.
        let rc = unsafe {
            wh_Client_NvmRead(
                self.ctx_ptr(),
                id.0,
                offset,
                len,
                &mut out_rc,
                &mut out_len,
                data.as_mut_ptr(),
            )
        };
        Error::check(rc, "wh_Client_NvmRead")?;
        Error::check(out_rc, "wh_Client_NvmRead(server)")?;
        if out_len as usize > data.len() {
            return Err(Error::ProtocolError {
                msg: "wh_Client_NvmRead: server reported out_len > requested length",
            });
        }
        data.truncate(out_len as usize);
        Ok(data)
    }

    /// Create a new NVM object.
    ///
    /// Fails if an object with `id` already exists (the server returns an
    /// error in that case).  Use [`nvm_overwrite`][Self::nvm_overwrite] when
    /// you need to replace an existing object.
    ///
    /// `id` must not be [`NvmId::INVALID`].  `label` is truncated to 24
    /// bytes.  `data` must fit in a `u16` (≤ 65535 bytes).
    ///
    /// `access` — access control flags; see `WH_NVM_ACCESS_*` constants in
    /// `wolfhsm/wh_nvm.h`. Pass `0` for unrestricted access.
    ///
    /// `flags` — object attribute flags; see `WH_NVM_FLAGS_*` constants in
    /// `wolfhsm/wh_nvm.h`. Pass `0` for default attributes.
    pub fn nvm_add(
        &mut self,
        id: NvmId,
        access: u16,
        flags: u16,
        label: impl AsRef<[u8]>,
        data: &[u8],
    ) -> Result<(), Error> {
        let label = label.as_ref();
        if id == NvmId::INVALID {
            return Err(Error::BadArgs {
                msg: "id must not be NvmId::INVALID (0)",
            });
        }
        let data_len = u16::try_from(data.len()).map_err(|_| Error::BadArgs {
            msg: "nvm_add data exceeds u16::MAX bytes",
        })?;
        let (mut label_buf, label_len) = truncate_label(label);
        let mut out_rc: i32 = 0;
        // SAFETY: all pointers are valid for the duration of this call; ctx_ptr is valid.
        let rc = unsafe {
            wh_Client_NvmAddObject(
                self.ctx_ptr(),
                id.0,
                access,
                flags,
                label_len as u16,
                label_buf.as_mut_ptr(),
                data_len,
                data.as_ptr(),
                &mut out_rc,
            )
        };
        Error::check(rc, "wh_Client_NvmAddObject")?;
        Error::check(out_rc, "wh_Client_NvmAddObject(server)")?;
        Ok(())
    }

    /// Overwrite an NVM object with new data. **Not atomic — see warning below.**
    ///
    /// **Warning — data loss hazard**: the existing object is deleted first,
    /// then the new object is added.  If the add fails after a successful
    /// delete, the original object is **permanently lost** and
    /// [`Error::DataLost`] is returned carrying the affected ID.  The
    /// NVM protocol has no rollback facility.
    ///
    /// The `id` must not be [`NvmId::INVALID`] — the server's auto-assign
    /// path is not supported because it does not return the assigned ID.
    /// Choose an explicit non-zero ID.
    ///
    /// `label` is truncated to 24 bytes if longer.  `data` must fit in a
    /// `u16` (≤ 65535 bytes).
    ///
    /// `access` — access control flags; see `WH_NVM_ACCESS_*` constants in
    /// `wolfhsm/wh_nvm.h`. Pass `0` for unrestricted access.
    ///
    /// `flags` — object attribute flags; see `WH_NVM_FLAGS_*` constants in
    /// `wolfhsm/wh_nvm.h`. Pass `0` for default attributes.
    pub fn nvm_overwrite(
        &mut self,
        id: NvmId,
        access: u16,
        flags: u16,
        label: impl AsRef<[u8]>,
        data: &[u8],
    ) -> Result<(), Error> {
        let label = label.as_ref();
        if id == NvmId::INVALID {
            return Err(Error::BadArgs {
                msg: "id must not be NvmId::INVALID (0); wolfHSM auto-assign does not return the assigned ID",
            });
        }
        let data_len = u16::try_from(data.len()).map_err(|_| Error::BadArgs {
            msg: "nvm_overwrite data exceeds u16::MAX bytes",
        })?;

        // Treat NOTFOUND as success: the object may not yet exist (initial write).
        match self.nvm_delete(id) {
            Ok(()) => {}
            Err(Error::Wh { code }) if code == WH_ERROR_NOTFOUND => {}
            Err(e) => return Err(e),
        }

        let (mut label_buf, label_len) = truncate_label(label);

        let mut out_rc: i32 = 0;

        // SAFETY: all pointers are valid for the duration of this call; ctx_ptr is valid.
        let rc = unsafe {
            wh_Client_NvmAddObject(
                self.ctx_ptr(),
                id.0,
                access,
                flags,
                label_len as u16,
                label_buf.as_mut_ptr(),
                data_len,
                data.as_ptr(),
                &mut out_rc,
            )
        };

        // If the add fails the prior delete has already run; report data loss
        // so the caller knows the old object is gone and cannot be recovered.
        let map_add_err = |_: Error| Error::DataLost { id: id.0 };
        Error::check(rc, "wh_Client_NvmAddObject").map_err(map_add_err)?;
        Error::check(out_rc, "wh_Client_NvmAddObject(server)").map_err(map_add_err)?;

        Ok(())
    }

    /// Delete the NVM object identified by `id`.
    pub fn nvm_delete(&mut self, id: NvmId) -> Result<(), Error> {
        let id_list = [id.0];
        let mut out_rc: i32 = 0;

        // SAFETY: id_list is a valid single-element array on the stack; ctx_ptr is valid.
        let rc = unsafe {
            wh_Client_NvmDestroyObjects(self.ctx_ptr(), 1, id_list.as_ptr(), &mut out_rc)
        };
        Error::check(rc, "wh_Client_NvmDestroyObjects")?;
        Error::check(out_rc, "wh_Client_NvmDestroyObjects(server)")?;

        Ok(())
    }
}