synta-certificate 0.2.0

X.509 certificate structures for synta ASN.1 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
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
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
//! NSS-backed PKCS#11 HSM signing.
//!
//! Provides [`NssHsmSigner`] and [`priv_load_from_pkcs11_uri_nss`] for
//! private keys that live in a real PKCS#11 token (e.g. a hardware HSM or a
//! software token like kryoptic / SoftHSMv2).
//!
//! Unlike [`super::signing::NssSigner`], this path does **not** import PKCS#8
//! into the NSS internal slot — there is no key material to import.  Instead,
//! it calls `PK11_ListPrivKeysInSlot` (`pk11pub.h`, NSS 3.4+) to locate the
//! key in the named token slot, then signs via `SEC_SignData` / `PK11_Sign`.
//!
//! Note: `PK11_FindPrivateKeyFromNickname` is declared in `pk11priv.h` (a
//! private header) and is **not exported** from `libnss3.so`, so it cannot be
//! used from external code.  `PK11_ListPrivKeysInSlot` is the publicly
//! exported alternative and is used here instead.
//!
//! ## Token requirement
//!
//! The `token=` path attribute is **mandatory** for the NSS backend.
//! `PK11_ListPrivKeysInSlot` operates on a specific slot handle obtained via
//! `PK11_FindSlotByName`, which requires a non-empty token label.  URIs
//! without a `token=` attribute are rejected with an error at key-load time
//! and at signing time.
//!
//! ## Token login
//!
//! If the URI contains a `?pin-value=…` query attribute **and** a non-empty
//! `token=…` path attribute, this module calls `PK11_Authenticate` on the
//! matching slot before searching for the key.  This is optional — tokens
//! without a PIN or with a cached login do not require it.
//!
//! ## Module registration
//!
//! The PKCS#11 module must be registered in the NSS secmod database before
//! calling this code.  This library does **not** load providers automatically.

use std::{ffi::CString, ptr};

use nss_sys::nspr::{PR_FALSE, PR_TRUE};
use nss_sys::{SECItemStr, SECItemType, SECStatus};

use super::ensure_nss_init;
use crate::crypto::{ErasedCertificateSigner, PrivateKeyError};
use crate::oids;

// ── Shared NSS types and FFI ──────────────────────────────────────────────────
// Key types and signing primitives are shared with signing.rs / signature.rs;
// import them from the central ffi module to have a single declaration each.

use super::ffi::{
    PK11_Authenticate, PK11_FindSlotByName, PK11_FreeSlot, PK11_ListPrivKeysInSlot, PK11_Sign,
    PK11_SignatureLen, SECITEM_FreeItem, SECKEYPrivateKeyList, SECKEYPrivateKeyListNode,
    SECKEYPrivateKeyStr, SECKEY_ConvertToPublicKey, SECKEY_CopyPrivateKey,
    SECKEY_DestroyPrivateKey, SECKEY_DestroyPrivateKeyList, SECKEY_DestroyPublicKey,
    SECKEY_EncodeDERSubjectPublicKeyInfo, SECOidTag, SEC_SignData,
    SEC_OID_ANSIX962_ECDSA_SHA256_SIGNATURE, SEC_OID_ANSIX962_ECDSA_SHA384_SIGNATURE,
    SEC_OID_ANSIX962_ECDSA_SHA512_SIGNATURE, SEC_OID_ED25519_SIGNATURE, SEC_OID_ML_DSA_44,
    SEC_OID_ML_DSA_65, SEC_OID_ML_DSA_87, SEC_OID_PKCS1_SHA256_WITH_RSA_ENCRYPTION,
    SEC_OID_PKCS1_SHA384_WITH_RSA_ENCRYPTION, SEC_OID_PKCS1_SHA512_WITH_RSA_ENCRYPTION,
};

/// Extract and copy the first key from `list`, leaving the list intact for
/// subsequent `SECKEY_DestroyPrivateKeyList`.
///
/// Returns `None` if the list is empty.  The returned pointer must be freed
/// with `SECKEY_DestroyPrivateKey`.
///
/// # Safety
/// `list` must be a non-null pointer returned by `PK11_ListPrivKeysInSlot`.
unsafe fn take_first_key(list: *mut SECKEYPrivateKeyList) -> Option<*mut SECKEYPrivateKeyStr> {
    // PRIVKEY_LIST_HEAD(l) == (SECKEYPrivateKeyListNode *)PR_LIST_HEAD(&l->list)
    //                       == l->list.next cast to node*
    let head = (*list).list.next as *const SECKEYPrivateKeyListNode;
    // PRIVKEY_LIST_END(n, l) == n == &l->list  (the sentinel)
    let sentinel = std::ptr::addr_of!((*list).list) as *const _;
    if std::ptr::eq(head as *const _, sentinel) {
        return None; // empty list
    }
    let key = SECKEY_CopyPrivateKey((*head).key);
    if key.is_null() {
        None
    } else {
        Some(key)
    }
}

// ── SPKI-based algorithm ID helper ────────────────────────────────────────────

/// Compute a DER-encoded signing `AlgorithmIdentifier` from a SPKI DER blob
/// and a hash algorithm name.  Parses the key type OID from the SPKI, then
/// maps it together with `hash_algo` to the correct signing OID.
///
/// Lives here rather than in `crypto::utils` because it is specific to the NSS
/// HSM path (PKCS#8 is unavailable; only the public SPKI is known).
fn sig_alg_der_from_spki(spki_der: &[u8], hash_algo: &str) -> Option<Vec<u8>> {
    use synta::{Decoder, Encoding};

    let mut dec = Decoder::new(spki_der, Encoding::Der);
    dec.read_tag().ok()?;
    dec.read_length().ok()?.definite().ok()?;

    let alg_start = dec.position();
    dec.read_tag().ok()?;
    let alg_content_len = dec.read_length().ok()?.definite().ok()?;
    let alg_end = dec.position() + alg_content_len;

    let mut alg_dec = Decoder::new(&spki_der[alg_start..alg_end], Encoding::Der);
    alg_dec.read_tag().ok()?;
    alg_dec.read_length().ok()?.definite().ok()?;
    let key_oid: synta::ObjectIdentifier = alg_dec.decode().ok()?;

    crate::signing_algorithm_der(&key_oid, hash_algo)
}

// ── Signing algorithm dispatch ────────────────────────────────────────────────

/// Map a signing `AlgorithmIdentifier` DER to the NSS `SECOidTag`.
///
/// Returns `None` for unsupported algorithms (e.g. Ed448).
fn sig_alg_der_to_nss_tag(sig_alg_der: &[u8]) -> Option<SECOidTag> {
    use crate::crypto::utils::split_alg_id;
    let (oid, _, _) = split_alg_id(sig_alg_der, |_| ()).ok()?;
    match oid.components() {
        c if c == oids::ECDSA_WITH_SHA256 => Some(SEC_OID_ANSIX962_ECDSA_SHA256_SIGNATURE),
        c if c == oids::ECDSA_WITH_SHA384 => Some(SEC_OID_ANSIX962_ECDSA_SHA384_SIGNATURE),
        c if c == oids::ECDSA_WITH_SHA512 => Some(SEC_OID_ANSIX962_ECDSA_SHA512_SIGNATURE),
        c if c == oids::SHA256_WITH_RSA => Some(SEC_OID_PKCS1_SHA256_WITH_RSA_ENCRYPTION),
        c if c == oids::SHA384_WITH_RSA => Some(SEC_OID_PKCS1_SHA384_WITH_RSA_ENCRYPTION),
        c if c == oids::SHA512_WITH_RSA => Some(SEC_OID_PKCS1_SHA512_WITH_RSA_ENCRYPTION),
        c if c == oids::ED25519 => Some(SEC_OID_ED25519_SIGNATURE),
        c if c == oids::ML_DSA_44 => Some(SEC_OID_ML_DSA_44),
        c if c == oids::ML_DSA_65 => Some(SEC_OID_ML_DSA_65),
        c if c == oids::ML_DSA_87 => Some(SEC_OID_ML_DSA_87),
        _ => None,
    }
}

// ── Core HSM sign function ────────────────────────────────────────────────────

/// Find the private key by `object_label` in the named token slot, optionally
/// authenticate, sign `tbs_der`, and release the key reference immediately.
///
/// `token_label` is required: `PK11_ListPrivKeysInSlot` (the exported NSS API)
/// operates on a specific slot, so the token must be identifiable by name.
fn do_hsm_sign(
    object_label: &str,
    pin: Option<&str>,
    token_label: Option<&str>,
    tbs_der: &[u8],
    nss_alg_tag: SECOidTag,
) -> Result<Vec<u8>, super::signing::NssSignerError> {
    use super::signing::NssSignerError;

    if !ensure_nss_init() {
        return Err(NssSignerError("NSS initialisation failed".to_string()));
    }

    // `PK11_ListPrivKeysInSlot` requires a slot handle; a token= attribute is
    // therefore mandatory for the NSS backend.
    let label = token_label.filter(|l| !l.is_empty()).ok_or_else(|| {
        NssSignerError(
            "NSS HSM signing requires a non-empty 'token=' attribute in the PKCS#11 URI"
                .to_string(),
        )
    })?;
    let label_c = CString::new(label)
        .map_err(|e| NssSignerError(format!("token label contains NUL: {}", e)))?;

    // SAFETY: label_c is a valid NUL-terminated C string.
    let slot = unsafe { PK11_FindSlotByName(label_c.as_ptr()) };
    if slot.is_null() {
        return Err(NssSignerError(format!(
            "token '{}' not found in NSS",
            label
        )));
    }

    // Authenticate if a PIN is provided.
    if let Some(pin) = pin {
        let pin_c =
            CString::new(pin).map_err(|e| NssSignerError(format!("PIN contains NUL: {}", e)))?;
        // SAFETY: slot is non-null; pin_c is a valid C string.
        unsafe { PK11_Authenticate(slot, PR_TRUE, pin_c.as_ptr()) };
    }

    // List private keys in this slot filtered by nickname (object label).
    // SAFETY: slot is non-null; nickname_c is a valid mutable C string.
    let nickname_c = CString::new(object_label)
        .map_err(|e| NssSignerError(format!("object label contains NUL: {}", e)))?;
    let key_list =
        unsafe { PK11_ListPrivKeysInSlot(slot, nickname_c.as_ptr() as *mut _, ptr::null()) };
    // SAFETY: slot is non-null; PK11_FreeSlot is declared in super (mod.rs).
    unsafe { PK11_FreeSlot(slot) };

    if key_list.is_null() {
        return Err(NssSignerError(format!(
            "PK11_ListPrivKeysInSlot returned NULL for key '{}' in token '{}'",
            object_label, label
        )));
    }

    // SAFETY: key_list is non-null.
    let priv_key = unsafe { take_first_key(key_list) };
    // SAFETY: key_list is non-null.
    unsafe { SECKEY_DestroyPrivateKeyList(key_list) };

    let priv_key = priv_key.ok_or_else(|| {
        NssSignerError(format!(
            "key '{}' not found in token '{}'; ensure the PKCS#11 module is registered",
            object_label, label
        ))
    })?;

    // Sign with the appropriate NSS primitive.
    let sig = if nss_alg_tag == SEC_OID_ED25519_SIGNATURE {
        // Ed25519: PK11_Sign passes the raw TBS to CKM_EDDSA (hash-and-sign).
        let sig_len = unsafe { PK11_SignatureLen(priv_key) };
        if sig_len <= 0 {
            unsafe { SECKEY_DestroyPrivateKey(priv_key) };
            return Err(NssSignerError(
                "PK11_SignatureLen returned non-positive value".to_string(),
            ));
        }
        let mut sig_buf = vec![0u8; sig_len as usize];
        let msg_item = SECItemStr {
            type_: SECItemType::siBuffer,
            data: tbs_der.as_ptr() as *mut _,
            len: tbs_der.len() as u32,
        };
        let mut sig_item = SECItemStr {
            type_: SECItemType::siBuffer,
            data: sig_buf.as_mut_ptr(),
            len: sig_len as u32,
        };
        // SAFETY: priv_key, msg_item, and sig_item reference valid memory.
        let status = unsafe { PK11_Sign(priv_key, &mut sig_item, &msg_item) };
        // SAFETY: priv_key is non-null.
        unsafe { SECKEY_DestroyPrivateKey(priv_key) };
        if status != SECStatus::SECSuccess {
            return Err(NssSignerError("PK11_Sign (Ed25519 HSM) failed".to_string()));
        }
        sig_buf.truncate(sig_item.len as usize);
        sig_buf
    } else {
        // Hash-then-sign: RSA PKCS#1 v1.5, ECDSA, ML-DSA.
        let mut sig_item = SECItemStr {
            type_: SECItemType::siBuffer,
            data: ptr::null_mut(),
            len: 0,
        };
        // SAFETY: tbs_der is valid for the call duration; priv_key is non-null.
        let status = unsafe {
            SEC_SignData(
                &mut sig_item,
                tbs_der.as_ptr(),
                tbs_der.len() as std::ffi::c_int,
                priv_key,
                nss_alg_tag,
            )
        };
        // SAFETY: priv_key is non-null.
        unsafe { SECKEY_DestroyPrivateKey(priv_key) };
        if status != SECStatus::SECSuccess {
            return Err(NssSignerError("SEC_SignData (HSM) failed".to_string()));
        }
        // SAFETY: sig_item.data is NSS-allocated and valid after a successful call.
        let sig =
            unsafe { std::slice::from_raw_parts(sig_item.data, sig_item.len as usize).to_vec() };
        // SAFETY: PR_FALSE frees only .data, not the stack-allocated SECItemStr.
        unsafe { SECITEM_FreeItem(&mut sig_item, PR_FALSE) };
        sig
    };

    Ok(sig)
}

// ── NssHsmSigner ─────────────────────────────────────────────────────────────

/// NSS-backed certificate signer for keys stored in a real PKCS#11 token.
///
/// Holds the parsed URI attributes and the pre-computed `AlgorithmIdentifier`
/// DER.  On each `sign_tbs_erased` call it finds the key in the token by its
/// object label, signs via the appropriate NSS primitive, and destroys the key
/// handle immediately.
pub(crate) struct NssHsmSigner {
    /// Object label passed as the `nickname` filter to `PK11_ListPrivKeysInSlot`.
    object_label: String,
    /// Optional PIN passed to `PK11_Authenticate` before signing.
    pin: Option<String>,
    /// Token label used with `PK11_FindSlotByName` to obtain the slot handle
    /// required by `PK11_ListPrivKeysInSlot`.  Also used with `PK11_Authenticate`
    /// when a PIN is present.  `None` is rejected at signing time — a non-empty
    /// `token=` attribute is mandatory for the NSS backend.
    token_label: Option<String>,
    /// Pre-computed DER-encoded `AlgorithmIdentifier`.
    sig_alg_der: Vec<u8>,
    /// NSS `SECOidTag` for the signing algorithm.
    nss_alg_tag: SECOidTag,
}

/// Build a boxed [`NssHsmSigner`] for the PKCS#11 key described by `pkcs11`.
///
/// The `pkcs11` argument carries the pre-parsed URI so no re-parsing is
/// required at signer construction time.  `spki_der` is used to determine the
/// key type (and thus the signing algorithm OID) without accessing the token.
///
/// Returns [`crate::nss_backend::NssUnsupportedSigner`] if:
/// - the URI has no `object=` attribute,
/// - `spki_der` does not map to a supported algorithm, or
/// - the algorithm combination is not supported by NSS.
///
/// Note: the `token=` attribute is **not** validated here; a missing or empty
/// `token=` will produce a [`PrivateKeyError`] only when `sign_tbs_erased` is
/// first called (inside `do_hsm_sign`).
pub(crate) fn nss_hsm_signer(
    pkcs11: &crate::pkcs11_uri::Pkcs11Uri,
    algorithm: &str,
    spki_der: &[u8],
) -> Box<dyn ErasedCertificateSigner> {
    let Some(object_label) = pkcs11.attrs.object.as_deref() else {
        return Box::new(crate::nss_backend::NssUnsupportedSigner);
    };
    let Some(sig_alg_der) = sig_alg_der_from_spki(spki_der, algorithm) else {
        return Box::new(crate::nss_backend::NssUnsupportedSigner);
    };
    let Some(nss_alg_tag) = sig_alg_der_to_nss_tag(&sig_alg_der) else {
        return Box::new(crate::nss_backend::NssUnsupportedSigner);
    };

    Box::new(NssHsmSigner {
        object_label: object_label.to_string(),
        pin: pkcs11.attrs.pin_value.clone(),
        token_label: pkcs11.attrs.token.clone(),
        sig_alg_der,
        nss_alg_tag,
    })
}

impl ErasedCertificateSigner for NssHsmSigner {
    fn signature_algorithm_der_erased(&self) -> Result<Vec<u8>, PrivateKeyError> {
        Ok(self.sig_alg_der.clone())
    }

    fn sign_tbs_erased(&self, tbs_der: &[u8]) -> Result<Vec<u8>, PrivateKeyError> {
        do_hsm_sign(
            &self.object_label,
            self.pin.as_deref(),
            self.token_label.as_deref(),
            tbs_der,
            self.nss_alg_tag,
        )
        .map_err(PrivateKeyError::new)
    }
}

// ── priv_load_from_pkcs11_uri_nss ─────────────────────────────────────────────

/// Load the private key identified by `uri` from a real PKCS#11 token via NSS.
///
/// Parses the URI, optionally authenticates to the token slot, finds the key
/// by its object label, extracts the SPKI DER from the public half, and
/// returns a [`crate::crypto::BackendPrivateKey`] with `pkcs8_der` empty and
/// `spki_cache` populated.
///
/// The PKCS#11 module must be registered in the NSS secmod database before
/// calling this function.
pub(crate) fn priv_load_from_pkcs11_uri_nss(
    uri: &str,
) -> Result<crate::crypto::BackendPrivateKey, super::signing::NssSignerError> {
    use super::signing::NssSignerError;

    if !ensure_nss_init() {
        return Err(NssSignerError("NSS initialisation failed".to_string()));
    }

    let pkcs11 = crate::pkcs11_uri::Pkcs11Uri::parse(uri)
        .ok_or_else(|| NssSignerError(format!("not a pkcs11: URI: {}", uri)))?;

    let object_label = pkcs11
        .attrs
        .object
        .as_deref()
        .ok_or_else(|| NssSignerError("PKCS#11 URI missing 'object' attribute".to_string()))?
        .to_string();

    // `PK11_ListPrivKeysInSlot` requires a slot handle; a token= attribute is
    // therefore mandatory for the NSS backend.
    let token_label = pkcs11
        .attrs
        .token
        .as_deref()
        .filter(|l| !l.is_empty())
        .ok_or_else(|| {
            NssSignerError(
                "PKCS#11 URI must include a non-empty 'token=' attribute for the NSS backend"
                    .to_string(),
            )
        })?;
    let label_c = CString::new(token_label)
        .map_err(|e| NssSignerError(format!("token label contains NUL: {}", e)))?;

    // SAFETY: label_c is a valid NUL-terminated C string.
    let slot = unsafe { PK11_FindSlotByName(label_c.as_ptr()) };
    if slot.is_null() {
        return Err(NssSignerError(format!(
            "token '{}' not found in NSS",
            token_label
        )));
    }

    // Authenticate if a PIN is provided.
    if let Some(pin) = &pkcs11.attrs.pin_value {
        let pin_c = CString::new(pin.as_str())
            .map_err(|e| NssSignerError(format!("PIN contains NUL: {}", e)))?;
        // SAFETY: slot is non-null; pin_c is valid.
        unsafe { PK11_Authenticate(slot, PR_TRUE, pin_c.as_ptr()) };
    }

    // List private keys in this slot filtered by object label (nickname).
    let nickname_c = CString::new(object_label.as_str())
        .map_err(|e| NssSignerError(format!("object label contains NUL: {}", e)))?;
    // SAFETY: slot is non-null; nickname_c is a valid mutable C string.
    let key_list =
        unsafe { PK11_ListPrivKeysInSlot(slot, nickname_c.as_ptr() as *mut _, ptr::null()) };
    // SAFETY: slot is non-null.
    unsafe { PK11_FreeSlot(slot) };

    if key_list.is_null() {
        return Err(NssSignerError(format!(
            "PK11_ListPrivKeysInSlot returned NULL for key '{}' in token '{}'",
            object_label, token_label
        )));
    }

    // SAFETY: key_list is non-null.
    let priv_key = unsafe { take_first_key(key_list) };
    // SAFETY: key_list is non-null.
    unsafe { SECKEY_DestroyPrivateKeyList(key_list) };

    let priv_key = priv_key.ok_or_else(|| {
        NssSignerError(format!(
            "key '{}' not found in token '{}'; ensure the PKCS#11 module is registered",
            object_label, token_label
        ))
    })?;

    // Extract the SPKI DER from the corresponding public key.
    // SAFETY: priv_key is non-null.
    let pub_key = unsafe { SECKEY_ConvertToPublicKey(priv_key) };
    if pub_key.is_null() {
        unsafe { SECKEY_DestroyPrivateKey(priv_key) };
        return Err(NssSignerError(
            "SECKEY_ConvertToPublicKey returned NULL".to_string(),
        ));
    }

    // SAFETY: pub_key is non-null.
    let spki_item_ptr = unsafe { SECKEY_EncodeDERSubjectPublicKeyInfo(pub_key) };
    if spki_item_ptr.is_null() {
        unsafe { SECKEY_DestroyPublicKey(pub_key) };
        unsafe { SECKEY_DestroyPrivateKey(priv_key) };
        return Err(NssSignerError(
            "SECKEY_EncodeDERSubjectPublicKeyInfo returned NULL".to_string(),
        ));
    }

    // Copy the SPKI bytes out of the NSS-allocated SECItem.
    // SAFETY: spki_item_ptr is non-null; its .data and .len are valid.
    let spki_der = unsafe {
        let item = &*spki_item_ptr;
        std::slice::from_raw_parts(item.data, item.len as usize).to_vec()
    };

    // Free NSS-allocated structures in reverse order.
    // SAFETY: all pointers are non-null; PR_TRUE frees both .data and the struct.
    unsafe { SECITEM_FreeItem(spki_item_ptr, PR_TRUE) };
    unsafe { SECKEY_DestroyPublicKey(pub_key) };
    unsafe { SECKEY_DestroyPrivateKey(priv_key) };

    Ok(crate::crypto::BackendPrivateKey {
        pkcs8_der: std::sync::OnceLock::new(),
        spki_cache: Some(spki_der),
        pkcs11: Some(pkcs11),
    })
}