synta 0.2.5

ASN.1 parser, decoder, and encoder library with DER/BER support and C FFI
Documentation
# PKCS Loaders


Five top-level functions extract X.509 certificates and/or PKCS#8 private keys from
container formats (PKCS#7, PKCS#12). All functions accept DER, BER, or CER input;
the encoding is detected automatically.

## Functions

### load_der_pkcs7_certificates

```python
load_der_pkcs7_certificates(data: bytes) -> list[Certificate]
```
Parse a DER or BER PKCS#7 SignedData blob and return its embedded certificates.
BER indefinite-length encodings (common in PKCS#7 files from older tools) are handled
transparently. Non-certificate entries in the CertificateSet are silently skipped.

Raises `ValueError` if the outer ContentInfo contentType is not id-signedData, or on any
ASN.1 structural error.

### load_pem_pkcs7_certificates

```python
load_pem_pkcs7_certificates(data: bytes) -> list[Certificate]
```
Decode PEM block(s) in `data` (any label accepted: `PKCS7`, `CMS`, `CERTIFICATE`, etc.),
then extract certificates from each PKCS#7 payload. All certificates across all blocks are
returned as a single flat list.

Raises `ValueError` if no PEM block is found, or if any block fails to parse as PKCS#7
SignedData.

### load_pkcs12_certificates

```python
load_pkcs12_certificates(data: bytes, password: bytes = None) -> list[Certificate]
```
Extract certificates from a PKCS#12 / PFX archive. Non-certificate bag types
(`keyBag`, `pkcs8ShroudedKeyBag`, `crlBag`, `secretBag`) are silently skipped.

`password` is the archive password as raw bytes (UTF-8, no NUL terminator). Pass `b""`
or omit for no-password archives.

Encrypted bags are supported when built with the `openssl` Cargo feature
(PBES2/PBKDF2/AES-256-CBC) and optionally `deprecated-pkcs12-algorithms` (legacy 3DES).
Without the `openssl` feature a `ValueError` is raised when any encrypted bag is
encountered.

### load_pkcs12_keys

```python
load_pkcs12_keys(data: bytes, password: bytes = None) -> list[bytes]
```
Extract PKCS#8 private keys from a PKCS#12 archive. Returns a list of raw DER-encoded
`OneAsymmetricKey` blobs, one per `keyBag` (unencrypted) and `pkcs8ShroudedKeyBag`
(decrypted with the `openssl` feature) entry. `certBag`, `crlBag`, and `secretBag`
entries are silently skipped.

### load_pkcs12

```python
load_pkcs12(data: bytes, password: bytes = None) -> tuple[list[Certificate], list[bytes]]
```
Extract both certificates and private keys from a PKCS#12 archive in a single call.
Returns `(certs, keys)` where `certs` is a `list[Certificate]` and `keys` is a `list[bytes]`
of DER-encoded PKCS#8 structures.

### create_pkcs12

```python
create_pkcs12(
    certificates: list[Certificate | bytes],
    private_key: PrivateKey | bytes | None = None,
    password: bytes | None = None,
    cipher: str | None = None,
    mac_algorithm: str | None = None,
) -> bytes
```
Build a DER-encoded PKCS#12 PFX archive. Each element of `certificates` may be a
`Certificate` object or raw DER `bytes`; both types may appear in the same list.
`private_key` may be a `PrivateKey` object or DER-encoded PKCS#8 `bytes`.

**`cipher`** selects the symmetric cipher for private-key encryption inside the archive.
Accepted values:

| Value | Cipher | Notes |
|---|---|---|
| `"aes256"` | AES-256-CBC | Default |
| `"aes128"` | AES-128-CBC | |

Raises `ValueError` for unknown or unsupported values (e.g. `"3des"`).

**`mac_algorithm`** selects the HMAC algorithm for the integrity MAC and PBKDF2 PRF.
Accepted values:

| Value | Algorithm | Notes |
|---|---|---|
| `"sha256"` | HMAC-SHA-256 | Default |
| `"sha384"` | HMAC-SHA-384 | |
| `"sha512"` | HMAC-SHA-512 | |

Raises `ValueError` for unknown or unsupported values (e.g. `"sha1"`).

When `password` is `None` or omitted, the archive is written without encryption and
`cipher` / `mac_algorithm` have no effect. When `password` is provided and the library is
built with the `openssl` Cargo feature, bags are encrypted with PBES2/PBKDF2 using
600,000 iterations and the selected cipher and MAC algorithm. Without the `openssl`
feature, supplying a password raises `ValueError`.

Raises `TypeError` for unrecognised element types in `certificates` or for an
unrecognised `private_key` type.

## Usage

```python
import synta

# ── PKCS#7 SignedData (DER or BER) ───────────────────────────────────────────
# amazon-roots.p7b uses BER indefinite-length encoding (0x30 0x80…); both are
# handled transparently — no caller-visible difference.
data = open("bundle.p7b", "rb").read()
certs = synta.load_der_pkcs7_certificates(data)
for cert in certs:
    print(cert.subject)

# ── PKCS#7 SignedData (PEM-wrapped) ──────────────────────────────────────────
pem = open("bundle.pem", "rb").read()
certs = synta.load_pem_pkcs7_certificates(pem)

# ── PKCS#12 — certificates only ──────────────────────────────────────────────
data = open("archive.p12", "rb").read()
certs = synta.load_pkcs12_certificates(data)             # unencrypted
certs = synta.load_pkcs12_certificates(data, b"s3cr3t")  # password-protected

print(f"found {len(certs)} certificate(s)")
for cert in certs:
    print(cert.subject, cert.not_before, "–", cert.not_after)

# ── PKCS#12 — private keys only ──────────────────────────────────────────────
# Returns a list[bytes] of DER-encoded OneAsymmetricKey (PKCS#8) structures.
keys = synta.load_pkcs12_keys(data, b"s3cr3t")
for key_der in keys:
    from cryptography.hazmat.primitives.serialization import load_der_private_key
    key = load_der_private_key(key_der, None)

# ── PKCS#12 — both certificates and keys in one call ─────────────────────────
certs, keys = synta.load_pkcs12(data, b"s3cr3t")
```

### Creating PKCS#12 Archives

```python
import synta

# ── Certificate objects directly ─────────────────────────────────────────────
certs = synta.load_pkcs12_certificates(open("src.p12", "rb").read())
pfx = synta.create_pkcs12(certs)                        # no password
open("bundle.p12", "wb").write(pfx)

# ── With a password (openssl feature required) ────────────────────────────────
try:
    pfx = synta.create_pkcs12(certs, password=b"s3cr3t")
except ValueError:
    # Raised when password is provided without the openssl Cargo feature
    pass

# ── With a cert and a PrivateKey object ──────────────────────────────────────
certs, keys = synta.load_pkcs12(open("src.p12", "rb").read())
key = synta.PrivateKey.from_der(keys[0])
pfx = synta.create_pkcs12(certs, private_key=key, password=b"s3cr3t")

# ── Custom cipher and MAC algorithm ──────────────────────────────────────────
pfx = synta.create_pkcs12(
    certs,
    private_key=key,
    password=b"s3cr3t",
    cipher="aes128",
    mac_algorithm="sha384",
)

# ── Raw DER bytes also accepted ───────────────────────────────────────────────
pfx = synta.create_pkcs12(
    [cert.to_der() for cert in certs],
    private_key=keys[0],           # bytes from load_pkcs12_keys
)

# ── Roundtrip verification ────────────────────────────────────────────────────
parsed_back = synta.load_pkcs12_certificates(pfx, None)
assert len(parsed_back) == len(certs)
```

**Encryption support:** when `password` is provided and the library is built with
`--features openssl`, bags are encrypted with PBES2/PBKDF2 using 600,000 iterations
(AES-256-CBC / HMAC-SHA-256 by default). Without the `openssl` feature, supplying a
non-`None` password raises `ValueError`.

See also [PKI Blocks](pki-blocks.md) for format-agnostic reading, and [PEM/DER](pem-der.md)
for PEM encode/decode helpers.