synta 0.2.2

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
# API Reference for Rust Crates

<style>
  /* Remove content padding and width cap so the iframe fills edge-to-edge */
  .content main { padding: 0 !important; max-width: none !important; }
</style>

<iframe
  src="api/synta/index.html"
  style="width:100%; height:calc(100vh - var(--menu-bar-height, 50px)); border:none; display:block;"
  title="Synta Rust API Reference (rustdoc)">
</iframe>

> If the frame above is empty, build the documentation first:
> ```bash
> bash contrib/ci/local-ci.sh doc
> ```

---

## PKCS#11 URI Key Loading

The functions and types described below are part of `synta-certificate` and
allow loading private keys directly from PKCS#11 hardware or software tokens
without ever extracting key material into process memory.

### `BackendPrivateKey::from_pkcs11_uri`

```rust
pub fn from_pkcs11_uri(uri: &str) -> Result<Self, PrivateKeyError>
```

Loads a private key from a PKCS#11 token identified by the given RFC 7512
`pkcs11:` URI.  The URI must begin with the `pkcs11:` scheme.

The key handle (or, for the NSS backend, the slot reference) is stored inside
the returned `BackendPrivateKey`.  Key material is never extracted; all signing
operations are delegated to the token.

**When to use it:** Use `from_pkcs11_uri` instead of `from_pem` or
`from_pkcs8_der` whenever the private key resides in an HSM, smart card, or
software token and must not leave the token boundary.

**Backend-specific requirements:**

| Backend | Mandatory URI attributes | Prerequisites |
|---------|--------------------------|--------------|
| OpenSSL (`openssl` feature) | None | `pkcs11-provider` configured via `OPENSSL_CONF` before the call |
| NSS (`nss` feature) | `token=` and `object=` | PKCS#11 module registered via `modutil` before the call |

See [deployment-guide.md](deployment-guide.md#hsm--pkcs11-key-storage) for
step-by-step setup instructions for each backend.

### `BackendPrivateKey::pkcs11_info`

```rust
pub fn pkcs11_info(&self) -> Option<&Pkcs11Uri>
```

Returns the parsed PKCS#11 URI for keys loaded via `from_pkcs11_uri`, or
`None` for software keys loaded from PEM/DER.

Useful for logging, diagnostics, or conditional logic that must behave
differently for HSM-backed keys.

### `Pkcs11Uri`

```rust
pub struct Pkcs11Uri {
    pub raw: String,              // verbatim URI string as supplied to from_pkcs11_uri
    pub attrs: Pkcs11UriAttributes,
}
```

Parsed representation of an RFC 7512 `pkcs11:` URI.  The `raw` field preserves
the original string exactly; `attrs` contains the pre-decoded path and query
attributes.

### `Pkcs11UriAttributes`

```rust
pub struct Pkcs11UriAttributes {
    pub token:     Option<String>,   // token= path attribute (slot/token label)
    pub object:    Option<String>,   // object= path attribute (key nickname/label)
    pub id:        Option<Vec<u8>>,  // id= CKA_ID, percent-decoded raw bytes
    pub pin_value: Option<String>,   // ?pin-value= query attribute (token PIN)
}
```

Pre-decoded path and query attributes extracted from a `pkcs11:` URI.

### URI format example

```
pkcs11:token=MyHSM;object=cakey;type=private?pin-value=1234
```

- `token=MyHSM` — the token label (mandatory for NSS, optional for OpenSSL)
- `object=cakey` — the key label / nickname (mandatory for NSS, optional for OpenSSL)
- `type=private` — selects the private-key object class (recommended)
- `pin-value=1234` — PIN supplied inline; omit if the token is already authenticated

### Note on key material

`BackendPrivateKey::pkcs8_der` is always empty for HSM-backed keys.  The token
handle is stored internally and used for every signing operation.  Callers
should never attempt to serialise an HSM key via `pkcs8_der`.

---

## Python PKCS#11 Token Management

The `synta.pkcs11` submodule provides Python-level access to PKCS#11 token
management: listing slots, enumerating keys, generating key pairs on the token,
deleting keys, and querying whether a specific key exists.  It is built on top
of the `TokenManager` trait in `synta-certificate` (feature `pkcs11-mgmt`).

For the underlying architecture see
[system-architecture.md](system-architecture.md#pkcs11-management-architecture).

### Availability

`synta.pkcs11` is present only when `synta-python` is compiled with the
`pkcs11-mgmt` feature, which is automatically enabled by the `openssl` and
`nss` features.  If the feature is absent the module is `None`:

```python
import synta
pkcs11 = synta.pkcs11  # None when pkcs11-mgmt feature is absent
```

Tests that require the submodule should guard with:

```python
pkcs11 = pytest.importorskip("synta.pkcs11", reason="pkcs11-mgmt feature not compiled in")
```

### `synta.pkcs11.list_slots`

```text
list_slots(module: str | None = None) -> list[SlotInfo]
```

Returns all PKCS#11 slots reported by the loaded module.  If `module` is
`None`, the module path is resolved in order: `PKCS11_MODULE_PATH` environment
variable, then candidate system paths (p11-kit-proxy.so on Fedora/RHEL and
Debian/Ubuntu variants).  Pass an absolute path to `module` to bypass
auto-detection.

Raises `ValueError` if `module` is a relative path, if the resolved path does
not exist, or if the PKCS#11 module fails to initialize.

### `synta.pkcs11.SlotInfo`

Frozen pyclass returned by `list_slots()`.

| Attribute | Type | Description |
|-----------|------|-------------|
| `slot_id` | `int` | `CK_SLOT_ID` — token slot identifier |
| `token_label` | `str` | Token label with trailing spaces stripped |
| `manufacturer_id` | `str` | Manufacturer identifier |
| `model` | `str` | Token model string |
| `serial_number` | `str` | Token serial number |
| `flags` | `int` | Subset of `CKF_*` bits: `0x0002` write-protected, `0x0004` login required, `0x0100` protected authentication path, `0x0400` token initialized |

### `synta.pkcs11.KeyInfo`

Frozen pyclass returned by `Pkcs11Token.list_keys()`.

| Attribute | Type | Description |
|-----------|------|-------------|
| `label` | `str` | `CKA_LABEL` |
| `id` | `bytes` | `CKA_ID` raw bytes; may be empty |
| `key_type` | `str` | `"RSA"`, `"EC"`, `"Ed"`, `"ML-DSA"`, `"ML-KEM"`, or `"Unknown"` |
| `key_bits` | `int` | RSA modulus bits; `0` for non-RSA types |

### `synta.pkcs11.Pkcs11Token`

```text
Pkcs11Token(uri: str, module: str | None = None) -> Pkcs11Token
```

Creates a token handle.  `uri` must be a `pkcs11:` URI (RFC 7512); raises
`ValueError` if the URI scheme is wrong or the module cannot be loaded.
`module` follows the same resolution rules as `list_slots`.

Each call to a method on `Pkcs11Token` opens its own PKCS#11 session,
authenticates if `pin-value=` is present in the URI, performs the operation,
and closes the session.  The `Pkcs11Token` object itself holds no open
session between calls.

`__repr__` always redacts the PIN: `pin-value=***`.

#### `Pkcs11Token.find_key`

```text
find_key(object_uri: str) -> bool
```

Returns `True` if a private-key object matching `object_uri` exists in the
token, `False` otherwise.  `object_uri` must be a valid `pkcs11:` URI.

#### `Pkcs11Token.list_keys`

```text
list_keys() -> list[KeyInfo]
```

Returns all private keys visible to the authenticated session.  The token's
`pin-value=` from the constructor URI is used to authenticate.

#### `Pkcs11Token.delete_key`

```text
delete_key(object_uri: str) -> None
```

Deletes the private key (and its matching public key object, if any) identified
by `object_uri`.  Raises `ValueError` if the key is not found or deletion
fails.

#### `Pkcs11Token.generate_key_pair`

```text
generate_key_pair(
    key_type: str,
    param: int | str,
    label: str,
    extractable: bool = False,
) -> synta.PrivateKey
```

Generates a key pair on the token and returns a `synta.PrivateKey` handle
backed by the token object.

`key_type` and `param` combinations:

| `key_type` | `param` | Notes |
|------------|---------|-------|
| `"rsa"` | `2048`, `3072`, or `4096` | Keys shorter than 2048 bits are rejected before opening a session |
| `"ec"` | `"P-256"`, `"P-384"`, or `"P-521"` | |
| `"ed25519"` | ignored | |
| `"ed448"` | ignored | |
| `"ml-dsa"` | `"ML-DSA-44"`, `"ML-DSA-65"`, or `"ML-DSA-87"` | Requires PKCS#11 v3.2 |
| `"ml-kem"` | `"ML-KEM-512"`, `"ML-KEM-768"`, or `"ML-KEM-1024"` | Requires PKCS#11 v3.2 |

`label` sets the `CKA_LABEL` attribute on both the private and public key
objects.  `extractable` controls `CKA_EXTRACTABLE`; the default `False` keeps
the private key material confined to the token.

### Usage example

```python
import synta.pkcs11 as pkcs11

# Enumerate tokens visible through the system p11-kit proxy
slots = pkcs11.list_slots()
for s in slots:
    print(s.slot_id, s.token_label, s.manufacturer_id)

# Manage a specific token identified by a PKCS#11 URI
token = pkcs11.Pkcs11Token("pkcs11:token=MySoftHSM2Token0?pin-value=1234")

# Generate an RSA-4096 key pair; key material stays on the token
key = token.generate_key_pair("rsa", 4096, "caKey")           # -> synta.PrivateKey

# Generate an EC key pair on curve P-256
key = token.generate_key_pair("ec", "P-256", "ecKey")         # -> synta.PrivateKey

# Check whether a specific key object exists
assert token.find_key("pkcs11:token=MySoftHSM2Token0;object=caKey")

# List all keys on the token
for k in token.list_keys():
    print(k.label, k.key_type, k.key_bits)

# Delete a key object
token.delete_key("pkcs11:token=MySoftHSM2Token0;object=caKey")

# Use an explicit module path instead of auto-detection
token = pkcs11.Pkcs11Token(
    "pkcs11:token=MySoftHSM2Token0?pin-value=1234",
    module="/usr/lib64/pkcs11/libsofthsm2.so",
)
```

### Rust API (`synta_certificate``pkcs11-mgmt` feature)

The Python classes mirror the Rust types re-exported from the crate root:

```rust
// Enumerate slots without constructing a Pkcs11Manager
pub fn list_pkcs11_slots() -> Result<Vec<SlotInfo>, PrivateKeyError>

// Obtain a default manager (resolves module from env / system paths)
pub fn pkcs11_manager() -> Result<impl TokenManager, PrivateKeyError>
```

`Pkcs11Manager` can also be constructed explicitly:

```rust
// Load a specific module by absolute path
let mgr = Pkcs11Manager::new("/usr/lib64/pkcs11/libsofthsm2.so")?;

// Resolve module from URI's module-path attr, env var, or candidate paths
let uri = Pkcs11Uri::parse("pkcs11:token=MySoftHSM2Token0?pin-value=1234")?;
let mgr = Pkcs11Manager::from_uri(&uri)?;

// Resolve from env var / candidate paths only
let mgr = Pkcs11Manager::from_env()?;
```

`Pkcs11Manager` is `Send + Sync`.  The `TokenManager` trait methods are:

```rust
pub trait TokenManager {
    fn list_slots(&self) -> Result<Vec<SlotInfo>, PrivateKeyError>;
    fn find_key(&self, uri: &Pkcs11Uri) -> Result<bool, PrivateKeyError>;
    fn list_keys(&self, token_name: &str, pin: Option<&str>) -> Result<Vec<Pkcs11KeyInfo>, PrivateKeyError>;
    fn delete_key(&self, uri: &Pkcs11Uri) -> Result<(), PrivateKeyError>;
    fn generate_key_pair_in_token(
        &self,
        spec: &KeySpec,
        uri: &Pkcs11Uri,
        extractable: bool,
    ) -> Result<BackendPrivateKey, PrivateKeyError>;
}
```

Module path resolution order for `Pkcs11Manager::from_uri` and `from_env`:
1. `module-path` attribute in the PKCS#11 URI
2. `PKCS11_MODULE_PATH` environment variable
3. Candidate system paths probed in order (first existing path wins):
   `/usr/lib64/pkcs11/p11-kit-proxy.so`,
   `/usr/lib/x86_64-linux-gnu/pkcs11/p11-kit-proxy.so`,
   `/usr/lib/aarch64-linux-gnu/pkcs11/p11-kit-proxy.so`,
   `/usr/lib/powerpc64le-linux-gnu/pkcs11/p11-kit-proxy.so`,
   `/usr/lib/s390x-linux-gnu/pkcs11/p11-kit-proxy.so`,
   `/usr/lib/pkcs11/p11-kit-proxy.so`

---

## RSA Key Transport

`BackendPublicKey` and `BackendPrivateKey` in `synta-certificate` expose
RSA encryption and decryption operations.  These are used in CMS
`EnvelopedData` construction (key transport RecipientInfo) and in any other
protocol that requires wrapping a symmetric key with an RSA public key.

Both backends (OpenSSL and NSS) are supported.  When both features are enabled,
OpenSSL takes priority.

### `BackendPublicKey::rsa_oaep_encrypt`

```rust
pub fn rsa_oaep_encrypt(
    &self,
    plaintext: &[u8],
    hash_alg: &str,
) -> Result<Vec<u8>, PrivateKeyError>
```

RSA-OAEP encryption using the recipient's public key.

`hash_alg` is the OAEP hash algorithm name: `"sha1"`, `"sha224"`,
`"sha256"`, `"sha384"`, or `"sha512"`.

Returns the ciphertext (same byte-length as the RSA modulus).

### `BackendPublicKey::rsa_pkcs1v15_encrypt`

```rust
pub fn rsa_pkcs1v15_encrypt(
    &self,
    plaintext: &[u8],
) -> Result<Vec<u8>, PrivateKeyError>
```

RSA PKCS#1 v1.5 encryption using the recipient's public key.  For new
protocols prefer RSA-OAEP; PKCS#1 v1.5 is supported for compatibility with
legacy CMS content.

Returns the ciphertext (same byte-length as the RSA modulus).

### `BackendPrivateKey::rsa_oaep_decrypt`

```rust
pub fn rsa_oaep_decrypt(
    &self,
    ciphertext: &[u8],
    hash_alg: &str,
) -> Result<Vec<u8>, PrivateKeyError>
```

RSA-OAEP decryption using the recipient's private key.

`hash_alg` must match the algorithm used during encryption.

### `BackendPrivateKey::rsa_pkcs1v15_decrypt`

```rust
pub fn rsa_pkcs1v15_decrypt(
    &self,
    ciphertext: &[u8],
) -> Result<Vec<u8>, PrivateKeyError>
```

RSA PKCS#1 v1.5 decryption using the recipient's private key.

**Backend notes:**

| Backend | Implementation |
|---------|----------------|
| OpenSSL (`openssl` feature) | Uses `EVP_PKEY_CTX_set_rsa_padding` / `EVP_PKEY_decrypt` |
| NSS (`nss` feature, no `openssl`) | Imports the public key via `SECKEY_DecodeDERSubjectPublicKeyInfo` and the private key via `PK11_ImportDERPrivateKeyInfoAndReturnKey` into the NSS internal softokn slot, then calls `PK11_PubEncrypt` / `PK11_PrivDecrypt` |

---

## AES-GCM Authenticated Encryption

Both the Rust `BlockCipherProvider` trait and the Python `synta.crypto` module expose
AES-GCM (AEAD) encryption and decryption. Both backends (OpenSSL and NSS) are supported.

### `BlockCipherProvider::aes_gcm_encrypt` (Rust)

```rust
fn aes_gcm_encrypt(
    &self,
    key: &[u8],
    nonce: &[u8],
    plaintext: &[u8],
    aad: &[u8],
) -> Result<Vec<u8>, Self::Error>
```

Encrypts `plaintext` with AES-GCM. `key` must be 16, 24, or 32 bytes (AES-128,
AES-192, or AES-256). `nonce` must be exactly 12 bytes. `aad` is additional
authenticated data that is authenticated but not encrypted; pass `&[]` when not
needed.

Returns `ciphertext ‖ tag` where the authentication tag is 16 bytes.

### `BlockCipherProvider::aes_gcm_decrypt` (Rust)

```rust
fn aes_gcm_decrypt(
    &self,
    key: &[u8],
    nonce: &[u8],
    ciphertext_with_tag: &[u8],
    aad: &[u8],
) -> Result<Vec<u8>, Self::Error>
```

Decrypts and verifies `ciphertext_with_tag` (the output of `aes_gcm_encrypt`).
The last 16 bytes of `ciphertext_with_tag` are treated as the authentication tag.
Returns `Err` if the tag does not verify or the inputs are otherwise invalid.

Obtain the provider via `default_block_cipher_provider()` for backend-agnostic code.

### `synta.crypto.aes_gcm_encrypt` (Python)

```text
synta.crypto.aes_gcm_encrypt(
    key: bytes,
    nonce: bytes,
    plaintext: bytes,
    aad: bytes | None = None,
) -> bytes
```

Python wrapper around `BlockCipherProvider::aes_gcm_encrypt`. `key` is 16, 24, or
32 bytes; `nonce` is 12 bytes. `aad` defaults to `None` (no AAD). Returns
`ciphertext ‖ tag` (16-byte tag). Output is byte-for-byte compatible with
`cryptography.hazmat.primitives.ciphers.aead.AESGCM(key).encrypt(nonce, plaintext, aad)`.

Raises `ValueError` on invalid key length, nonce length, or backend error.

### `synta.crypto.aes_gcm_decrypt` (Python)

```text
synta.crypto.aes_gcm_decrypt(
    key: bytes,
    nonce: bytes,
    ciphertext_with_tag: bytes,
    aad: bytes | None = None,
) -> bytes
```

Decrypts and verifies the output of `aes_gcm_encrypt`. `nonce` and `aad` must
match those used during encryption. Raises `ValueError` if the authentication tag
does not verify.

---

## Importing Private Keys from Raw Components

Two `BackendPrivateKey` constructors allow importing key material that is already
available as raw big-endian byte slices. Both require the `openssl` or `nss` feature
and are available from the Rust API only; there is no Python-level equivalent.

### `BackendPrivateKey::from_ec_private_scalar`

```rust
pub fn from_ec_private_scalar(
    d: &[u8],
    x: &[u8],
    y: &[u8],
    curve: &str,
) -> Result<Self, PrivateKeyError>
```

Builds an EC private key from the private scalar `d` and the affine public-point
coordinates `x` and `y` (all big-endian). `curve` must be `"P-256"`, `"P-384"`,
or `"P-521"`.

Returns `Err(PrivateKeyError)` if `curve` is unrecognised or if the backend rejects
the key material (e.g. the point is not on the curve).

### `RsaPrivateComponents`

```rust
pub struct RsaPrivateComponents<'a> {
    pub n:  &'a [u8],  // RSA modulus
    pub e:  &'a [u8],  // public exponent
    pub d:  &'a [u8],  // private exponent
    pub p:  &'a [u8],  // first prime factor
    pub q:  &'a [u8],  // second prime factor
    pub dp: &'a [u8],  // d mod (p - 1)
    pub dq: &'a [u8],  // d mod (q - 1)
    pub qi: &'a [u8],  // q^{-1} mod p
}
```

Helper struct that groups the eight CRT fields required to import an RSA private key.
All fields are big-endian unsigned integers, the same encoding used by JWK and PKCS#1.

### `BackendPrivateKey::from_rsa_private_components`

```rust
pub fn from_rsa_private_components(
    components: &RsaPrivateComponents<'_>,
) -> Result<Self, PrivateKeyError>
```

Builds an RSA private key from the fields supplied in `components`. Returns
`Err(PrivateKeyError)` if the backend rejects the key material (e.g. inconsistent
CRT values).

---

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

The types below are defined in `synta-mtc` and generated from `asn1/MTC.asn1`.
All types carry a lifetime parameter `'a` tied to the input buffer (zero-copy
borrowed fields).

### `ProofNode`

```rust
pub type ProofNode = OctetString;
```

A single hash value in an inclusion path (spec §4.3.2).  `ProofNode` is a plain
`OCTET STRING` containing the sibling hash at one level of the tree.  There are
no direction bits; direction is determined from the leaf index at verification
time.

### `InclusionProof`

```rust
pub struct InclusionProof {
    pub log_entry_index: Integer,
    pub tree_size:       Integer,
    pub subtree_start:   Integer,
    pub subtree_end:     Integer,
    pub inclusion_path:  Vec<ProofNode>,
}
```

Per spec §6.1 MTCProof.  `subtree_start` and `subtree_end` bound the subtree
attested by the accompanying cosignature (`[subtree_start, subtree_end)`).
`log_entry_index` is the absolute leaf position in the full log; this must equal
the `serialNumber` field of the enclosed `TBSCertificate` (see
`verify_serial_number` below).

### `TBSCertificateLogEntry`

```rust
pub struct TBSCertificateLogEntry<'a> {
    pub version:                      Option<Integer>,          // [0] EXPLICIT, DEFAULT 0
    pub issuer:                       Name<'a>,
    pub validity:                     Validity<'a>,
    pub subject:                      Name<'a>,
    pub subject_public_key_algorithm: AlgorithmIdentifier<'a>,
    pub subject_public_key_info_hash: OctetString,              // SHA-256 of SubjectPublicKeyInfo
    pub issuer_unique_id:             Option<BitStringRef<'a>>, // [1] EXPLICIT
    pub subject_unique_id:            Option<BitStringRef<'a>>, // [2] EXPLICIT
    pub extensions:                   Option<RawDer<'a>>,       // [3] EXPLICIT
}
```

Fields changed relative to earlier drafts:
- `subject_public_key_info_hash` (was `subject_public_key_hash`)
- `extensions` is at tag `[3]` (was `[2]`)
- `version` is a new optional field at tag `[0] EXPLICIT INTEGER DEFAULT 0`

### `CosignerID`

```rust
pub type CosignerID<'a> = TrustAnchorID<'a>;

pub struct TrustAnchorID<'a> {
    pub hash_algorithm: AlgorithmIdentifier<'a>,
    pub public_key:     SubjectPublicKeyInfo<'a>,
}
```

`CosignerID` mirrors `LogID`: a cosigner is identified by a hash algorithm and
its public key.  The previous structure (`{ issuer: Name, serialNumber:
INTEGER }`) was replaced by this `TrustAnchorID`-style identity.

### `verify_subtree_inclusion_proof`

```rust
pub fn verify_subtree_inclusion_proof(
    algorithm: HashAlgorithm,
    index: u64,
    start: u64,
    end: u64,
    leaf_hash: &[u8],
    proof_path: &[Vec<u8>],
    expected_subtree_hash: &[u8],
) -> Result<()>
```

Verifies a compact proof path against a specific subtree root hash (spec
§4.3.3).  Translates the absolute `index` to a subtree-relative position
(`index - start`) and delegates to `verify_inclusion_proof`.  Use this instead
of `verify_subtree_consistency` when only a proof path (not all leaf hashes) is
available.  Exported from `synta_mtc::crypto`.

### Serial number enforcement

Per spec §6.1, the `TBSCertificate.serialNumber` field of a Merkle Tree
Certificate must equal its `log_entry_index` and must be at least 1 (0 is
reserved for `null_entry`).  The `CertificateValidator` checks this constraint
automatically as part of both `validate_standalone` and `validate_landmark`.