# Keys
## Algorithm identification
`identify_signature_algorithm` and `identify_public_key_algorithm` use integer
dispatch on OID components. They never allocate and return `&'static str`.
```rust,ignore
use synta::ObjectIdentifier;
use synta_certificate::{oids, names, identify_signature_algorithm, identify_public_key_algorithm};
let oid = ObjectIdentifier::new(oids::SHA256_WITH_RSA).unwrap();
assert_eq!(identify_signature_algorithm(&oid), names::SHA256_WITH_RSA);
let oid = ObjectIdentifier::new(oids::ML_DSA_44).unwrap();
assert_eq!(identify_signature_algorithm(&oid), names::ML_DSA_44);
let oid = ObjectIdentifier::new(oids::ML_KEM_768).unwrap();
assert_eq!(identify_public_key_algorithm(&oid), Some(names::ML_KEM_768));
```
## Supported signature algorithms
| RSA (PKCS #1) | 1.2.840.113549.1.1.* | RFC 3279 |
| ECDSA | 1.2.840.10045.4.* | RFC 5480 |
| EdDSA (Ed25519) | 1.3.101.112 | RFC 8410 |
| EdDSA (Ed448) | 1.3.101.113 | RFC 8410 |
| ML-DSA-44/65/87 | 2.16.840.1.101.3.4.3.17–19 | FIPS 204 |
| Composite ML-DSA (18 variants) | 1.3.6.1.5.5.7.6.37–54 | draft-ietf-lamps-pq-composite-sigs-19 |
| DSA | 1.2.840.10040.4.* | FIPS 186 |
## Supported public key algorithms
| RSA | RFC 3279 |
| ECDSA (P-256, P-384, P-521) | RFC 5480 |
| EdDSA (Ed25519, Ed448) | RFC 8410 |
| ML-DSA (44, 65, 87) | FIPS 204 |
| Composite ML-DSA (18 variants) | draft-ietf-lamps-pq-composite-sigs-19 |
| ML-KEM (512, 768, 1024) | FIPS 203 |
| DSA | FIPS 186 |
## Public key decoding
`decode_public_key_info` dispatches on the algorithm OID and returns a
`PublicKeyInfo` enum with algorithm-specific fields:
```rust,ignore
use synta_certificate::{decode_public_key_info, PublicKeyInfo};
let spki = &cert.tbs_certificate.subject_public_key_info;
let info = decode_public_key_info(
&spki.algorithm.algorithm,
spki.algorithm.parameters.as_ref(),
spki.subject_public_key.as_bytes(),
spki.subject_public_key.bit_len(),
);
match info {
PublicKeyInfo::Rsa { modulus, exponent, bit_count } => {
println!("RSA-{}, exponent={}", bit_count, exponent);
}
PublicKeyInfo::Ec { bit_count, curve_nist_name, .. } => {
println!("EC-{} ({})", bit_count, curve_nist_name.unwrap_or("?"));
}
PublicKeyInfo::MlDsa { security_level, public_key_size } => {
println!("ML-DSA-{} ({} bytes)", security_level, public_key_size);
}
PublicKeyInfo::Unknown { alg_name, .. } => {
println!("Unknown: {}", alg_name);
}
}
```
## PKCS#8 private keys
The `pkcs8_types` module provides `OneAsymmetricKey<'a>` (RFC 5958) and a
`PrivateKeyInfo<'a>` alias:
```rust,ignore
use synta::{Decoder, Encoding};
use synta_certificate::pkcs8_types::OneAsymmetricKey;
let der = std::fs::read("private.key")?;
let mut dec = Decoder::new(&der, Encoding::Der);
let key: OneAsymmetricKey = dec.decode()?;
println!("Version: {:?}", key.version);
println!("Algorithm: {:?}", key.private_key_algorithm.algorithm);
```
## RFC 3279 algorithm parameters
The `pkixalgs_types` module provides DSA/DH domain parameter types, ECDSA
signature value types, and EC parameter types:
```rust,ignore
use synta::{Decoder, Encoding};
use synta_certificate::pkixalgs_types::{DssParms, EcdsaSigValue, ECParameters};
// DSA domain parameters
let parms = DssParms {
p: Integer::from(23u64),
q: Integer::from(11u64),
g: Integer::from(5u64),
};
// ECDSA signature value (r, s)
let sig = EcdsaSigValue {
r: Integer::from(42u64),
s: Integer::from(17u64),
};
// EC named curve (P-256 = 1.2.840.10045.3.1.7)
let curve_oid = synta::ObjectIdentifier::new(&[1, 2, 840, 10045, 3, 1, 7]).unwrap();
let params = ECParameters::NamedCurve(curve_oid);
```
## Crypto backends: NSS and OpenSSL
`synta-certificate` supports two pluggable crypto backends selected at compile time:
| `openssl` | yes | OpenSSL bindings via the `native-ossl` crate |
| `nss` | no | Mozilla NSS via the `nss-sys` crate |
When both features are compiled in, **OpenSSL takes priority** and the NSS
backend module is excluded entirely (`#[cfg(all(feature = "nss", not(feature = "openssl")))]`).
To select NSS as the sole backend:
```toml
[dependencies]
synta-certificate = { version = "0.1", default-features = false, features = ["std", "derive", "nss"] }
```
### Backend-agnostic key abstractions
The recommended approach to key generation and signing never names a backend type:
```rust,ignore
use synta_certificate::{PrivateKeyBuilder, CertificateBuilder};
// Generate a P-256 key (delegates to OpenSSL or NSS depending on active features)
let key = PrivateKeyBuilder::ec("P-256").generate()?;
// Public key as DER-encoded SubjectPublicKeyInfo — use it with CertificateBuilder
let spki_der = key.public_key_spki_der()?;
// Create a signer; "sha256" selects the hash (ignored for EdDSA)
let signer = key.as_signer("sha256");
// Pass the signer directly to CertificateBuilder::sign
let cert_der = CertificateBuilder::new()
/* … set fields … */
.sign(signer.as_ref())?;
```
Other key algorithms:
```rust,ignore
let rsa_key = PrivateKeyBuilder::rsa(3072).generate()?;
let ed_key = PrivateKeyBuilder::ed25519().generate()?;
let pqc_key = PrivateKeyBuilder::ml_dsa("ML-DSA-65").generate()?; // OpenSSL 3.5+ or NSS
// Composite ML-DSA-65 + ECDSA-P256-SHA512 (sub-arc 45):
// requires openssl + pqc features (OpenSSL 3.3+) or the nss feature
let comp_key = PrivateKeyBuilder::composite_ml_dsa(45).generate()?;
```
`BackendPrivateKey` can be used when you need to load, store, or serialise PKCS#8 bytes:
```rust,ignore
use synta_certificate::BackendPrivateKey;
// Load from unencrypted PKCS#8 DER (validates the key on load)
let key = BackendPrivateKey::from_der(&pkcs8_der)?;
// Load from PEM, optionally password-protected
let key = BackendPrivateKey::from_pem(pem_bytes, None)?;
let key = BackendPrivateKey::from_pem(pem_bytes, Some(b"passphrase"))?;
```
`BackendPrivateKey` also implements the `PrivateKey` trait, so `key.as_signer("sha256")`
and `key.public_key_spki_der()` work just like with the boxed trait object from
`PrivateKeyBuilder::generate`.
### BackendPrivateKey — serialisation, generation, and decryption
All methods in this section require the `openssl` feature.
**Serialisation**
| `from_der(der: &[u8])` | Load from unencrypted PKCS#8 DER |
| `from_pem(pem: &[u8], password: Option<&[u8]>)` | Load from PEM, optionally encrypted |
| `from_pkcs8_encrypted(data: &[u8], password: &[u8])` | Load from encrypted PKCS#8 DER |
| `to_der()` | Serialize to unencrypted PKCS#8 DER |
| `to_pem(password: Option<&[u8]>)` | Serialize to PEM (AES-256-CBC when password is given) |
| `to_pkcs8_encrypted(password: &[u8])` | Serialize to `EncryptedPrivateKeyInfo` DER |
| `public_key()` | Extract the public half as `BackendPublicKey` |
| `key_type()` | Key algorithm as a lowercase string (`"rsa"`, `"ec"`, `"ed25519"`, …) |
| `key_bit_size()` | Key size in bits, or `None` for EdDSA keys |
```rust,ignore
use synta_certificate::BackendPrivateKey;
let key = BackendPrivateKey::generate_rsa(3072, 65537)?;
let pem = key.to_pem(Some(b"s3cr3t"))?;
std::fs::write("key.pem", &pem)?;
let loaded = BackendPrivateKey::from_pem(&pem, Some(b"s3cr3t"))?;
let pub_key = loaded.public_key()?;
```
**Generation** (prefer `PrivateKeyBuilder` for backend-agnostic code)
`BackendPrivateKey` has PQC generation; classical algorithms are on `OpensslPrivateKey`:
| `generate_ml_dsa(parameter_set: &str)` | `BackendPrivateKey` | ML-DSA-44/65/87 (OpenSSL 3.5+, `pqc` feature) |
| `generate_ml_kem(parameter_set: &str)` | `BackendPrivateKey` | ML-KEM-512/768/1024 (OpenSSL 3.5+, `pqc` feature) |
| `generate_rsa(key_size: u32, public_exponent: u32)` | `OpensslPrivateKey` | RSA (e.g. 3072, 65537) |
| `generate_ec(curve: &str)` | `OpensslPrivateKey` | EC key on `"P-256"`, `"P-384"`, or `"P-521"` |
| `generate_ed25519()` | `OpensslPrivateKey` | Ed25519 key |
| `generate_ed448()` | `OpensslPrivateKey` | Ed448 key |
**Decryption** (`OpensslPrivateKey` only, not on `BackendPrivateKey`)
| `rsa_pkcs1v15_decrypt(ciphertext: &[u8])` | RSA PKCS#1 v1.5 decryption |
| `rsa_oaep_decrypt(ciphertext: &[u8], hash_alg: &str)` | RSA-OAEP decryption |
Use `BackendPrivateKey::from_pkcs11_uri` or `PrivateKeyBuilder` when you do not need
direct access to the raw `OpensslPrivateKey` type.
### ML-KEM key exchange
ML-KEM (FIPS 203) uses an encapsulation/decapsulation exchange rather than signing.
Both methods require either the `openssl` or `nss` feature.
```rust,ignore
use synta_certificate::{BackendPrivateKey, BackendPublicKey};
// Sender: encapsulate against the recipient's public key
let (ciphertext, shared_secret) = pub_key.ml_kem_encapsulate()?;
// Recipient: decapsulate to recover the shared secret
let recovered = priv_key.ml_kem_decapsulate(&ciphertext)?;
assert_eq!(shared_secret, recovered);
```
### ML-DSA context-string operations
Both methods pass an explicit FIPS 204 §5.2 context string for domain separation.
Pass `b""` for the default empty context.
```rust,ignore
use synta_certificate::{BackendPrivateKey, BackendPublicKey};
// Sign with context
let sig = priv_key.sign_ml_dsa_with_context(data, b"app-v1")?;
// Verify with context
pub_key.verify_ml_dsa_with_context(data, &sig, b"app-v1")?;
```
### Importing private keys from raw components
`BackendPrivateKey` provides two constructors for importing key material that
already exists as raw big-endian byte slices (e.g. from a JWK or a hardware-
derived key buffer). Both require the `openssl` or `nss` feature.
**`RsaPrivateComponents`** is a helper struct that names the eight CRT fields
needed for RSA import. All fields are `&[u8]` slices containing big-endian
unsigned integers, matching the encoding used by JWK and PKCS#1:
| `n` | RSA modulus |
| `e` | Public exponent |
| `d` | Private exponent |
| `p` | First prime factor |
| `q` | Second prime factor |
| `dp` | CRT exponent *d* mod (*p* − 1) |
| `dq` | CRT exponent *d* mod (*q* − 1) |
| `qi` | CRT coefficient *q*⁻¹ mod *p* |
```rust,ignore
use synta_certificate::{BackendPrivateKey, RsaPrivateComponents};
// Import an RSA private key from its CRT components
let key = BackendPrivateKey::from_rsa_private_components(&RsaPrivateComponents {
n: &n_bytes,
e: &e_bytes,
d: &d_bytes,
p: &p_bytes,
q: &q_bytes,
dp: &dp_bytes,
dq: &dq_bytes,
qi: &qi_bytes,
})?;
// Import an EC private key from scalar d and public-point affine coordinates
// curve must be "P-256", "P-384", or "P-521"
let key = BackendPrivateKey::from_ec_private_scalar(
&d_bytes, // private scalar (big-endian)
&x_bytes, // affine X coordinate (big-endian)
&y_bytes, // affine Y coordinate (big-endian)
"P-256",
)?;
```
Both constructors return `Err(PrivateKeyError)` when the backend rejects the
supplied key material (e.g. the EC point is not on the curve, or the RSA CRT
values are internally inconsistent). There is no Python-level equivalent;
Python callers must first encode the raw bytes into a PKCS#8 or PEM structure
and then use `PrivateKey.from_der` / `PrivateKey.from_pem`.
### Backend-dispatched factory functions
`default_signature_verifier` and `default_key_id_hasher` return trait objects backed by
whichever feature is active:
```rust,ignore
use synta_certificate::{default_signature_verifier, default_key_id_hasher};
let verifier = default_signature_verifier(); // Box<dyn ErasedSignatureVerifier>
let hasher = default_key_id_hasher(); // Box<dyn ErasedKeyIdHasher>
```
Additional default factory functions cover symmetric crypto:
| `default_data_hasher()` | `Box<dyn ErasedDataHasher>` | `DataHasher` |
| `default_hmac_provider()` | `Box<dyn ErasedHmacProvider>` | `HmacProvider` |
| `default_streaming_hasher()` | `Box<dyn ErasedStreamingHasher>` | `StreamingHasher` |
| `default_streaming_hmac_provider()` | `Box<dyn ErasedStreamingHmacProvider>` | `StreamingHmacProvider` |
| `default_pbkdf2_provider()` | `impl Pbkdf2Provider` | `Pbkdf2Provider` |
| `default_block_cipher_provider()` | `impl BlockCipherProvider` | `BlockCipherProvider` |
| `default_secure_random()` | `impl SecureRandom` | `SecureRandom` |
`BlockCipherProvider` exposes AES-CBC, AES-GCM (AEAD), and 3DES-CBC operations.
The AES-GCM methods perform authenticated encryption with additional data (AEAD):
```rust,ignore
use synta_certificate::default_block_cipher_provider;
let cipher = default_block_cipher_provider();
// Encrypt: key is 16/24/32 bytes; nonce is 12 bytes; aad may be empty.
let pt = cipher.aes_gcm_decrypt(&key, &nonce, &ct, aad)?;
```
All functions select the NSS backend when `nss` is active, fall back to OpenSSL, and
return a no-op stub when neither feature is compiled in.
```rust,ignore
use synta_certificate::{
default_hmac_provider, default_streaming_hasher, default_pbkdf2_provider,
};
let hmac = default_hmac_provider(); // Box<dyn ErasedHmacProvider>
let hasher = default_streaming_hasher(); // Box<dyn ErasedStreamingHasher>
let pbkdf2 = default_pbkdf2_provider(); // impl Pbkdf2Provider
```
### BackendPublicKey — verify any X.509 signature
`BackendPublicKey::verify_signature` verifies an X.509 signature using the full
`AlgorithmIdentifier` DER from the certificate, routing to NSS when enabled.
When the key was created via `from_der` or `from_pem`, the parsed `EVP_PKEY` is
cached at construction time (an O(1) refcount clone). Subsequent calls to
`verify_signature` reuse that cached key rather than re-parsing the DER, which
matters for workloads that verify many items — for example, 1 024 certificates
signed by the same CA key — against a single `BackendPublicKey` instance.
```rust,ignore
use synta_certificate::BackendPublicKey;
// spki_der: DER-encoded SubjectPublicKeyInfo (Vec<u8> or &[u8])
let pub_key = BackendPublicKey::from_spki_der(spki_der);
pub_key.verify_signature(tbs_der, sig_alg_der, signature)?;
```
- `tbs_der` — DER bytes of `TBSCertificate` (the bytes that were signed).
- `sig_alg_der` — DER bytes of the outer `AlgorithmIdentifier` SEQUENCE.
- `signature` — raw signature bytes (BIT STRING value, unused-bits byte stripped).
Returns `Ok(())` on success, `Err(PrivateKeyError)` on failure or unsupported algorithm.
**Loading and construction** (all require `openssl` feature unless noted)
| `from_spki_der(spki_der: Vec<u8>)` | Wrap raw SPKI DER without validation (no feature required) |
| `from_der(der: &[u8])` | Parse SPKI DER; caches `EVP_PKEY` for fast repeated use |
| `from_pem(pem: &[u8])` | Parse PEM SubjectPublicKeyInfo; caches `EVP_PKEY` |
| `from_rsa_components(n: &[u8], e: &[u8])` | Build RSA key from big-endian modulus and exponent |
| `from_ec_components(x: &[u8], y: &[u8], curve: &str)` | Build EC key from affine coordinates and curve name |
**Introspection** (all require `openssl` feature)
| `key_type()` | `&'static str` | `"rsa"`, `"ec"`, `"ed25519"`, `"ed448"`, or `"unknown"` |
| `key_bit_size()` | `Option<i64>` | Key size in bits; `None` for EdDSA keys |
| `rsa_modulus()` | `Result<Option<Vec<u8>>, _>` | RSA modulus *n* as big-endian bytes |
| `rsa_public_exponent()` | `Result<Option<Vec<u8>>, _>` | RSA exponent *e* as big-endian bytes |
| `ec_curve_name()` | `Result<Option<&'static str>, _>` | NIST curve name for EC keys |
| `ec_affine_coordinates()` | `Result<Option<EcAffineCoords>, _>` | Affine (X, Y) for EC keys |
```rust,ignore
use synta_certificate::BackendPublicKey;
let pub_key = BackendPublicKey::from_der(&spki_der)?;
println!("Type: {}", pub_key.key_type()); // "ec"
println!("Bits: {:?}", pub_key.key_bit_size()); // Some(256)
if let Some(curve) = pub_key.ec_curve_name()? {
println!("Curve: {}", curve); // "P-256"
}
if let Some(coords) = pub_key.ec_affine_coordinates()? {
println!("X len: {}", coords.x.len());
}
```
**Encryption** (available on both `openssl` and NSS backends)
| `rsa_oaep_encrypt(plaintext: &[u8], hash_alg: &str)` | RSA-OAEP encryption |
| `rsa_pkcs1v15_encrypt(plaintext: &[u8])` | RSA PKCS#1 v1.5 encryption |
**Signature verification** (available on `openssl` backend)
| `verify_message(data: &[u8], signature: &[u8], algorithm: Option<&str>)` | Verify using a hash-name string |
| `verify_signature(tbs_der, sig_alg_der, signature)` | Verify using full `AlgorithmIdentifier` DER |
| `verify_ml_dsa_with_context(data, signature, context)` | ML-DSA verify with FIPS 204 context string |
### PKCS#11 URI — hardware token keys
`BackendPrivateKey::from_pkcs11_uri` loads a key reference from a PKCS#11 URI
(RFC 7512) without extracting private key material from the token.
The `pkcs11-provider` OpenSSL provider (or an equivalent NSS PKCS#11 module) must
be configured in the OpenSSL or NSS configuration before calling this function.
```rust,ignore
use synta_certificate::BackendPrivateKey;
let key = BackendPrivateKey::from_pkcs11_uri(
"pkcs11:token=MyHSM;object=signing-key;type=private?pin-value=1234"
)?;
// The public SPKI is extracted from the token; private material stays in the HSM
let spki_der = key.public_key_spki_der()?;
let signer = key.as_signer("sha256");
```
`Pkcs11Uri` and `Pkcs11UriAttributes` are exported from the crate root and give
structured access to a parsed URI without re-parsing on each use:
```rust,ignore
use synta_certificate::{Pkcs11Uri, Pkcs11UriAttributes};
let uri = Pkcs11Uri::parse("pkcs11:token=MyHSM;object=cakey;id=%01%02?pin-value=1234")
.expect("not a pkcs11: URI");
println!("Token: {:?}", uri.attrs.token); // Some("MyHSM")
println!("Object: {:?}", uri.attrs.object); // Some("cakey")
println!("ID: {:?}", uri.attrs.id); // Some([0x01, 0x02])
println!("PIN: {:?}", uri.attrs.pin_value); // Some("1234")
println!("Raw: {}", uri.raw);
```
`Pkcs11UriAttributes` fields:
| `token` | `Option<String>` | `token=` (path component) |
| `object` | `Option<String>` | `object=` (path component) |
| `id` | `Option<Vec<u8>>` | `id=` decoded from percent-encoded bytes |
| `pin_value` | `Option<String>` | `?pin-value=` (query attribute) |
Unknown path and query attributes are silently ignored per RFC 7512 §2.1.
### NSS backend types (`nss` feature)
| `NssSignatureVerifier` | crate root | Verifies signatures via `VFY_VerifyDataWithAlgorithmID`; supports RSA PKCS#1 / RSA-PSS / ECDSA / EdDSA / ML-DSA |
| `NssVerifierError` | crate root | Error type for `NssSignatureVerifier` |
| `NssSigner` | `nss_backend::NssSigner` | Signs TBS blobs from a PKCS#8 private key imported into the NSS in-memory softokn slot |
| `NssSignerError` | `nss_backend::NssSignerError` | Error type for `NssSigner` |
| `NssKeyIdHasher` | crate root | SHA-1/256/384/512 via `PK11_HashBuf` for SKI/AKI extension encoding |
### OpenSSL backend types (`openssl` feature)
| `OpensslPrivateKey` | OpenSSL-backed private key; implements `PrivateKey` |
| `OpensslCertificateSigner` | Implements `CertificateSigner` around a `native_ossl::pkey::Pkey<Private>`; supports ML-DSA context strings via `with_context` |
| `OpensslCertificateSignerError` | Error type for `OpensslCertificateSigner` |
| `OpensslSignatureVerifier` | Verifies X.509 signatures via OpenSSL EVP |
| `OpensslVerifierError` | Error type for `OpensslSignatureVerifier` |
| `OpensslKeyIdHasher` | SHA-1/256/384/512 hashing for SKI/AKI encoding |
| `OpensslKeyIdHasherError` | Error type for `OpensslKeyIdHasher` |
### ML-DSA context strings
FIPS 204 §5.2 allows an application-defined context string (≤ 255 bytes) for
domain separation. Pass it via the `with_context` builder:
```rust,ignore
use synta_certificate::OpensslCertificateSigner;
let signer = OpensslCertificateSigner::new(&ml_dsa_key, "")
.with_context(b"my-application-v1");
let cert_der = CertificateBuilder::new()
/* … */
.sign(&signer)?;
```
`with_context` is a no-op for RSA, ECDSA, and EdDSA keys.
## Composite ML-DSA (draft-ietf-lamps-pq-composite-sigs-19)
Composite ML-DSA algorithms pair an ML-DSA component with a traditional algorithm
(RSA-PSS, RSA-PKCS#1v15, ECDSA, Ed25519, or Ed448) to produce a single composite
public key and signature. The 18 algorithm variants occupy OID sub-arcs 37–54 of
the `id-alg` arc `1.3.6.1.5.5.7.6`.
### Key generation
Select a variant by its OID sub-arc and call `composite_ml_dsa`:
```rust,ignore
use synta_certificate::{PrivateKeyBuilder, CertificateBuilder};
// Sub-arc 45 = MLDSA65-ECDSA-P256-SHA512
let key = PrivateKeyBuilder::composite_ml_dsa(45).generate()?;
let spki_der = key.public_key_spki_der()?;
let signer = key.as_signer(""); // algorithm string is ignored for composite ML-DSA
let cert_der = CertificateBuilder::new()
/* … set fields … */
.sign(signer.as_ref())?;
```
### OID sub-arc table
| 37 | MLDSA44-RSA2048-PSS-SHA256 | 44 |
| 38 | MLDSA44-RSA2048-PKCS15-SHA256 | 44 |
| 39 | MLDSA44-Ed25519-SHA512 | 44 |
| 40 | MLDSA44-ECDSA-P256-SHA256 | 44 |
| 41 | MLDSA65-RSA3072-PSS-SHA512 | 65 |
| 42 | MLDSA65-RSA3072-PKCS15-SHA512 | 65 |
| 43 | MLDSA65-RSA4096-PSS-SHA512 | 65 |
| 44 | MLDSA65-RSA4096-PKCS15-SHA512 | 65 |
| 45 | MLDSA65-ECDSA-P256-SHA512 | 65 |
| 46 | MLDSA65-ECDSA-P384-SHA512 | 65 |
| 47 | MLDSA65-ECDSA-Brainpool-P256r1-SHA512 | 65 |
| 48 | MLDSA65-Ed25519-SHA512 | 65 |
| 49 | MLDSA87-ECDSA-P384-SHA512 | 87 |
| 50 | MLDSA87-ECDSA-Brainpool-P384r1-SHA512 | 87 |
| 51 | MLDSA87-Ed448-SHAKE256 | 87 |
| 52 | MLDSA87-RSA3072-PSS-SHA512 | 87 |
| 53 | MLDSA87-RSA4096-PSS-SHA512 | 87 |
| 54 | MLDSA87-ECDSA-P521-SHA512 | 87 |
### Feature requirements
| OpenSSL | `openssl` + `pqc` | OpenSSL 3.3+ (`cfg(ossl_mldsa)`) |
| NSS | `nss` | Any supported NSS version |
### Encoding rules (draft-19)
- **SPKI BIT STRING payload**: `mldsa_pk || trad_pk`, split at ML-DSA public key size
(1312 / 1952 / 2592 bytes for ML-DSA-44 / 65 / 87).
- **Composite signature**: `mldsa_sig || trad_sig`, split at ML-DSA signature size
(2420 / 3309 / 4627 bytes).
- **PKCS#8 privateKey OCTET STRING content**: `mldsa_seed (32 bytes) || trad_sk`.
- **Message representative M'**: `"CompositeAlgorithmSignatures2025" || Label || 0x00 || PH(TBS)`
where `PH` is SHA-256, SHA-512, or SHAKE256-512bit per the variant spec.
### OID constants
All 18 OID constants are exported from `synta_certificate::oids`:
```rust,ignore
use synta_certificate::oids::{
MLDSA44_RSA2048_PSS_SHA256,
MLDSA44_RSA2048_PKCS15_SHA256,
MLDSA44_ED25519_SHA512,
MLDSA44_ECDSA_P256_SHA256,
MLDSA65_RSA3072_PSS_SHA512,
// … through MLDSA87_ECDSA_P521_SHA512
COMPOSITE_MLDSA_ARC, // prefix OID 1.3.6.1.5.5.7.6 for arc membership checks
};
```
### NSS backend limitations
The following restrictions apply when using the NSS backend for composite ML-DSA:
- **SHAKE256 hash** (sub-arc 51, MLDSA87-Ed448-SHAKE256) is not available via
`PK11_HashBuf` and is therefore unsupported for both signing and verification.
- **Brainpool curves** (sub-arcs 47, 50) are not supported for either signing
or verification on NSS (no brainpool support in the NSS EC parameter table).
## PKCS#9 OID constants
9 attribute OIDs from RFC 2985, RFC 5652, RFC 2986, and RFC 7292 are exposed in
`oids`:
| `PKCS9_EMAIL_ADDRESS` | 9.1 |
| `PKCS9_UNSTRUCTURED_NAME` | 9.2 |
| `PKCS9_CONTENT_TYPE` | 9.3 |
| `PKCS9_MESSAGE_DIGEST` | 9.4 |
| `PKCS9_SIGNING_TIME` | 9.5 |
| `PKCS9_COUNTERSIGNATURE` | 9.6 |
| `PKCS9_CHALLENGE_PASSWORD` | 9.7 |
| `PKCS9_EXTENSION_REQUEST` | 9.14 |
| `PKCS9_FRIENDLY_NAME` | 9.20 |
| `PKCS9_LOCAL_KEY_ID` | 9.21 |