Skip to main content

uselesskey_pkcs11_mock/
lib.rs

1#![forbid(unsafe_code)]
2
3//! Deterministic PKCS#11-like mock fixtures.
4//!
5//! This crate provides a tiny test fixture layer for hardware-adjacent tests.
6//! It does **not** emulate a full PKCS#11 daemon.
7
8use std::collections::HashMap;
9use std::fmt;
10use std::sync::{Arc, Mutex};
11
12use sha2::{Digest, Sha256};
13use uselesskey_core::Factory;
14
15/// Stable cache domain for PKCS#11 mock artifacts.
16pub const DOMAIN_PKCS11_MOCK: &str = "uselesskey:pkcs11:mock:v1";
17
18/// Metadata describing a mock slot and token.
19#[derive(Clone, Debug, PartialEq, Eq)]
20pub struct SlotTokenInfo {
21    pub slot_id: u64,
22    pub token_label: String,
23    pub manufacturer_id: String,
24    pub model: String,
25    pub serial_number: String,
26}
27
28/// Identifier to reference a key in the mock provider.
29#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
30pub struct KeyHandle(pub u64);
31
32#[derive(Clone)]
33pub struct MockPkcs11Provider {
34    inner: Arc<Inner>,
35}
36
37impl fmt::Debug for MockPkcs11Provider {
38    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
39        f.debug_struct("MockPkcs11Provider")
40            .field("slot", &self.inner.slot)
41            .field("key_count", &self.inner.keys.len())
42            .finish()
43    }
44}
45
46struct Inner {
47    slot: SlotTokenInfo,
48    certificates: HashMap<KeyHandle, Vec<u8>>,
49    keys: HashMap<KeyHandle, KeyRecord>,
50    next_sign_count: Mutex<u64>,
51}
52
53struct KeyRecord {
54    label: String,
55    algorithm: String,
56    secret: [u8; 32],
57}
58
59/// Deterministic builder spec for PKCS#11-like fixtures.
60#[derive(Clone, Debug, PartialEq, Eq)]
61pub struct Pkcs11MockSpec {
62    pub token_label: String,
63    pub manufacturer_id: String,
64    pub model: String,
65    pub key_labels: Vec<String>,
66}
67
68impl Pkcs11MockSpec {
69    pub fn basic(token_label: impl Into<String>) -> Self {
70        Self {
71            token_label: token_label.into(),
72            manufacturer_id: "uselesskey".to_string(),
73            model: "UK-PKCS11-MOCK".to_string(),
74            key_labels: vec!["signing-key".to_string()],
75        }
76    }
77
78    pub fn stable_bytes(&self) -> Vec<u8> {
79        let mut out = Vec::new();
80        write_field(&mut out, "token_label", self.token_label.as_bytes());
81        write_field(&mut out, "manufacturer_id", self.manufacturer_id.as_bytes());
82        write_field(&mut out, "model", self.model.as_bytes());
83        for label in self.effective_key_labels() {
84            write_field(&mut out, "key_label", label.as_bytes());
85        }
86        out
87    }
88
89    fn effective_key_labels(&self) -> impl Iterator<Item = &str> {
90        self.key_labels
91            .iter()
92            .map(String::as_str)
93            .chain((self.key_labels.is_empty()).then_some("signing-key"))
94    }
95}
96
97/// Extension trait to build PKCS#11-like mock providers from a core [`Factory`].
98pub trait Pkcs11MockFactoryExt {
99    fn pkcs11_mock(&self, label: impl AsRef<str>, spec: Pkcs11MockSpec) -> MockPkcs11Provider;
100}
101
102impl Pkcs11MockFactoryExt for Factory {
103    fn pkcs11_mock(&self, label: impl AsRef<str>, spec: Pkcs11MockSpec) -> MockPkcs11Provider {
104        let spec_bytes = spec.stable_bytes();
105        self.get_or_init(
106            DOMAIN_PKCS11_MOCK,
107            label.as_ref(),
108            &spec_bytes,
109            "good",
110            move |seed| build_provider(spec, *seed.bytes()),
111        )
112        .as_ref()
113        .clone()
114    }
115}
116
117impl MockPkcs11Provider {
118    pub fn slot_info(&self) -> SlotTokenInfo {
119        self.inner.slot.clone()
120    }
121
122    pub fn key_handles(&self) -> Vec<KeyHandle> {
123        let mut handles: Vec<KeyHandle> = self.inner.keys.keys().copied().collect();
124        handles.sort_by_key(|h| h.0);
125        handles
126    }
127
128    pub fn sign(&self, handle: KeyHandle, message: &[u8]) -> Option<Vec<u8>> {
129        let key = self.inner.keys.get(&handle)?;
130        let mut hasher = Sha256::new();
131        hasher.update(key.secret);
132        hasher.update(key.algorithm.as_bytes());
133        hasher.update(message);
134        Some(hasher.finalize().to_vec())
135    }
136
137    pub fn verify(&self, handle: KeyHandle, message: &[u8], signature: &[u8]) -> bool {
138        self.sign(handle, message)
139            .is_some_and(|expected| expected == signature)
140    }
141
142    pub fn certificate_der(&self, handle: KeyHandle) -> Option<&[u8]> {
143        self.inner.certificates.get(&handle).map(Vec::as_slice)
144    }
145
146    pub fn key_label(&self, handle: KeyHandle) -> Option<&str> {
147        self.inner.keys.get(&handle).map(|key| key.label.as_str())
148    }
149
150    pub fn next_sign_count(&self) -> u64 {
151        let mut guard = self.inner.next_sign_count.lock().expect("sign_count mutex");
152        *guard += 1;
153        *guard
154    }
155}
156
157fn build_provider(spec: Pkcs11MockSpec, seed: [u8; 32]) -> MockPkcs11Provider {
158    let slot_id = u64::from_le_bytes(seed[0..8].try_into().expect("seed slice for slot id"));
159    let serial_hex = hex8(&seed[8..16]);
160    let mut keys = HashMap::new();
161    let mut certs = HashMap::new();
162
163    let key_labels: Vec<&str> = spec.effective_key_labels().collect();
164    for (idx, key_label) in key_labels.iter().enumerate() {
165        let mut key_hasher = Sha256::new();
166        key_hasher.update(seed);
167        key_hasher.update((idx as u32).to_le_bytes());
168        key_hasher.update(key_label.as_bytes());
169        let key_seed = key_hasher.finalize();
170
171        let mut secret = [0u8; 32];
172        secret.copy_from_slice(&key_seed[..32]);
173
174        let handle = KeyHandle((idx as u64) + 1);
175        let cert = mock_certificate_der(
176            &spec.token_label,
177            key_label,
178            &spec.manufacturer_id,
179            &spec.model,
180            &secret,
181        );
182        certs.insert(handle, cert);
183        keys.insert(
184            handle,
185            KeyRecord {
186                label: (*key_label).to_string(),
187                algorithm: "MOCK-SHA256".to_string(),
188                secret,
189            },
190        );
191    }
192
193    MockPkcs11Provider {
194        inner: Arc::new(Inner {
195            slot: SlotTokenInfo {
196                slot_id,
197                token_label: spec.token_label,
198                manufacturer_id: spec.manufacturer_id,
199                model: spec.model,
200                serial_number: serial_hex,
201            },
202            certificates: certs,
203            keys,
204            next_sign_count: Mutex::new(0),
205        }),
206    }
207}
208
209fn mock_certificate_der(
210    token_label: &str,
211    key_label: &str,
212    manufacturer_id: &str,
213    model: &str,
214    secret: &[u8; 32],
215) -> Vec<u8> {
216    let mut out = Vec::with_capacity(128);
217    out.extend_from_slice(&[0x30, 0x82]); // looks DER-like for parser tests
218    out.extend_from_slice(&[0x00, 0x00]);
219    write_field(&mut out, "token", token_label.as_bytes());
220    write_field(&mut out, "key", key_label.as_bytes());
221    write_field(&mut out, "mfr", manufacturer_id.as_bytes());
222    write_field(&mut out, "model", model.as_bytes());
223    write_field(&mut out, "fingerprint", &Sha256::digest(secret));
224    let body_len = u16_len("certificate_der_body", out.len() - 4);
225    out[2..4].copy_from_slice(&body_len.to_be_bytes());
226    out
227}
228
229fn write_field(out: &mut Vec<u8>, name: &str, value: &[u8]) {
230    out.extend_from_slice(name.as_bytes());
231    out.push(b'=');
232    out.extend_from_slice(&u16_len(name, value.len()).to_be_bytes());
233    out.extend_from_slice(value);
234    out.push(0);
235}
236
237fn u16_len(field_name: &str, len: usize) -> u16 {
238    u16::try_from(len).unwrap_or_else(|_| {
239        panic!(
240            "{field_name} length {len} exceeds u16::MAX; fixture encoding requires <= {} bytes",
241            u16::MAX
242        )
243    })
244}
245
246fn hex8(bytes: &[u8]) -> String {
247    bytes.iter().map(|b| format!("{b:02X}")).collect::<String>()
248}
249
250#[cfg(test)]
251mod tests {
252    use std::collections::{HashMap, HashSet};
253
254    use uselesskey_core::Seed;
255    use uselesskey_test_support::{TestResult, require_ok, require_some};
256
257    use super::*;
258
259    #[test]
260    fn deterministic_provider_stable() {
261        let fx = Factory::deterministic(Seed::from_env_value("pkcs11-seed").unwrap());
262        let spec = Pkcs11MockSpec::basic("HSM-A");
263
264        let a = fx.pkcs11_mock("issuer", spec.clone());
265        let b = fx.pkcs11_mock("issuer", spec);
266
267        assert_eq!(a.slot_info(), b.slot_info());
268        assert_eq!(a.key_handles(), b.key_handles());
269    }
270
271    #[test]
272    fn sign_verify_round_trip() {
273        let fx = Factory::random();
274        let provider = fx.pkcs11_mock("rt", Pkcs11MockSpec::basic("HSM-RT"));
275        let handle = provider.key_handles()[0];
276        let msg = b"hello from fixture";
277
278        let sig = provider.sign(handle, msg).expect("signature");
279        assert!(provider.verify(handle, msg, &sig));
280        assert!(!provider.verify(handle, b"other", &sig));
281    }
282
283    #[test]
284    fn sign_count_increments_from_one() {
285        let fx = Factory::random();
286        let provider = fx.pkcs11_mock("count", Pkcs11MockSpec::basic("HSM-COUNT"));
287
288        assert_eq!(provider.next_sign_count(), 1);
289        assert_eq!(provider.next_sign_count(), 2);
290    }
291
292    #[test]
293    fn debug_summary_names_slot_and_key_count() {
294        let fx = Factory::random();
295        let provider = fx.pkcs11_mock("debug", Pkcs11MockSpec::basic("HSM-DEBUG"));
296        let debug = format!("{provider:?}");
297
298        assert!(debug.contains("MockPkcs11Provider"));
299        assert!(debug.contains("slot"));
300        assert!(debug.contains("key_count"));
301    }
302
303    #[test]
304    fn multiple_keys_get_one_based_sequential_handles() {
305        let fx = Factory::deterministic(Seed::from_env_value("pkcs11-handles").unwrap());
306        let mut spec = Pkcs11MockSpec::basic("HSM-HANDLES");
307        spec.key_labels = vec!["signing-key".to_string(), "verification-key".to_string()];
308
309        let provider = fx.pkcs11_mock("handles", spec);
310        let handles = provider.key_handles();
311
312        assert_eq!(handles, vec![KeyHandle(1), KeyHandle(2)]);
313        assert_eq!(provider.key_label(KeyHandle(1)), Some("signing-key"));
314        assert_eq!(provider.key_label(KeyHandle(2)), Some("verification-key"));
315    }
316
317    #[test]
318    fn cert_lookup_returns_der_like_bytes() {
319        let fx = Factory::random();
320        let provider = fx.pkcs11_mock("der", Pkcs11MockSpec::basic("HSM-DER"));
321        let handle = provider.key_handles()[0];
322        let der = provider.certificate_der(handle).expect("certificate");
323        assert_eq!(&der[0..2], &[0x30, 0x82]);
324        let body_len = u16::from_be_bytes(der[2..4].try_into().expect("DER body length"));
325        assert_eq!(usize::from(body_len), der.len() - 4);
326    }
327
328    #[test]
329    fn slot_serial_is_uppercase_hex() {
330        let fx = Factory::deterministic(Seed::from_env_value("pkcs11-serial").unwrap());
331        let provider = fx.pkcs11_mock("serial", Pkcs11MockSpec::basic("HSM-SERIAL"));
332        let serial = provider.slot_info().serial_number;
333
334        assert_eq!(serial.len(), 16);
335        assert!(
336            serial
337                .bytes()
338                .all(|byte| byte.is_ascii_digit() || (b'A'..=b'F').contains(&byte)),
339            "expected uppercase hex serial, got {serial}"
340        );
341    }
342
343    #[test]
344    fn empty_key_labels_falls_back_to_default_key() {
345        let fx = Factory::deterministic(Seed::from_env_value("pkcs11-empty-keys").unwrap());
346        let mut spec = Pkcs11MockSpec::basic("HSM-EMPTY");
347        spec.key_labels.clear();
348
349        let provider = fx.pkcs11_mock("empty", spec);
350        let handles = provider.key_handles();
351        assert_eq!(handles.len(), 1);
352        assert_eq!(provider.key_label(handles[0]), Some("signing-key"));
353    }
354
355    #[test]
356    fn empty_key_labels_and_explicit_default_share_stable_identity() {
357        let mut empty = Pkcs11MockSpec::basic("HSM-EMPTY");
358        empty.key_labels.clear();
359        let explicit = Pkcs11MockSpec::basic("HSM-EMPTY");
360
361        assert_eq!(empty.stable_bytes(), explicit.stable_bytes());
362    }
363
364    #[test]
365    fn key_labels_participate_in_stable_identity() {
366        let explicit = Pkcs11MockSpec::basic("HSM-IDENTITY");
367        let mut alternate = Pkcs11MockSpec::basic("HSM-IDENTITY");
368        alternate.key_labels = vec!["verification-key".to_string()];
369
370        let stable = explicit.stable_bytes();
371        assert_contains_bytes(&stable, b"key_label");
372        assert_contains_bytes(&stable, b"signing-key");
373        assert_ne!(stable, alternate.stable_bytes());
374    }
375
376    fn assert_contains_bytes(haystack: &[u8], needle: &[u8]) {
377        assert!(
378            haystack
379                .windows(needle.len())
380                .any(|window| window == needle),
381            "expected stable identity to contain {}",
382            String::from_utf8_lossy(needle)
383        );
384    }
385
386    #[test]
387    #[should_panic(expected = "token_label length")]
388    fn rejects_oversized_field_length() {
389        let fx = Factory::random();
390        let spec = Pkcs11MockSpec::basic("a".repeat((u16::MAX as usize) + 1));
391        let _ = fx.pkcs11_mock("oversized-field", spec);
392    }
393
394    #[test]
395    #[should_panic(expected = "certificate_der_body length")]
396    fn rejects_oversized_der_body() {
397        let fx = Factory::random();
398        let mut spec = Pkcs11MockSpec::basic("token");
399        let long = "z".repeat(22_000);
400        spec.manufacturer_id = long.clone();
401        spec.model = long;
402        spec.key_labels = vec!["k".repeat(22_000)];
403        let _ = fx.pkcs11_mock("oversized-der", spec);
404    }
405
406    #[test]
407    fn domain_constant_is_stable_for_the_lifetime_of_v1() {
408        // Changing this constant changes derived outputs. The test exists so
409        // an accidental rename is caught by CI rather than by silent fixture
410        // drift in downstream test suites.
411        assert_eq!(DOMAIN_PKCS11_MOCK, "uselesskey:pkcs11:mock:v1");
412    }
413
414    #[test]
415    fn sign_with_unknown_handle_returns_none() {
416        let fx = Factory::random();
417        let provider = fx.pkcs11_mock("err-sign", Pkcs11MockSpec::basic("HSM-ERR-SIGN"));
418
419        // Handle 0 is below the 1-based handle range, and 999 is above the
420        // single-key default — neither maps to a stored KeyRecord.
421        assert!(provider.sign(KeyHandle(0), b"msg").is_none());
422        assert!(provider.sign(KeyHandle(999), b"msg").is_none());
423    }
424
425    #[test]
426    fn verify_with_unknown_handle_returns_false() -> TestResult<()> {
427        let fx = Factory::random();
428        let provider = fx.pkcs11_mock("err-verify", Pkcs11MockSpec::basic("HSM-ERR-VERIFY"));
429        let valid = *require_some(provider.key_handles().first(), "default key handle present")?;
430        let real_sig = require_some(
431            provider.sign(valid, b"msg"),
432            "signing succeeds for valid handle",
433        )?;
434
435        // A real signature must not verify under an unknown handle, and an
436        // empty signature buffer must not verify either.
437        assert!(!provider.verify(KeyHandle(0), b"msg", &real_sig));
438        assert!(!provider.verify(KeyHandle(999), b"msg", &real_sig));
439        assert!(!provider.verify(KeyHandle(999), b"msg", &[]));
440        Ok(())
441    }
442
443    #[test]
444    fn certificate_der_with_unknown_handle_returns_none() {
445        let fx = Factory::random();
446        let provider = fx.pkcs11_mock("err-cert", Pkcs11MockSpec::basic("HSM-ERR-CERT"));
447
448        assert!(provider.certificate_der(KeyHandle(0)).is_none());
449        assert!(provider.certificate_der(KeyHandle(999)).is_none());
450    }
451
452    #[test]
453    fn key_label_with_unknown_handle_returns_none() {
454        let fx = Factory::random();
455        let provider = fx.pkcs11_mock("err-label", Pkcs11MockSpec::basic("HSM-ERR-LABEL"));
456
457        assert!(provider.key_label(KeyHandle(0)).is_none());
458        assert!(provider.key_label(KeyHandle(999)).is_none());
459    }
460
461    #[test]
462    fn key_handle_is_copy_and_usable_in_hash_collections() {
463        let original = KeyHandle(42);
464        let copied = original; // Copy, not move
465        // Exercising the explicit Clone impl is the point of this test, so
466        // suppress the Copy-friendly suggestion.
467        #[allow(clippy::clone_on_copy)]
468        let cloned = original.clone();
469
470        // Copy semantics: original is still usable after the copy/clone.
471        assert_eq!(original, copied);
472        assert_eq!(original, cloned);
473        assert_eq!(original.0, 42);
474
475        // Hash + Eq make KeyHandle a valid HashMap key.
476        let mut map: HashMap<KeyHandle, &'static str> = HashMap::new();
477        map.insert(original, "first");
478        // Inserting via the copy must collide with the original key.
479        let prior = map.insert(copied, "second");
480        assert_eq!(prior, Some("first"));
481        assert_eq!(map.len(), 1);
482        assert_eq!(map.get(&KeyHandle(42)), Some(&"second"));
483
484        // And HashSet de-duplicates equal-by-value handles.
485        let mut set: HashSet<KeyHandle> = HashSet::new();
486        set.insert(original);
487        set.insert(copied);
488        set.insert(KeyHandle(42));
489        assert_eq!(set.len(), 1);
490    }
491
492    #[test]
493    fn slot_token_info_round_trips_clone_debug_and_equality() {
494        let info = SlotTokenInfo {
495            slot_id: 7,
496            token_label: "HSM-RT".to_string(),
497            manufacturer_id: "uselesskey".to_string(),
498            model: "UK-PKCS11-MOCK".to_string(),
499            serial_number: "ABCDEF0123456789".to_string(),
500        };
501
502        let cloned = info.clone();
503        assert_eq!(info, cloned);
504
505        // Debug output names every field — so a future field rename trips this
506        // assertion before downstream parsers silently regress.
507        let debug = format!("{info:?}");
508        for needle in [
509            "SlotTokenInfo",
510            "slot_id",
511            "token_label",
512            "manufacturer_id",
513            "model",
514            "serial_number",
515        ] {
516            assert!(debug.contains(needle), "debug missing {needle}: {debug}");
517        }
518
519        // Mutating the clone must not change the original (true `Clone`, not
520        // shared state) and must compare unequal afterward.
521        let mut diverged = info.clone();
522        diverged.slot_id = info.slot_id + 1;
523        assert_ne!(info, diverged);
524        assert_eq!(info.slot_id, 7);
525    }
526
527    #[test]
528    fn next_sign_count_increments_monotonically() -> TestResult<()> {
529        let fx = Factory::deterministic(require_ok(
530            Seed::from_env_value("pkcs11-monotonic"),
531            "must parse seed",
532        )?);
533        let provider = fx.pkcs11_mock("monotonic", Pkcs11MockSpec::basic("HSM-MONO"));
534
535        let observed: Vec<u64> = (0..5).map(|_| provider.next_sign_count()).collect();
536
537        // Pin the +1 semantic: starts at 1 and grows by exactly 1 per call.
538        assert_eq!(observed, vec![1, 2, 3, 4, 5]);
539        Ok(())
540    }
541}