# 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
tools --> mtc
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:
| `_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 prepends a variable-length extensions field followed by a two-byte
`uint16` entry-type tag before the entry payload:
```text
| `null_entry` | `uint16(ext_len) \|\| ext_bytes \|\| [0x00, 0x00]` | Extensions prefix + two-byte type tag; no DER payload |
| `tbs_cert_entry` | `uint16(ext_len) \|\| ext_bytes \|\| [0x00, 0x01]` + DER contents | Extensions prefix + type tag + inner SEQUENCE contents (outer `0x30 <len>` stripped) |
The extensions bytes come from the `MTCProof.extensions` field when verifying
an X.509 wrapper certificate, or are empty (`ext_len = 0`) for entries without
extensions.
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 and extensions participate in the preimage and that
two entries with different types or extensions 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
| 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:
| `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
| `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
| 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