# 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
| `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
| `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.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.