synta 0.2.5

ASN.1 parser, decoder, and encoder library with DER/BER support and C FFI
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
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
# Keys

## Algorithm identification

`identify_signature_algorithm` and `identify_public_key_algorithm` use integer
dispatch on OID components.  They never allocate and return `&'static str`.

```rust,ignore
use synta::ObjectIdentifier;
use synta_certificate::{oids, names, identify_signature_algorithm, identify_public_key_algorithm};

let oid = ObjectIdentifier::new(oids::SHA256_WITH_RSA).unwrap();
assert_eq!(identify_signature_algorithm(&oid), names::SHA256_WITH_RSA);

let oid = ObjectIdentifier::new(oids::ML_DSA_44).unwrap();
assert_eq!(identify_signature_algorithm(&oid), names::ML_DSA_44);

let oid = ObjectIdentifier::new(oids::ML_KEM_768).unwrap();
assert_eq!(identify_public_key_algorithm(&oid), Some(names::ML_KEM_768));
```

## Supported signature algorithms

| Algorithm | OID prefix | Standard |
|-----------|-----------|---------|
| RSA (PKCS #1) | 1.2.840.113549.1.1.* | RFC 3279 |
| ECDSA | 1.2.840.10045.4.* | RFC 5480 |
| EdDSA (Ed25519) | 1.3.101.112 | RFC 8410 |
| EdDSA (Ed448) | 1.3.101.113 | RFC 8410 |
| ML-DSA-44/65/87 | 2.16.840.1.101.3.4.3.17–19 | FIPS 204 |
| Composite ML-DSA (18 variants) | 1.3.6.1.5.5.7.6.37–54 | draft-ietf-lamps-pq-composite-sigs-19 |
| DSA | 1.2.840.10040.4.* | FIPS 186 |

## Supported public key algorithms

| Algorithm | Standard |
|-----------|---------|
| RSA | RFC 3279 |
| ECDSA (P-256, P-384, P-521) | RFC 5480 |
| EdDSA (Ed25519, Ed448) | RFC 8410 |
| ML-DSA (44, 65, 87) | FIPS 204 |
| Composite ML-DSA (18 variants) | draft-ietf-lamps-pq-composite-sigs-19 |
| ML-KEM (512, 768, 1024) | FIPS 203 |
| DSA | FIPS 186 |

## Public key decoding

`decode_public_key_info` dispatches on the algorithm OID and returns a
`PublicKeyInfo` enum with algorithm-specific fields:

```rust,ignore
use synta_certificate::{decode_public_key_info, PublicKeyInfo};

let spki = &cert.tbs_certificate.subject_public_key_info;
let info = decode_public_key_info(
    &spki.algorithm.algorithm,
    spki.algorithm.parameters.as_ref(),
    spki.subject_public_key.as_bytes(),
    spki.subject_public_key.bit_len(),
);

match info {
    PublicKeyInfo::Rsa { modulus, exponent, bit_count } => {
        println!("RSA-{}, exponent={}", bit_count, exponent);
    }
    PublicKeyInfo::Ec { bit_count, curve_nist_name, .. } => {
        println!("EC-{} ({})", bit_count, curve_nist_name.unwrap_or("?"));
    }
    PublicKeyInfo::MlDsa { security_level, public_key_size } => {
        println!("ML-DSA-{} ({} bytes)", security_level, public_key_size);
    }
    PublicKeyInfo::Unknown { alg_name, .. } => {
        println!("Unknown: {}", alg_name);
    }
}
```

## PKCS#8 private keys

The `pkcs8_types` module provides `OneAsymmetricKey<'a>` (RFC 5958) and a
`PrivateKeyInfo<'a>` alias:

```rust,ignore
use synta::{Decoder, Encoding};
use synta_certificate::pkcs8_types::OneAsymmetricKey;

let der = std::fs::read("private.key")?;
let mut dec = Decoder::new(&der, Encoding::Der);
let key: OneAsymmetricKey = dec.decode()?;

println!("Version: {:?}", key.version);
println!("Algorithm: {:?}", key.private_key_algorithm.algorithm);
```

## RFC 3279 algorithm parameters

The `pkixalgs_types` module provides DSA/DH domain parameter types, ECDSA
signature value types, and EC parameter types:

```rust,ignore
use synta::{Decoder, Encoding};
use synta_certificate::pkixalgs_types::{DssParms, EcdsaSigValue, ECParameters};

// DSA domain parameters
let parms = DssParms {
    p: Integer::from(23u64),
    q: Integer::from(11u64),
    g: Integer::from(5u64),
};

// ECDSA signature value (r, s)
let sig = EcdsaSigValue {
    r: Integer::from(42u64),
    s: Integer::from(17u64),
};

// EC named curve (P-256 = 1.2.840.10045.3.1.7)
let curve_oid = synta::ObjectIdentifier::new(&[1, 2, 840, 10045, 3, 1, 7]).unwrap();
let params = ECParameters::NamedCurve(curve_oid);
```

## Crypto backends: NSS and OpenSSL

`synta-certificate` supports two pluggable crypto backends selected at compile time:

| Feature | Default | Backend |
|---------|---------|---------|
| `openssl` | yes | OpenSSL bindings via the `native-ossl` crate |
| `nss` | no | Mozilla NSS via the `nss-sys` crate |

When both features are compiled in, **OpenSSL takes priority** and the NSS
backend module is excluded entirely (`#[cfg(all(feature = "nss", not(feature = "openssl")))]`).
To select NSS as the sole backend:

```toml
[dependencies]
synta-certificate = { version = "0.1", default-features = false, features = ["std", "derive", "nss"] }
```

### Backend-agnostic key abstractions

The recommended approach to key generation and signing never names a backend type:

```rust,ignore
use synta_certificate::{PrivateKeyBuilder, CertificateBuilder};

// Generate a P-256 key (delegates to OpenSSL or NSS depending on active features)
let key = PrivateKeyBuilder::ec("P-256").generate()?;

// Public key as DER-encoded SubjectPublicKeyInfo — use it with CertificateBuilder
let spki_der = key.public_key_spki_der()?;

// Create a signer; "sha256" selects the hash (ignored for EdDSA)
let signer = key.as_signer("sha256");

// Pass the signer directly to CertificateBuilder::sign
let cert_der = CertificateBuilder::new()
    /* … set fields … */
    .sign(signer.as_ref())?;
```

Other key algorithms:

```rust,ignore
let rsa_key = PrivateKeyBuilder::rsa(3072).generate()?;
let ed_key  = PrivateKeyBuilder::ed25519().generate()?;
let pqc_key = PrivateKeyBuilder::ml_dsa("ML-DSA-65").generate()?; // OpenSSL 3.5+ or NSS

// Composite ML-DSA-65 + ECDSA-P256-SHA512 (sub-arc 45):
// requires openssl + pqc features (OpenSSL 3.3+) or the nss feature
let comp_key = PrivateKeyBuilder::composite_ml_dsa(45).generate()?;
```

`BackendPrivateKey` can be used when you need to load, store, or serialise PKCS#8 bytes:

```rust,ignore
use synta_certificate::BackendPrivateKey;

// Load from unencrypted PKCS#8 DER (validates the key on load)
let key = BackendPrivateKey::from_der(&pkcs8_der)?;

// Load from PEM, optionally password-protected
let key = BackendPrivateKey::from_pem(pem_bytes, None)?;
let key = BackendPrivateKey::from_pem(pem_bytes, Some(b"passphrase"))?;
```

`BackendPrivateKey` also implements the `PrivateKey` trait, so `key.as_signer("sha256")`
and `key.public_key_spki_der()` work just like with the boxed trait object from
`PrivateKeyBuilder::generate`.

### BackendPrivateKey — serialisation, generation, and decryption

All methods in this section require the `openssl` feature.

**Serialisation**

| Method | Description |
|--------|-------------|
| `from_der(der: &[u8])` | Load from unencrypted PKCS#8 DER |
| `from_pem(pem: &[u8], password: Option<&[u8]>)` | Load from PEM, optionally encrypted |
| `from_pkcs8_encrypted(data: &[u8], password: &[u8])` | Load from encrypted PKCS#8 DER |
| `to_der()` | Serialize to unencrypted PKCS#8 DER |
| `to_pem(password: Option<&[u8]>)` | Serialize to PEM (AES-256-CBC when password is given) |
| `to_pkcs8_encrypted(password: &[u8])` | Serialize to `EncryptedPrivateKeyInfo` DER |
| `public_key()` | Extract the public half as `BackendPublicKey` |
| `key_type()` | Key algorithm as a lowercase string (`"rsa"`, `"ec"`, `"ed25519"`, …) |
| `key_bit_size()` | Key size in bits, or `None` for EdDSA keys |

```rust,ignore
use synta_certificate::BackendPrivateKey;

let key = BackendPrivateKey::generate_rsa(3072, 65537)?;
let pem = key.to_pem(Some(b"s3cr3t"))?;
std::fs::write("key.pem", &pem)?;

let loaded = BackendPrivateKey::from_pem(&pem, Some(b"s3cr3t"))?;
let pub_key = loaded.public_key()?;
```

**Generation** (prefer `PrivateKeyBuilder` for backend-agnostic code)

`BackendPrivateKey` has PQC generation; classical algorithms are on `OpensslPrivateKey`:

| Method | Type | Description |
|--------|------|-------------|
| `generate_ml_dsa(parameter_set: &str)` | `BackendPrivateKey` | ML-DSA-44/65/87 (OpenSSL 3.5+, `pqc` feature) |
| `generate_ml_kem(parameter_set: &str)` | `BackendPrivateKey` | ML-KEM-512/768/1024 (OpenSSL 3.5+, `pqc` feature) |
| `generate_rsa(key_size: u32, public_exponent: u32)` | `OpensslPrivateKey` | RSA (e.g. 3072, 65537) |
| `generate_ec(curve: &str)` | `OpensslPrivateKey` | EC key on `"P-256"`, `"P-384"`, or `"P-521"` |
| `generate_ed25519()` | `OpensslPrivateKey` | Ed25519 key |
| `generate_ed448()` | `OpensslPrivateKey` | Ed448 key |

**Decryption** (`OpensslPrivateKey` only, not on `BackendPrivateKey`)

| Method | Description |
|--------|-------------|
| `rsa_pkcs1v15_decrypt(ciphertext: &[u8])` | RSA PKCS#1 v1.5 decryption |
| `rsa_oaep_decrypt(ciphertext: &[u8], hash_alg: &str)` | RSA-OAEP decryption |

Use `BackendPrivateKey::from_pkcs11_uri` or `PrivateKeyBuilder` when you do not need
direct access to the raw `OpensslPrivateKey` type.

### ML-KEM key exchange

ML-KEM (FIPS 203) uses an encapsulation/decapsulation exchange rather than signing.
Both methods require either the `openssl` or `nss` feature.

```rust,ignore
use synta_certificate::{BackendPrivateKey, BackendPublicKey};

// Sender: encapsulate against the recipient's public key
let (ciphertext, shared_secret) = pub_key.ml_kem_encapsulate()?;

// Recipient: decapsulate to recover the shared secret
let recovered = priv_key.ml_kem_decapsulate(&ciphertext)?;
assert_eq!(shared_secret, recovered);
```

### ML-DSA context-string operations

Both methods pass an explicit FIPS 204 §5.2 context string for domain separation.
Pass `b""` for the default empty context.

```rust,ignore
use synta_certificate::{BackendPrivateKey, BackendPublicKey};

// Sign with context
let sig = priv_key.sign_ml_dsa_with_context(data, b"app-v1")?;

// Verify with context
pub_key.verify_ml_dsa_with_context(data, &sig, b"app-v1")?;
```

### Importing private keys from raw components

`BackendPrivateKey` provides two constructors for importing key material that
already exists as raw big-endian byte slices (e.g. from a JWK or a hardware-
derived key buffer).  Both require the `openssl` or `nss` feature.

**`RsaPrivateComponents`** is a helper struct that names the eight CRT fields
needed for RSA import.  All fields are `&[u8]` slices containing big-endian
unsigned integers, matching the encoding used by JWK and PKCS#1:

| Field | Meaning |
|-------|---------|
| `n`   | RSA modulus |
| `e`   | Public exponent |
| `d`   | Private exponent |
| `p`   | First prime factor |
| `q`   | Second prime factor |
| `dp`  | CRT exponent *d* mod (*p* − 1) |
| `dq`  | CRT exponent *d* mod (*q* − 1) |
| `qi`  | CRT coefficient *q*⁻¹ mod *p* |

```rust,ignore
use synta_certificate::{BackendPrivateKey, RsaPrivateComponents};

// Import an RSA private key from its CRT components
let key = BackendPrivateKey::from_rsa_private_components(&RsaPrivateComponents {
    n: &n_bytes,
    e: &e_bytes,
    d: &d_bytes,
    p: &p_bytes,
    q: &q_bytes,
    dp: &dp_bytes,
    dq: &dq_bytes,
    qi: &qi_bytes,
})?;

// Import an EC private key from scalar d and public-point affine coordinates
// curve must be "P-256", "P-384", or "P-521"
let key = BackendPrivateKey::from_ec_private_scalar(
    &d_bytes,   // private scalar (big-endian)
    &x_bytes,   // affine X coordinate (big-endian)
    &y_bytes,   // affine Y coordinate (big-endian)
    "P-256",
)?;
```

Both constructors return `Err(PrivateKeyError)` when the backend rejects the
supplied key material (e.g. the EC point is not on the curve, or the RSA CRT
values are internally inconsistent).  There is no Python-level equivalent;
Python callers must first encode the raw bytes into a PKCS#8 or PEM structure
and then use `PrivateKey.from_der` / `PrivateKey.from_pem`.

### Backend-dispatched factory functions

`default_signature_verifier` and `default_key_id_hasher` return trait objects backed by
whichever feature is active:

```rust,ignore
use synta_certificate::{default_signature_verifier, default_key_id_hasher};

let verifier = default_signature_verifier(); // Box<dyn ErasedSignatureVerifier>
let hasher   = default_key_id_hasher();      // Box<dyn ErasedKeyIdHasher>
```

Additional default factory functions cover symmetric crypto:

| Function | Return type | Trait |
|----------|-------------|-------|
| `default_data_hasher()` | `Box<dyn ErasedDataHasher>` | `DataHasher` |
| `default_hmac_provider()` | `Box<dyn ErasedHmacProvider>` | `HmacProvider` |
| `default_streaming_hasher()` | `Box<dyn ErasedStreamingHasher>` | `StreamingHasher` |
| `default_streaming_hmac_provider()` | `Box<dyn ErasedStreamingHmacProvider>` | `StreamingHmacProvider` |
| `default_pbkdf2_provider()` | `impl Pbkdf2Provider` | `Pbkdf2Provider` |
| `default_block_cipher_provider()` | `impl BlockCipherProvider` | `BlockCipherProvider` |
| `default_secure_random()` | `impl SecureRandom` | `SecureRandom` |

`BlockCipherProvider` exposes AES-CBC, AES-GCM (AEAD), and 3DES-CBC operations.
The AES-GCM methods perform authenticated encryption with additional data (AEAD):

```rust,ignore
use synta_certificate::default_block_cipher_provider;

let cipher = default_block_cipher_provider();

// Encrypt: key is 16/24/32 bytes; nonce is 12 bytes; aad may be empty.
// Returns ciphertext || 16-byte authentication tag.
let ct = cipher.aes_gcm_encrypt(&key, &nonce, plaintext, aad)?;

// Decrypt: ciphertext_with_tag is ciphertext || tag (last 16 bytes).
// Returns Err if the tag does not verify.
let pt = cipher.aes_gcm_decrypt(&key, &nonce, &ct, aad)?;
```

All functions select the NSS backend when `nss` is active, fall back to OpenSSL, and
return a no-op stub when neither feature is compiled in.

```rust,ignore
use synta_certificate::{
    default_hmac_provider, default_streaming_hasher, default_pbkdf2_provider,
};

let hmac    = default_hmac_provider();          // Box<dyn ErasedHmacProvider>
let hasher  = default_streaming_hasher();       // Box<dyn ErasedStreamingHasher>
let pbkdf2  = default_pbkdf2_provider();        // impl Pbkdf2Provider
```

### BackendPublicKey — verify any X.509 signature

`BackendPublicKey::verify_signature` verifies an X.509 signature using the full
`AlgorithmIdentifier` DER from the certificate, routing to NSS when enabled.

When the key was created via `from_der` or `from_pem`, the parsed `EVP_PKEY` is
cached at construction time (an O(1) refcount clone).  Subsequent calls to
`verify_signature` reuse that cached key rather than re-parsing the DER, which
matters for workloads that verify many items — for example, 1 024 certificates
signed by the same CA key — against a single `BackendPublicKey` instance.

```rust,ignore
use synta_certificate::BackendPublicKey;

// spki_der: DER-encoded SubjectPublicKeyInfo (Vec<u8> or &[u8])
let pub_key = BackendPublicKey::from_spki_der(spki_der);
pub_key.verify_signature(tbs_der, sig_alg_der, signature)?;
```

- `tbs_der` — DER bytes of `TBSCertificate` (the bytes that were signed).
- `sig_alg_der` — DER bytes of the outer `AlgorithmIdentifier` SEQUENCE.
- `signature` — raw signature bytes (BIT STRING value, unused-bits byte stripped).

Returns `Ok(())` on success, `Err(PrivateKeyError)` on failure or unsupported algorithm.

**Loading and construction** (all require `openssl` feature unless noted)

| Method | Description |
|--------|-------------|
| `from_spki_der(spki_der: Vec<u8>)` | Wrap raw SPKI DER without validation (no feature required) |
| `from_der(der: &[u8])` | Parse SPKI DER; caches `EVP_PKEY` for fast repeated use |
| `from_pem(pem: &[u8])` | Parse PEM SubjectPublicKeyInfo; caches `EVP_PKEY` |
| `from_rsa_components(n: &[u8], e: &[u8])` | Build RSA key from big-endian modulus and exponent |
| `from_ec_components(x: &[u8], y: &[u8], curve: &str)` | Build EC key from affine coordinates and curve name |

**Introspection** (all require `openssl` feature)

| Method | Return | Description |
|--------|--------|-------------|
| `key_type()` | `&'static str` | `"rsa"`, `"ec"`, `"ed25519"`, `"ed448"`, or `"unknown"` |
| `key_bit_size()` | `Option<i64>` | Key size in bits; `None` for EdDSA keys |
| `rsa_modulus()` | `Result<Option<Vec<u8>>, _>` | RSA modulus *n* as big-endian bytes |
| `rsa_public_exponent()` | `Result<Option<Vec<u8>>, _>` | RSA exponent *e* as big-endian bytes |
| `ec_curve_name()` | `Result<Option<&'static str>, _>` | NIST curve name for EC keys |
| `ec_affine_coordinates()` | `Result<Option<EcAffineCoords>, _>` | Affine (X, Y) for EC keys |

```rust,ignore
use synta_certificate::BackendPublicKey;

let pub_key = BackendPublicKey::from_der(&spki_der)?;

println!("Type: {}", pub_key.key_type());         // "ec"
println!("Bits: {:?}", pub_key.key_bit_size());    // Some(256)

if let Some(curve) = pub_key.ec_curve_name()? {
    println!("Curve: {}", curve);                  // "P-256"
}

if let Some(coords) = pub_key.ec_affine_coordinates()? {
    println!("X len: {}", coords.x.len());
}
```

**Encryption** (available on both `openssl` and NSS backends)

| Method | Description |
|--------|-------------|
| `rsa_oaep_encrypt(plaintext: &[u8], hash_alg: &str)` | RSA-OAEP encryption |
| `rsa_pkcs1v15_encrypt(plaintext: &[u8])` | RSA PKCS#1 v1.5 encryption |

**Signature verification** (available on `openssl` backend)

| Method | Description |
|--------|-------------|
| `verify_message(data: &[u8], signature: &[u8], algorithm: Option<&str>)` | Verify using a hash-name string |
| `verify_signature(tbs_der, sig_alg_der, signature)` | Verify using full `AlgorithmIdentifier` DER |
| `verify_ml_dsa_with_context(data, signature, context)` | ML-DSA verify with FIPS 204 context string |

### PKCS#11 URI — hardware token keys

`BackendPrivateKey::from_pkcs11_uri` loads a key reference from a PKCS#11 URI
(RFC 7512) without extracting private key material from the token.

The `pkcs11-provider` OpenSSL provider (or an equivalent NSS PKCS#11 module) must
be configured in the OpenSSL or NSS configuration before calling this function.

```rust,ignore
use synta_certificate::BackendPrivateKey;

let key = BackendPrivateKey::from_pkcs11_uri(
    "pkcs11:token=MyHSM;object=signing-key;type=private?pin-value=1234"
)?;

// The public SPKI is extracted from the token; private material stays in the HSM
let spki_der = key.public_key_spki_der()?;
let signer   = key.as_signer("sha256");
```

`Pkcs11Uri` and `Pkcs11UriAttributes` are exported from the crate root and give
structured access to a parsed URI without re-parsing on each use:

```rust,ignore
use synta_certificate::{Pkcs11Uri, Pkcs11UriAttributes};

let uri = Pkcs11Uri::parse("pkcs11:token=MyHSM;object=cakey;id=%01%02?pin-value=1234")
    .expect("not a pkcs11: URI");

println!("Token:  {:?}", uri.attrs.token);      // Some("MyHSM")
println!("Object: {:?}", uri.attrs.object);     // Some("cakey")
println!("ID:     {:?}", uri.attrs.id);         // Some([0x01, 0x02])
println!("PIN:    {:?}", uri.attrs.pin_value);  // Some("1234")
println!("Raw:    {}", uri.raw);
```

`Pkcs11UriAttributes` fields:

| Field | Type | RFC 7512 attribute |
|-------|------|-------------------|
| `token` | `Option<String>` | `token=` (path component) |
| `object` | `Option<String>` | `object=` (path component) |
| `id` | `Option<Vec<u8>>` | `id=` decoded from percent-encoded bytes |
| `pin_value` | `Option<String>` | `?pin-value=` (query attribute) |

Unknown path and query attributes are silently ignored per RFC 7512 §2.1.

### NSS backend types (`nss` feature)

| Type | Export path | Description |
|------|-------------|-------------|
| `NssSignatureVerifier` | crate root | Verifies signatures via `VFY_VerifyDataWithAlgorithmID`; supports RSA PKCS#1 / RSA-PSS / ECDSA / EdDSA / ML-DSA |
| `NssVerifierError` | crate root | Error type for `NssSignatureVerifier` |
| `NssSigner` | `nss_backend::NssSigner` | Signs TBS blobs from a PKCS#8 private key imported into the NSS in-memory softokn slot |
| `NssSignerError` | `nss_backend::NssSignerError` | Error type for `NssSigner` |
| `NssKeyIdHasher` | crate root | SHA-1/256/384/512 via `PK11_HashBuf` for SKI/AKI extension encoding |

### OpenSSL backend types (`openssl` feature)

| Type | Description |
|------|-------------|
| `OpensslPrivateKey` | OpenSSL-backed private key; implements `PrivateKey` |
| `OpensslCertificateSigner` | Implements `CertificateSigner` around a `native_ossl::pkey::Pkey<Private>`; supports ML-DSA context strings via `with_context` |
| `OpensslCertificateSignerError` | Error type for `OpensslCertificateSigner` |
| `OpensslSignatureVerifier` | Verifies X.509 signatures via OpenSSL EVP |
| `OpensslVerifierError` | Error type for `OpensslSignatureVerifier` |
| `OpensslKeyIdHasher` | SHA-1/256/384/512 hashing for SKI/AKI encoding |
| `OpensslKeyIdHasherError` | Error type for `OpensslKeyIdHasher` |


### ML-DSA context strings

FIPS 204 §5.2 allows an application-defined context string (≤ 255 bytes) for
domain separation.  Pass it via the `with_context` builder:

```rust,ignore
use synta_certificate::OpensslCertificateSigner;

let signer = OpensslCertificateSigner::new(&ml_dsa_key, "")
    .with_context(b"my-application-v1");

let cert_der = CertificateBuilder::new()
    /* … */
    .sign(&signer)?;
```

`with_context` is a no-op for RSA, ECDSA, and EdDSA keys.

## Composite ML-DSA (draft-ietf-lamps-pq-composite-sigs-19)

Composite ML-DSA algorithms pair an ML-DSA component with a traditional algorithm
(RSA-PSS, RSA-PKCS#1v15, ECDSA, Ed25519, or Ed448) to produce a single composite
public key and signature.  The 18 algorithm variants occupy OID sub-arcs 37–54 of
the `id-alg` arc `1.3.6.1.5.5.7.6`.

### Key generation

Select a variant by its OID sub-arc and call `composite_ml_dsa`:

```rust,ignore
use synta_certificate::{PrivateKeyBuilder, CertificateBuilder};

// Sub-arc 45 = MLDSA65-ECDSA-P256-SHA512
let key = PrivateKeyBuilder::composite_ml_dsa(45).generate()?;
let spki_der = key.public_key_spki_der()?;
let signer   = key.as_signer(""); // algorithm string is ignored for composite ML-DSA

let cert_der = CertificateBuilder::new()
    /* … set fields … */
    .sign(signer.as_ref())?;
```

### OID sub-arc table

| Sub-arc | Algorithm | ML-DSA level |
|---------|-----------|-------------|
| 37 | MLDSA44-RSA2048-PSS-SHA256 | 44 |
| 38 | MLDSA44-RSA2048-PKCS15-SHA256 | 44 |
| 39 | MLDSA44-Ed25519-SHA512 | 44 |
| 40 | MLDSA44-ECDSA-P256-SHA256 | 44 |
| 41 | MLDSA65-RSA3072-PSS-SHA512 | 65 |
| 42 | MLDSA65-RSA3072-PKCS15-SHA512 | 65 |
| 43 | MLDSA65-RSA4096-PSS-SHA512 | 65 |
| 44 | MLDSA65-RSA4096-PKCS15-SHA512 | 65 |
| 45 | MLDSA65-ECDSA-P256-SHA512 | 65 |
| 46 | MLDSA65-ECDSA-P384-SHA512 | 65 |
| 47 | MLDSA65-ECDSA-Brainpool-P256r1-SHA512 | 65 |
| 48 | MLDSA65-Ed25519-SHA512 | 65 |
| 49 | MLDSA87-ECDSA-P384-SHA512 | 87 |
| 50 | MLDSA87-ECDSA-Brainpool-P384r1-SHA512 | 87 |
| 51 | MLDSA87-Ed448-SHAKE256 | 87 |
| 52 | MLDSA87-RSA3072-PSS-SHA512 | 87 |
| 53 | MLDSA87-RSA4096-PSS-SHA512 | 87 |
| 54 | MLDSA87-ECDSA-P521-SHA512 | 87 |

### Feature requirements

| Backend | Feature flags required | Minimum version |
|---------|----------------------|----------------|
| OpenSSL | `openssl` + `pqc` | OpenSSL 3.3+ (`cfg(ossl_mldsa)`) |
| NSS | `nss` | Any supported NSS version |

### Encoding rules (draft-19)

- **SPKI BIT STRING payload**: `mldsa_pk || trad_pk`, split at ML-DSA public key size
  (1312 / 1952 / 2592 bytes for ML-DSA-44 / 65 / 87).
- **Composite signature**: `mldsa_sig || trad_sig`, split at ML-DSA signature size
  (2420 / 3309 / 4627 bytes).
- **PKCS#8 privateKey OCTET STRING content**: `mldsa_seed (32 bytes) || trad_sk`.
- **Message representative M'**: `"CompositeAlgorithmSignatures2025" || Label || 0x00 || PH(TBS)`
  where `PH` is SHA-256, SHA-512, or SHAKE256-512bit per the variant spec.

### OID constants

All 18 OID constants are exported from `synta_certificate::oids`:

```rust,ignore
use synta_certificate::oids::{
    MLDSA44_RSA2048_PSS_SHA256,
    MLDSA44_RSA2048_PKCS15_SHA256,
    MLDSA44_ED25519_SHA512,
    MLDSA44_ECDSA_P256_SHA256,
    MLDSA65_RSA3072_PSS_SHA512,
    // … through MLDSA87_ECDSA_P521_SHA512
    COMPOSITE_MLDSA_ARC, // prefix OID 1.3.6.1.5.5.7.6 for arc membership checks
};
```

### NSS backend limitations

The following restrictions apply when using the NSS backend for composite ML-DSA:

- **SHAKE256 hash** (sub-arc 51, MLDSA87-Ed448-SHAKE256) is not available via
  `PK11_HashBuf` and is therefore unsupported for both signing and verification.
- **Brainpool curves** (sub-arcs 47, 50) are not supported for either signing
  or verification on NSS (no brainpool support in the NSS EC parameter table).

## PKCS#9 OID constants

9 attribute OIDs from RFC 2985, RFC 5652, RFC 2986, and RFC 7292 are exposed in
`oids`:

| Constant | OID suffix |
|----------|------------|
| `PKCS9_EMAIL_ADDRESS` | 9.1 |
| `PKCS9_UNSTRUCTURED_NAME` | 9.2 |
| `PKCS9_CONTENT_TYPE` | 9.3 |
| `PKCS9_MESSAGE_DIGEST` | 9.4 |
| `PKCS9_SIGNING_TIME` | 9.5 |
| `PKCS9_COUNTERSIGNATURE` | 9.6 |
| `PKCS9_CHALLENGE_PASSWORD` | 9.7 |
| `PKCS9_EXTENSION_REQUEST` | 9.14 |
| `PKCS9_FRIENDLY_NAME` | 9.20 |
| `PKCS9_LOCAL_KEY_ID` | 9.21 |