synta 0.2.3

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
# System Architecture

This document describes the high-level architecture of the Synta project: how
the crates relate to each other, how data flows through the library, and how
the three language interfaces (Rust, C, Python) are layered on top of a common
core.

---

## Crate Dependency Graph

```mermaid
graph TD
    synta["synta\n(core ASN.1 codec)"]
    derive["synta-derive\n(proc macros)"]
    codegen["synta-codegen\n(ASN.1 → Rust generator)"]
    cert["synta-certificate\n(X.509 / PKI)"]
    cbor["synta-cbor\n(CBOR / RFC 8949)"]
    krb5["synta-krb5\n(Kerberos V5)"]
    mtc["synta-mtc\n(Merkle Tree Certs)"]
    x509v["synta-x509-verification\n(RFC 5280 path validation)"]
    ffi["synta-ffi\n(C FFI — libcsynta)"]
    tools["synta-tools\n(CLI utilities)"]
    pycommon["synta-python-common\n(shared PyO3 helpers)"]
    py["synta-python\n(_synta.so)"]
    pykrb5["synta-python-krb5\n(_krb5.so)"]
    pymtc["synta-python-mtc\n(_mtc.so)"]
    bench["synta-bench\n(benchmarks)"]
    fuzz["synta-fuzz\n(fuzzer)"]

    cryptoki["cryptoki 0.12\n(optional — dlopen)"]

    derive --> synta
    codegen --> synta
    cert --> synta
    cert --> derive
    cert -. "pkcs11-mgmt feature" .-> cryptoki
    cbor --> synta
    krb5 --> synta
    krb5 --> derive
    mtc --> synta
    mtc --> cert
    x509v --> synta
    x509v --> cert
    ffi --> synta
    ffi --> cert
    tools --> synta
    tools --> cert
    pycommon --> synta
    py --> synta
    py --> cert
    py --> x509v
    py --> pycommon
    pykrb5 --> synta
    pykrb5 --> krb5
    pykrb5 --> pycommon
    pymtc --> synta
    pymtc --> mtc
    pymtc --> pycommon
    bench --> synta
    bench --> cert
    bench --> ffi
    fuzz --> synta
    fuzz --> cert

    style cryptoki fill:#fff8e1
```

---

## Layer Architecture

```mermaid
graph BT
    subgraph "Language Interfaces"
        python["Python 3.8+\nsynta package\n(PyO3 ABI3)"]
        c["C / C++\nlibcsynta\n(cbindgen header)"]
        rust["Rust\ndirect crate dependency"]
    end

    subgraph "Protocol Crates"
        cert["synta-certificate\nX.509, CRL, CSR, OCSP\nCMS, PKCS#7, PKCS#12\nKerberos PKINIT"]
        cbor["synta-cbor\nCBOR encoder/decoder\n(RFC 8949 / RFC 9090)"]
        krb5["synta-krb5\nKerberos V5\nSPNEGO / GSSAPI\nPKINIT"]
        mtc["synta-mtc\nMerkle Tree\nCertificates"]
        x509v["synta-x509-verification\nRFC 5280 path\nvalidation"]
    end

    subgraph "Code Generation"
        codegen["synta-codegen\nASN.1 schema parser\nRust / C code emitter"]
        asn1files["ASN.1 Schemas (47)\nX.509, Kerberos, CMS\nPKCS, OCSP, MTC, PQC"]
    end

    subgraph "Core Library"
        synta["synta\nDER/BER/CER decoder\nEncoder (backpatching)\nASN.1 type system\nDerive macros"]
    end

    python --> cert
    python --> krb5
    python --> mtc
    python --> x509v
    c --> cert
    rust --> cert
    rust --> krb5
    rust --> mtc
    rust --> x509v

    cert --> synta
    cbor --> synta
    krb5 --> synta
    mtc --> synta
    x509v --> synta

    codegen --> asn1files
    asn1files -.->|build.rs generates| cert
    asn1files -.->|build.rs generates| krb5
    asn1files -.->|build.rs generates| mtc
```

---

## Data Flow: Certificate Parsing

```mermaid
sequenceDiagram
    participant Caller
    participant synta_cert as synta-certificate
    participant synta_core as synta (core)
    participant Decode as Decode trait

    Caller->>synta_cert: Certificate::from_der(bytes)
    synta_cert->>synta_core: Decoder::new(bytes, DER)
    synta_cert->>synta_core: decoder.decode::<TBSCertificate>()
    synta_core->>Decode: Decode::decode(&mut decoder)
    Note over synta_core,Decode: #[derive(Asn1Sequence)] generated impl<br/>reads SEQUENCE tag, iterates fields
    Decode-->>synta_core: TBSCertificate { version, serial, ... }
    synta_core-->>synta_cert: Ok(Certificate { tbs, alg, sig })
    synta_cert-->>Caller: Ok(Certificate)
    Caller->>synta_cert: cert.tbs_certificate.subject
    synta_cert-->>Caller: &Name<'_>  (zero-copy borrowed)
```

---

## Data Flow: Code Generation

```mermaid
sequenceDiagram
    participant Schema as .asn1 file
    participant Codegen as synta-codegen
    participant BuildRS as build.rs
    participant Rust as Generated .rs

    BuildRS->>Codegen: parse("X509-Certificate.asn1")
    Codegen->>Schema: read schema text
    Codegen-->>BuildRS: Module AST
    BuildRS->>Codegen: generate_with_config(module, config)
    Note over Codegen: StringTypeMode::Borrowed<br/>DeriveMode::FeatureGated<br/>any_as_raw_der: true
    Codegen-->>BuildRS: Rust source string
    BuildRS->>Rust: write OUT_DIR/x509_generated.rs
    Rust-->>BuildRS: include!(concat!(env!("OUT_DIR"), "/x509_generated.rs"))
```

---

## Memory Model: Zero-Copy Parsing

Synta's core design avoids heap allocation on the parse path wherever possible.

```mermaid
graph LR
    subgraph Input
        buf["&[u8] input buffer"]
    end
    subgraph Decoder
        cursor["Decoder cursor\n(pointer + length)"]
    end
    subgraph Borrowed Output
        raw["OctetStringRef<'a>\nBitStringRef<'a>\nRawDer<'a>\nSequence<'a>"]
    end
    subgraph Owned Output
        own["OctetString\nBitString\nInteger (SmallVec)"]
    end

    buf --> cursor
    cursor -- "zero-copy slice" --> raw
    cursor -- "heap alloc" --> own
    raw -. "lifetime 'a tied to buf" .-> buf
```

Key invariants:
- Borrowed types (`*Ref<'a>`, `RawDer<'a>`) carry a lifetime tied to the
  decoder's input buffer — the borrow checker prevents use-after-free.
- `Sequence<'a>` / `Set<'a>` capture raw content bytes and decode elements
  lazily on iteration — O(1) decode, O(n) iteration.
- `Integer` uses a 16-byte inline `SmallVec` (no heap allocation for i128/u128
  and smaller).
- `ObjectIdentifier` uses a 10-element inline `SmallVec` (covers nearly all
  real-world OIDs without allocation).

---

## C FFI Layer

```mermaid
graph TD
    cheader["include/synta.h\n(generated by cbindgen)"]
    clib["libcsynta.so\n(cdylib from synta-ffi)"]
    certrs["synta-ffi/src/certificate.rs\nowned DER buffer + parsed TypedCert"]
    typesr["synta-ffi/src/types.rs\nSyntaByteArray (16 bytes)\nSyntaDecoder, SyntaEncoder"]
    errorr["synta-ffi/src/error.rs\nthread-local error message"]
    cert["synta-certificate\n(Rust types)"]
    synta["synta\n(core codec)"]

    cheader -- "mirrors" --> clib
    clib --> certrs
    clib --> typesr
    clib --> errorr
    certrs --> cert
    cert --> synta

    style cheader fill:#e8f4e8
    style clib fill:#e8f4e8
```

**Memory ownership conventions** (OpenSSL-style naming):
- `synta_*_parse_der()` — returns owned opaque handle; caller must free.
- `synta_*_free()` — frees an owned handle.
- `get0_*()` — returns borrowed pointer valid while parent handle is alive; do **not** free.
- `SyntaByteArray::owned == 0` — borrowed data; `owned != 0` — heap-allocated data.

---

## Python Extension Architecture

Three shared libraries compose the `synta` Python package:

| Library | Crate | Registers |
|---------|-------|-----------|
| `_synta.so` | `synta-python` | All core types + `synta.crypto`, `synta.ext`, `synta.x509`, `synta.certificate`, `synta.general_name` |
| `_krb5.so` | `synta-python-krb5` | `synta.krb5`, `synta.spnego` |
| `_mtc.so` | `synta-python-mtc` | `synta.mtc` |

Pure Python stub modules in `python/synta/` provide:
- `.pyi` type stubs for IDEs and static analysis (PEP 561)
- Documentation strings accessible from `help()`
- Re-exports that make sub-attributes accessible on the top-level `synta` module

```mermaid
graph TB
    py["import synta"]
    init["python/synta/__init__.py"]
    synta_so["_synta.so\n(synta-python)"]
    krb5_so["_krb5.so\n(synta-python-krb5)"]
    mtc_so["_mtc.so\n(synta-python-mtc)"]
    stubs["python/synta/*.pyi\n(type stubs)"]

    py --> init
    init --> synta_so
    init --> krb5_so
    init --> mtc_so
    init --> stubs

    synta_so --> cert["synta-certificate\n(Rust)"]
    synta_so --> x509v["synta-x509-verification\n(Rust)"]
    krb5_so --> krb5["synta-krb5\n(Rust)"]
    mtc_so --> mtc["synta-mtc\n(Rust)"]
```

---

## Build System Integration

```mermaid
graph LR
    cargo["cargo build\n--workspace"]
    buildrs_cert["synta-certificate/build.rs"]
    buildrs_krb5["synta-krb5/build.rs"]
    buildrs_mtc["synta-mtc/build.rs"]
    codegen_bin["synta-codegen\n(library)"]
    asn1["asn1/*.asn1 schemas"]
    out_cert["OUT_DIR/\n*_generated.rs"]
    out_krb5["OUT_DIR/\nkerberos_v5_generated.rs\n..."]
    out_mtc["OUT_DIR/\nmtc_generated.rs"]
    cbindgen["cbindgen\n(synta-ffi/build.rs)"]
    header["include/synta.h"]

    cargo --> buildrs_cert
    cargo --> buildrs_krb5
    cargo --> buildrs_mtc
    buildrs_cert --> codegen_bin
    buildrs_krb5 --> codegen_bin
    buildrs_mtc --> codegen_bin
    codegen_bin --> asn1
    codegen_bin --> out_cert
    codegen_bin --> out_krb5
    codegen_bin --> out_mtc
    cargo --> cbindgen
    cbindgen --> header
```

---

## Merkle Tree Certificate Architecture (`synta-mtc`)

`synta-mtc` implements the MTC specification
([draft-ietf-plants-merkle-tree-certs](https://davidben.github.io/merkle-tree-certs/draft-ietf-plants-merkle-tree-certs.html)).
This section describes the design decisions that diverge from naive expectations.

### Proof path encoding

Inclusion proof paths contain only sibling hashes (`ProofNode = OCTET STRING`).
There are no direction bits stored in the wire format.  The direction (left vs.
right) at each tree level is computed from the leaf index during verification
(spec §4.3.2): if the current position is even it is a left child; if it is odd
it is a right child; if it is the last node at an odd-sized level it carries
to the next level without hashing.

This matches RFC 9162 (Certificate Transparency v2) and simplifies the encoding:
callers produce and consume `Vec<Vec<u8>>` (one `Vec<u8>` per tree level).

### Leaf hash computation

Leaf hashes use TLS wire encoding rather than raw DER (spec §4.1).  The wire
encoding inserts a two-byte `uint16` entry-type tag before the entry payload:

| Entry type | Wire bytes | Effect |
|------------|-----------|--------|
| `null_entry` | `[0x00, 0x00]` | Two bytes total; no DER payload |
| `tbs_cert_entry` | `[0x00, 0x01]` + DER contents | Type prefix + inner SEQUENCE contents (outer `0x30 <len>` stripped) |

The 0x00 domain-separation byte is then prepended by `hash_leaf` (RFC 6962 §2.1):
`leaf_hash = Hash(0x00 || tls_wire_bytes)`.

This ensures the entry type participates in the preimage and that two entries
with different types but identical DER bytes produce different leaf hashes.

### Cosignature signed-data format

Cosigners sign a TLS binary structure (`CosignedMessage`) rather than a
DER-encoded `Checkpoint`.  The `CosignedMessage` wire format (spec §5.4.1) is:

```text
struct {
    uint8  label[12] = "subtree/v1\n\0";  // domain separation
    opaque cosigner_name<1..255>;          // length-prefixed OID string
    uint64 timestamp;                      // Unix seconds from checkpoint
    opaque log_origin<1..255>;             // length-prefixed OID string
    uint64 start;                          // subtree start (big-endian)
    uint64 end;                            // subtree end (big-endian)
    HashValue subtree_hash;                // raw subtree hash bytes
} CosignedMessage;
```

Both `cosigner_name` and `log_origin` are the dotted-decimal OID string of the
respective hash algorithm, prefixed with `"oid/"`.

### Subtree alignment constraint

Per spec §4.1, every subtree must satisfy an alignment constraint:
`start % BIT_CEIL(end - start) == 0`, where `BIT_CEIL(n)` is the smallest
power of two at least as large as `n` (Rust: `n.next_power_of_two()`).  When
`start == 0` the constraint is trivially satisfied.  The constraint is checked
by `constraint::validate_subtree_range` and is applied in both
`verify_subtree_consistency` and `validate_cosignature_structure`.

### Serial number equals log entry index

Per spec §6.1, `TBSCertificate.serialNumber` must equal the `log_entry_index`
from the embedded `InclusionProof`, and must be at least 1 (0 is reserved for
`null_entry`).  `CertificateValidator` verifies this before computing the leaf
hash, so a certificate with a mismatched serial number fails validation even if
the inclusion proof path itself is mathematically valid.

---

## Key Design Decisions

| Decision | Rationale |
|----------|-----------|
| Lifetime-tied borrowed types | Prevents use-after-free without `unsafe`; zero-copy parse |
| Backpatching encoder | Avoids two-pass encoding; computes length after writing content |
| `SmallVec` for Integer/OID | Covers common sizes inline; avoids heap allocation on hot paths |
| Lazy `Sequence<'a>` | O(1) overhead; decodes only accessed fields |
| `RawDer<'a>` for ANY fields | Enables lazy decode of expensive structures (e.g., extensions) |
| Trait-based `CryptoOps` | Crypto-agnostic path validation; swap OpenSSL for NSS without recompile |
| Feature-gated derive | `synta-derive` is optional at the crate level; avoids proc-macro overhead in `no_std` |
| 16-byte `SyntaByteArray` | Fits in two 64-bit registers; borrowed/owned flag as `u32` (no padding) |
| `OnceLock` per Python field | Lazy decode on first access; `#[pyclass(frozen)]` eliminates borrow flag |
| `OwnedStore` for trust anchors | Heap-allocates CA cert DER at construction time; `Box<[u8]>` heap-stability makes `&'static [u8]` safe; pre-parses once — zero CA re-parsing per TLS handshake |
| Module-level feature gating in `synta-certificate` | `openssl_backend` and `nss_backend` are declared at the top of `lib.rs` under `#[cfg]` attributes; only the active backend compiles, preventing dead-code bloat and link-time errors from unused FFI symbols |
| OpenSSL takes priority over NSS | When both `openssl` and `nss` features are enabled together, the NSS backend module is excluded (`#[cfg(all(feature = "nss", not(feature = "openssl")))]`); this ensures a deterministic single-backend build |

---

## Crypto Backend Feature Gating

The two crypto backends in `synta-certificate` are mutually exclusive at the
module level.  The declarations in `lib.rs` are:

```rust
// NSS backend: active only when nss is enabled and openssl is not.
#[cfg(all(feature = "nss", not(feature = "openssl")))]
pub mod nss_backend;

// OpenSSL backend: active whenever openssl is enabled, regardless of nss.
#[cfg(feature = "openssl")]
pub mod openssl_backend;
```

**Priority rule:** when both `openssl` and `nss` feature flags are enabled
simultaneously (e.g. a downstream crate enables both), the OpenSSL backend wins
and the NSS backend module is not compiled at all.  Only one backend is ever
active in a given build.

This gating is propagated transitively: `synta-python`, `synta-mtc`,
`synta-x509-verification`, and `synta-ffi` each forward the `openssl` and `nss`
feature flags to `synta-certificate` in their `Cargo.toml`.

The table below summarises which backend is compiled under common flag
combinations:

| Features enabled | Active backend |
|------------------|----------------|
| `openssl` only (default) | OpenSSL |
| `nss` only | NSS |
| `openssl` + `nss` | OpenSSL (NSS excluded) |
| neither | No crypto backend; signing/verification returns `PrivateKeyError` |

---

## PKCS#11 Key Loading Architecture

The PKCS#11 key-loading path in `synta-certificate` has two independent
implementations — one for each supported crypto backend — that share the same
public `BackendPrivateKey::from_pkcs11_uri` entry point.

### OpenSSL backend sequence

```mermaid
sequenceDiagram
    participant Caller
    participant BPK as BackendPrivateKey
    participant Store as OSSL_STORE_open_ex
    participant Provider as pkcs11-provider
    participant HSM as PKCS#11 Token

    Caller->>BPK: from_pkcs11_uri("pkcs11:token=…;object=…")
    BPK->>Store: OSSL_STORE_open_ex(uri, libctx, …)
    Store->>Provider: PKCS11_open(uri)
    Provider->>HSM: C_FindObjects (CKA_CLASS=CKO_PRIVATE_KEY)
    HSM-->>Provider: CK_OBJECT_HANDLE
    Provider-->>Store: EVP_PKEY (token reference)
    Store-->>BPK: EVP_PKEY cached in BackendPrivateKey::pkey
    Note over BPK: Key material never extracted
    BPK-->>Caller: Ok(BackendPrivateKey)

    Caller->>BPK: sign(data, algorithm)
    BPK->>Provider: EVP_DigestSign (uses cached EVP_PKEY)
    Provider->>HSM: C_Sign (key stays on token)
    HSM-->>Provider: signature bytes
    Provider-->>BPK: signature
    BPK-->>Caller: Ok(signature)
```

**Setup requirement:** `pkcs11-provider` must be registered in the process's
OpenSSL configuration (via `OPENSSL_CONF` pointing to an `openssl.cnf` that
loads the provider) before `from_pkcs11_uri` is called.  The provider is
looked up through the process-default libctx passed to `OSSL_STORE_open_ex`.

### NSS backend sequence

```mermaid
sequenceDiagram
    participant Caller
    participant BPK as BackendPrivateKey
    participant Slot as PK11_FindSlotByName
    participant List as PK11_ListPrivKeysInSlot
    participant Auth as PK11_Authenticate
    participant HSM as PKCS#11 Token

    Caller->>BPK: from_pkcs11_uri("pkcs11:token=MyHSM;object=cakey")
    BPK->>Slot: PK11_FindSlotByName("MyHSM")
    Slot-->>BPK: PK11SlotInfo*
    BPK->>Auth: PK11_Authenticate(slot, pin-value)
    Note over Auth: Only called if pin-value= is present
    Auth-->>BPK: SECSuccess
    BPK->>List: PK11_ListPrivKeysInSlot(slot, "cakey", …)
    List-->>BPK: SECKEYPrivateKey* (handle)
    Note over BPK: SPKI extracted from public key at load time;<br/>private key handle not retained between calls
    BPK-->>Caller: Ok(BackendPrivateKey)

    Caller->>BPK: sign(data, algorithm)
    BPK->>HSM: SEC_SignData / PK11_Sign (RSA/ECDSA/ML-DSA/Ed25519)
    HSM-->>BPK: signature bytes
    BPK-->>Caller: Ok(signature)
```

**Setup requirement:** The PKCS#11 module must be registered in the NSS
secmod database (via `modutil`) before `from_pkcs11_uri` is called.  Both
`token=` and `object=` URI attributes are mandatory for the NSS backend.

### Key design decisions for PKCS#11 key loading

| Decision | Rationale |
|----------|-----------|
| `PK11_ListPrivKeysInSlot` instead of `PK11_FindPrivateKeyFromNickname` | `PK11_FindPrivateKeyFromNickname` is declared in `pk11priv.h` (NSS internal header) and is not exported from `libnss3.so`; `PK11_ListPrivKeysInSlot` is in `pk11pub.h` and exported since NSS 3.4 |
| SPKI extracted at load time (NSS) | Extracting the Subject Public Key Info once at load avoids repeated token round-trips during certificate building; the SPKI is stored alongside the key handle |
| `pkcs11` field stores raw URI and pre-decoded attributes | Storing `Pkcs11Uri { raw, attrs }` avoids re-parsing the URI on every access and keeps the original string available for logging and diagnostics |
| `EVP_PKEY` cached in `BackendPrivateKey::pkey` (OpenSSL) | The token reference is valid for the lifetime of the `BackendPrivateKey`; caching avoids reopening the store on every signing call |

---

## PKCS#11 Management Architecture

The `pkcs11-mgmt` feature in `synta-certificate` adds a backend-independent
token management layer on top of the existing per-backend key-loading paths.
Where the key-loading path (above) is backend-specific — OpenSSL uses
`OSSL_STORE_open_ex`; NSS uses `PK11_ListPrivKeysInSlot` — the management
layer talks to PKCS#11 tokens directly through the `cryptoki` crate, which
dlopen()s the module at runtime.

### Feature activation

`pkcs11-mgmt` is automatically enabled whenever the `openssl` or `nss` feature
is active.  A `compile_error!` fires if `pkcs11-mgmt` is requested without
either crypto backend:

```
openssl  ─┐
           ├─ auto-enables ─► pkcs11-mgmt  ─►  cryptoki 0.12 (dlopen)
nss      ─┘
```

### Component overview

```mermaid
graph TD
    pm["Pkcs11Manager\n(synta_certificate::pkcs11_mgmt)"]
    tm["TokenManager trait\n(synta_certificate::crypto::token_manager)"]
    si["SlotInfo / Pkcs11KeyInfo\n(data types, re-exported from crate root)"]
    cf["cryptoki 0.12\n(dynamic feature — dlopen)"]
    so["PKCS#11 module\n(.so / .dll loaded at runtime)"]
    pymod["synta.pkcs11\n(Python submodule — synta-python/src/pkcs11.rs)"]
    conv["list_pkcs11_slots()\npkcs11_manager()\n(convenience fns, crate root)"]

    pm -- "implements" --> tm
    pm --> cf
    cf --> so
    conv --> pm
    pymod --> pm
    tm --> si
```

### Session lifecycle

Each call to a `TokenManager` method follows an open-authenticate-operate-close
cycle.  `Pkcs11Manager` holds no persistent session between calls:

```mermaid
sequenceDiagram
    participant Caller
    participant Mgr as Pkcs11Manager
    participant Cky as cryptoki
    participant Token as PKCS#11 Token

    Caller->>Mgr: list_keys("MySoftHSM2Token0", Some("1234"))
    Mgr->>Cky: C_OpenSession(slot_id, CKF_SERIAL_SESSION)
    Cky->>Token: open session
    Token-->>Cky: CK_SESSION_HANDLE
    Mgr->>Cky: C_Login(USER, pin)
    Cky->>Token: authenticate
    Token-->>Cky: CKR_OK
    Mgr->>Cky: C_FindObjects (CKO_PRIVATE_KEY)
    Cky->>Token: enumerate keys
    Token-->>Cky: key handles
    Mgr->>Cky: C_GetAttributeValue (CKA_LABEL, CKA_ID, CKA_KEY_TYPE, ...)
    Cky-->>Mgr: attribute values → Vec<Pkcs11KeyInfo>
    Mgr->>Cky: C_CloseSession
    Mgr-->>Caller: Ok(Vec<Pkcs11KeyInfo>)
```

### Key design decisions for PKCS#11 management

| Decision | Rationale |
|----------|-----------|
| Single `cryptoki`-based management layer (not per-backend) | Both OpenSSL and NSS can use their native PKCS#11 paths for signing, but neither exposes a uniform API for slot enumeration or key generation; `cryptoki` provides a cross-backend management surface without duplicating logic in each backend |
| `cryptoki` `dynamic` feature (dlopen) | Avoids a hard link-time dependency on a specific PKCS#11 module; the module path is resolved at runtime from the URI, environment, or candidate system paths |
| RSA key length checked before opening a session | Rejecting keys shorter than 2048 bits early avoids the overhead of opening and authenticating a session only to have the token reject the key spec |
| `Send + Sync` on `Pkcs11Manager` | Allows the manager to be shared across threads (e.g. in a web service that serves multiple concurrent PKCS#11 operations); each call opens its own session, so there is no shared mutable session state |
| `pin_value` is `pub(crate)` with accessor methods | Prevents accidental PIN leakage through derived `Debug` or `Display` impls; `__repr__` in Python always shows `pin-value=***` |

### Python submodule registration

`synta.pkcs11` is registered inside `_synta.so` and imported conditionally in
`python/synta/__init__.py`:

```python
try:
    from synta._synta import pkcs11   # registers synta.pkcs11
except ImportError:
    pass
pkcs11 = _sys.modules.get("synta.pkcs11")  # None if feature absent
```

When the feature is absent the import silently fails and `synta.pkcs11`
remains `None`, so downstream code can check `if synta.pkcs11 is not None`
rather than catching `ImportError`.

---


## See Also

- [Codebase Summary]codebase-summary.md — file inventory and key dependency list
- [Limitations]limitations.md — unsupported ASN.1 constructs
- [Contributing]contribution.md — development setup and CI workflow
- [API Reference]api-reference.md — generated rustdoc