synta 0.2.0

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


`PublicKey` and `PrivateKey` are backend-agnostic key types that wrap OpenSSL keys. They
support RSA, EC (NIST curves), EdDSA (Ed25519 / Ed448), ML-DSA (FIPS 204), and ML-KEM
(FIPS 203). Both types are exported from the top-level `synta` package.

## PublicKey

### Construction

```python
PublicKey.from_pem(data: bytes) -> PublicKey
```
Load from PEM-encoded SubjectPublicKeyInfo.

```python
PublicKey.from_der(data: bytes) -> PublicKey
```
Load from DER-encoded SubjectPublicKeyInfo.

```python
PublicKey.from_rsa_components(n: bytes, e: bytes) -> PublicKey
```
Construct an RSA public key from big-endian modulus `n` and public exponent `e` bytes.
Raises `ValueError` if the inputs are not a valid RSA key.

```python
PublicKey.from_ec_components(x: bytes, y: bytes, curve: str) -> PublicKey
```
Construct an EC public key from affine coordinates `x`, `y` and NIST curve name.
`curve` must be `"P-256"`, `"P-384"`, or `"P-521"`. Raises `ValueError` for unknown curve
names or invalid coordinates.

### Serialisation

```python
pub_key.to_pem() -> bytes
pub_key.to_der() -> bytes
```

### Properties

| Property | Type | Description |
|---|---|---|
| `key_type` | `str` | Lowercase key algorithm: `"rsa"`, `"ec"`, `"ed25519"`, `"ed448"`, `"dsa"`, `"ml-dsa-44"`, `"ml-dsa-65"`, `"ml-dsa-87"`, `"ml-kem-512"`, `"ml-kem-768"`, `"ml-kem-1024"`, or `"unknown"` |
| `key_size` | `int \| None` | Key size in bits, or `None` for EdDSA and post-quantum keys |
| `modulus` | `bytes \| None` | RSA modulus `n` as big-endian bytes, or `None` for non-RSA keys |
| `public_exponent` | `bytes \| None` | RSA public exponent `e` as big-endian bytes, or `None` for non-RSA keys |
| `curve_name` | `str \| None` | NIST curve name for EC keys (e.g. `"P-256"`), or `None` |
| `x` | `bytes \| None` | EC affine X coordinate as big-endian bytes, or `None` for non-EC keys |
| `y` | `bytes \| None` | EC affine Y coordinate as big-endian bytes, or `None` for non-EC keys |

### Methods

```python
pub_key.rsa_oaep_encrypt(plaintext: bytes, hash_algorithm: str = "sha256") -> bytes
pub_key.rsa_pkcs1v15_encrypt(plaintext: bytes) -> bytes
pub_key.kem_encapsulate() -> tuple[bytes, bytes]   # ML-KEM: (ciphertext, shared_secret)
```

```python
pub_key.verify_signature(
    signature: bytes,
    data: bytes,
    algorithm: str | None = None,
    context: bytes | None = None,
) -> None
```
Verify a signature over `data`. Raises `ValueError` on failure.
- `algorithm`: hash name (e.g. `"sha256"`) for RSA and ECDSA. Pass `None` for Ed25519, Ed448,
  and ML-DSA keys.
- `context`: ML-DSA domain-separation string (FIPS 204); `None` = empty. Ignored for non-ML-DSA
  keys.

```python
pub_key.verify_certificate_signature(
    tbs_der: bytes,
    sig_alg_der: bytes,
    signature: bytes,
) -> None
```
Verify a certificate signature using a DER-encoded `AlgorithmIdentifier`. This is the
counterpart to `verify_signature` for the X.509 use case: instead of supplying a hash
algorithm name, you pass the raw DER bytes of the certificate's outer `signatureAlgorithm`
field and the value bytes of the `signature` BIT STRING. Raises `ValueError` if the
signature is invalid or the algorithm is unsupported. Uses NSS when the library was
compiled with the `nss` feature, OpenSSL otherwise.

- `tbs_der` — DER bytes of the TBSCertificate (or equivalent TBS structure) that was signed.
- `sig_alg_der` — DER bytes of the `AlgorithmIdentifier` SEQUENCE from the certificate's
  `signatureAlgorithm` field.
- `signature` — raw signature bytes (the BIT STRING value, without tag, length, or the
  leading unused-bits byte).

```python
import synta

cert = synta.Certificate.from_der(open("cert.der", "rb").read())
issuer_cert = synta.Certificate.from_der(open("issuer.der", "rb").read())

pub_key = synta.PublicKey.from_der(issuer_cert.subject_public_key_info_der)
try:
    pub_key.verify_certificate_signature(
        cert.tbs_certificate_der,
        cert.signature_algorithm_der,
        cert.signature_value,
    )
    print("Signature valid")
except ValueError as e:
    print(f"Signature invalid: {e}")
```

### Full class stub

```python
class PublicKey:
    @staticmethod
    def from_pem(data: bytes) -> PublicKey: ...
    @staticmethod
    def from_der(data: bytes) -> PublicKey: ...
    @staticmethod
    def from_rsa_components(n: bytes, e: bytes) -> PublicKey: ...
    @staticmethod
    def from_ec_components(x: bytes, y: bytes, curve: str) -> PublicKey: ...
    def to_pem(self) -> bytes: ...
    def to_der(self) -> bytes: ...
    key_type: str
    key_size: int | None
    modulus: bytes | None
    public_exponent: bytes | None
    curve_name: str | None
    x: bytes | None
    y: bytes | None
    def rsa_oaep_encrypt(self, plaintext: bytes, hash_algorithm: str = "sha256") -> bytes: ...
    def rsa_pkcs1v15_encrypt(self, plaintext: bytes) -> bytes: ...
    def kem_encapsulate(self) -> tuple[bytes, bytes]: ...
    def verify_signature(
        self,
        signature: bytes,
        data: bytes,
        algorithm: str | None = None,
        context: bytes | None = None,
    ) -> None: ...
    def verify_certificate_signature(
        self,
        tbs_der: bytes,
        sig_alg_der: bytes,
        signature: bytes,
    ) -> None: ...
```

---

## PrivateKey

### Construction

```python
PrivateKey.from_pem(data: bytes, password: bytes | None = None) -> PrivateKey
```
Load from PEM-encoded data (optionally password-protected).

```python
PrivateKey.from_der(data: bytes) -> PrivateKey
```
Load from unencrypted PKCS#8 DER bytes.

```python
PrivateKey.from_pkcs8_encrypted(data: bytes, password: bytes) -> PrivateKey
```
Decrypt and load from PKCS#8 EncryptedPrivateKeyInfo DER.

```python
PrivateKey.from_pkcs11_uri(uri: str) -> PrivateKey
```
Load a private key by PKCS#11 URI (RFC 7512). The key material stays inside the
hardware token (HSM or smart card); only a reference is returned. Signing and
decryption operations are delegated to the token.

Requires the library to be compiled with the `openssl` or `nss` Cargo feature and
a PKCS#11 provider configured in the system crypto stack (e.g. the `pkcs11-provider`
OpenSSL provider, or an NSS PKCS#11 module).

The URI form is:

```
pkcs11:token=MyToken;id=%01%02%03;pin-value=1234
pkcs11:token=MyHSM;object=ca-signing-key;type=private?pin-value=1234
```

Raises `ValueError` if the URI cannot be parsed or the key cannot be loaded from
the token.

### Key generation

```python
PrivateKey.generate_rsa(key_size: int, public_exponent: int = 65537) -> PrivateKey
```
Generate a new RSA private key. `key_size` is the modulus bit-length (e.g. `2048`, `3072`, `4096`).
Raises `ValueError` for unsupported key sizes.

```python
PrivateKey.generate_ec(curve: str = "P-256") -> PrivateKey
```
Generate a new EC private key. `curve` must be `"P-256"`, `"P-384"`, or `"P-521"`.
Raises `ValueError` for unknown curve names.

```python
PrivateKey.generate_ed25519() -> PrivateKey
PrivateKey.generate_ed448() -> PrivateKey
```
Generate a new Ed25519 or Ed448 private key (RFC 8032).

```python
PrivateKey.generate_ml_dsa(parameter_set: str) -> PrivateKey
```
Generate a new ML-DSA private key (FIPS 204). `parameter_set` must be `"ML-DSA-44"`,
`"ML-DSA-65"`, or `"ML-DSA-87"`. Requires OpenSSL 3.5 or newer.

```python
PrivateKey.generate_ml_kem(parameter_set: str) -> PrivateKey
```
Generate a new ML-KEM private key (FIPS 203). `parameter_set` must be `"ML-KEM-512"`,
`"ML-KEM-768"`, or `"ML-KEM-1024"`. Requires OpenSSL 3.5 or newer.

### Importing keys from raw components

Importing a private key directly from raw big-endian byte components is available
only from the Rust API (`BackendPrivateKey::from_ec_private_scalar` and
`BackendPrivateKey::from_rsa_private_components`). There is no Python-level
equivalent in the current release.

If you hold raw EC or RSA key material in Python you can work around this by
assembling a PKCS#8 or PEM structure with a third-party library (e.g.
`cryptography`) and then loading it with `PrivateKey.from_der` or
`PrivateKey.from_pem`. See the Rust documentation for details on the
raw-component import API.

### Serialisation

```python
key.to_pem() -> bytes
key.to_der() -> bytes   # unencrypted PKCS#8 DER
```

### Properties

| Property | Type | Description |
|---|---|---|
| `key_type` | `str` | Same values as `PublicKey.key_type` |
| `key_size` | `int \| None` | Key size in bits, or `None` |

### Methods

```python
key.public_key() -> PublicKey              # extract the matching public key
key.sign(data: bytes, algorithm: str | None = None, context: bytes | None = None) -> bytes
key.rsa_oaep_decrypt(ciphertext: bytes, hash_algorithm: str = "sha256") -> bytes
key.rsa_pkcs1v15_decrypt(ciphertext: bytes) -> bytes
key.kem_decapsulate(ciphertext: bytes) -> bytes   # ML-KEM: returns shared_secret
```

### Full class stub

```python
class PrivateKey:
    @staticmethod
    def from_pem(data: bytes, password: bytes | None = None) -> PrivateKey: ...
    @staticmethod
    def from_der(data: bytes) -> PrivateKey: ...
    @staticmethod
    def from_pkcs8_encrypted(data: bytes, password: bytes) -> PrivateKey: ...
    @staticmethod
    def from_pkcs11_uri(uri: str) -> PrivateKey: ...
    @staticmethod
    def generate_rsa(key_size: int, public_exponent: int = 65537) -> PrivateKey: ...
    @staticmethod
    def generate_ec(curve: str = "P-256") -> PrivateKey: ...
    @staticmethod
    def generate_ed25519() -> PrivateKey: ...
    @staticmethod
    def generate_ed448() -> PrivateKey: ...
    @staticmethod
    def generate_ml_dsa(parameter_set: str) -> PrivateKey: ...
    @staticmethod
    def generate_ml_kem(parameter_set: str) -> PrivateKey: ...
    def to_pem(self) -> bytes: ...
    def to_der(self) -> bytes: ...
    key_type: str
    key_size: int | None
    def public_key(self) -> PublicKey: ...
    def sign(
        self,
        data: bytes,
        algorithm: str | None = None,
        context: bytes | None = None,
    ) -> bytes: ...
    def rsa_oaep_decrypt(self, ciphertext: bytes, hash_algorithm: str = "sha256") -> bytes: ...
    def rsa_pkcs1v15_decrypt(self, ciphertext: bytes) -> bytes: ...
    def kem_decapsulate(self, ciphertext: bytes) -> bytes: ...
```

## Usage

```python
import synta

# Generate keys
rsa_key  = synta.PrivateKey.generate_rsa(2048)
ec_key   = synta.PrivateKey.generate_ec("P-256")
ed25519  = synta.PrivateKey.generate_ed25519()
ed448    = synta.PrivateKey.generate_ed448()
mldsa65  = synta.PrivateKey.generate_ml_dsa("ML-DSA-65")   # requires OpenSSL 3.5+
mlkem768 = synta.PrivateKey.generate_ml_kem("ML-KEM-768")  # requires OpenSSL 3.5+

# Serialise / load back
pem_bytes = ec_key.to_pem()
ec_key2   = synta.PrivateKey.from_pem(pem_bytes)
pub       = ec_key2.public_key()

# Load a certificate and verify a signature using the embedded public key
cert = synta.Certificate.from_der(open("cert.der", "rb").read())
pub_key = synta.PublicKey.from_der(cert.public_key)
# For ECDSA with SHA-256:
try:
    pub_key.verify_signature(signature, message, algorithm="sha256")
    print("Valid")
except ValueError:
    print("Invalid")

# Load a private key and sign
priv_key = synta.PrivateKey.from_pem(open("key.pem", "rb").read())
sig = priv_key.sign(message, algorithm="sha256")

# ML-KEM key exchange
kem_pub = synta.PublicKey.from_der(kem_pub_der)
ciphertext, shared_secret = kem_pub.kem_encapsulate()

priv_kem = synta.PrivateKey.from_der(kem_priv_der)
recovered = priv_kem.kem_decapsulate(ciphertext)
assert shared_secret == recovered

# Load a key from a PKCS#11 hardware token (HSM or smart card)
# Requires the openssl or nss feature and a configured PKCS#11 provider.
hsm_key = synta.PrivateKey.from_pkcs11_uri(
    "pkcs11:token=MyHSM;object=ca-signing-key;type=private?pin-value=1234"
)
# The key material stays in the HSM; signing is performed by the token.
sig = hsm_key.sign(message, algorithm="sha256")
```

See also [PKCS#8](../protocols/pkcs8.md) for parsing PKCS#8 private-key envelopes and
[CMS-KEM](../cms/kem.md) for RFC 9629 quantum-safe KEM recipient structures.