"""
quantum_safe.protocols.x509
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Hybrid X.509 certificate builder.
Hybrid X.509 certificates carry both a classical signature and a PQC
co-signature, making them valid to both classical and post-quantum verifiers.
Standards basis
---------------
The approach follows two IETF drafts:
draft-ounsworth-pq-composite-sigs:
Defines "composite" key and signature formats where a single public key
is actually two sub-keys, and a single signature is two sub-signatures.
A verifier that understands composite must validate both; a legacy verifier
that only understands the primary (classical) sub-key can still validate it.
draft-truskovsky-lamps-pq-hybrid-x509:
Alternative approach using the SubjectAltPublicKeyInfo extension to carry
the PQC public key alongside the classical one.
We implement a simplified version: the certificate is signed with a classical
key (ECDSA P-256 or Ed25519) using standard X.509, and the PQC co-signature
is stored in a non-critical extension (OID 1.3.6.1.4.1.99999.1) as a DER
OCTET STRING. This is backward compatible: classical verifiers ignore the
unknown extension; our verifier checks both.
Note on the OID 1.3.6.1.4.1.99999.1: this is a placeholder. A real
deployment would need a registered OID from IANA or a private enterprise
arc. We document this prominently so it's never accidentally used as-is
in production.
Supported classical signature algorithms
-----------------------------------------
- Ed25519 (via cryptography library's x509 builder)
- ECDSA P-256, P-384
Supported PQC co-signature algorithms
---------------------------------------
- ML-DSA-65 (default)
- ML-DSA-87
"""
from __future__ import annotations
import datetime
import ipaddress
import warnings
from dataclasses import dataclass, field
from typing import Any
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric.ec import (
SECP256R1,
SECP384R1,
generate_private_key,
)
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from cryptography.hazmat.primitives.serialization import Encoding
from cryptography.x509.oid import NameOID
from quantum_safe._internal import serialization as _ser
from quantum_safe.types import KeyPair, PublicKey
# Placeholder OID for the PQC co-signature extension.
# WARNING: This is NOT a registered OID. Register your own before production use.
_PQC_COSIG_OID = x509.ObjectIdentifier("1.3.6.1.4.1.99999.1")
_PQC_PUBKEY_OID = x509.ObjectIdentifier("1.3.6.1.4.1.99999.2")
# Default certificate validity
_DEFAULT_VALIDITY_DAYS = 365
# The data that gets PQC-signed for the co-signature.
# We sign: DER-encoded TBS (to-be-signed) certificate + version byte.
# This binds the co-signature to the exact certificate content.
_COSIG_INFO_PREFIX = b"qs-x509-cosig-v1\x00"
[docs]
@dataclass
class HybridCertificateBuilder:
"""Builder for hybrid X.509 certificates.
Usage::
from quantum_safe.protocols.x509 import HybridCertificateBuilder
from quantum_safe.signatures.hybrid import HybridSign
# Generate key material
signer = HybridSign()
hybrid_kp = signer.generate_keypair()
classical_priv = Ed25519PrivateKey.generate()
builder = HybridCertificateBuilder(
subject_cn="service.internal",
classical_private_key=classical_priv,
pqc_keypair=hybrid_kp,
validity_days=365,
)
cert_pem, cosig_bundle = builder.build()
"""
subject_cn: str
classical_private_key: Any # Ed25519PrivateKey | EllipticCurvePrivateKey
pqc_keypair: KeyPair
validity_days: int = _DEFAULT_VALIDITY_DAYS
is_ca: bool = False
dns_names: list[str] = field(default_factory=list)
ip_addresses: list[str] = field(default_factory=list)
organization: str = ""
country: str = ""
issuer_cert: Any = None # x509.Certificate — for non-self-signed
issuer_key: Any = None # classical private key of issuer
extended_key_usage: list[x509.ObjectIdentifier] = field(default_factory=list)
[docs]
def build(self, signer: object = None) -> tuple[bytes, bytes]:
"""Build the hybrid certificate.
Returns:
(cert_pem, cosig_bundle):
cert_pem: PEM-encoded X.509 certificate with embedded
PQC public key extension. Valid to classical
verifiers.
cosig_bundle: CBOR-encoded co-signature bundle:
{pqc_sig: bytes, pqc_algo: str, cert_fp: str}
Verifiers that support hybrid validation check
this alongside the certificate.
"""
# Build subject name
name_attrs = [x509.NameAttribute(NameOID.COMMON_NAME, self.subject_cn)]
if self.organization:
name_attrs.append(x509.NameAttribute(NameOID.ORGANIZATION_NAME, self.organization))
if self.country:
name_attrs.append(x509.NameAttribute(NameOID.COUNTRY_NAME, self.country))
subject = x509.Name(name_attrs)
# Issuer: self-signed unless issuer_cert is provided
issuer = subject if self.issuer_cert is None else self.issuer_cert.subject
# Validity window
now = datetime.datetime.now(datetime.timezone.utc)
not_valid_before = now - datetime.timedelta(minutes=1) # small grace for clock skew
not_valid_after = now + datetime.timedelta(days=self.validity_days)
# Signing key for the classical signature
signing_key = self.issuer_key if self.issuer_key else self.classical_private_key
# Build the certificate
builder = (
x509.CertificateBuilder()
.subject_name(subject)
.issuer_name(issuer)
.public_key(self.classical_private_key.public_key())
.serial_number(x509.random_serial_number())
.not_valid_before(not_valid_before)
.not_valid_after(not_valid_after)
)
# Basic constraints
builder = builder.add_extension(
x509.BasicConstraints(ca=self.is_ca, path_length=0 if self.is_ca else None),
critical=True,
)
# Subject Alternative Names
san_entries: list[Any] = []
for dns in self.dns_names:
san_entries.append(x509.DNSName(dns))
for ip in self.ip_addresses:
try:
san_entries.append(x509.IPAddress(ipaddress.ip_address(ip)))
except ValueError:
warnings.warn(f"Invalid IP address in SAN: {ip!r}", stacklevel=2)
if san_entries:
builder = builder.add_extension(
x509.SubjectAlternativeName(san_entries),
critical=False,
)
# Extended key usage
if self.extended_key_usage:
builder = builder.add_extension(
x509.ExtendedKeyUsage(self.extended_key_usage),
critical=False,
)
# Embed PQC public key as a non-critical extension.
# This lets post-quantum-aware verifiers find the PQC public key
# without out-of-band distribution.
pqc_pub_der = _ser.dumps(
{
"algo": self.pqc_keypair.algorithm,
"pub": self.pqc_keypair.public.raw_bytes,
"qs-version": 1,
}
)
builder = builder.add_extension(
x509.UnrecognizedExtension(
oid=_PQC_PUBKEY_OID,
value=pqc_pub_der,
),
critical=False,
)
# Sign with classical key
if isinstance(signing_key, Ed25519PrivateKey):
cert = builder.sign(signing_key, algorithm=None, backend=default_backend())
else:
cert = builder.sign(signing_key, hashes.SHA256(), backend=default_backend())
cert_pem = cert.public_bytes(Encoding.PEM)
# Generate PQC co-signature over the DER-encoded certificate.
# The co-signature is over the entire cert bytes so any modification
# invalidates it.
cert_der = cert.public_bytes(Encoding.DER)
cosig_input = _COSIG_INFO_PREFIX + cert_der
cosig_bundle = self._generate_cosig(cosig_input, signer=signer)
return cert_pem, cosig_bundle
def _generate_cosig(self, data: bytes, signer: Any = None) -> bytes: # noqa: ANN401
"""Generate the PQC co-signature bundle."""
from quantum_safe.signatures.core import Sign
from quantum_safe.signatures.hybrid import HybridSign
algo = self.pqc_keypair.algorithm
if signer is None:
if "+" in algo:
# Use the normal constructor so validate_hybrid_combination()
# runs and unapproved pairs are rejected.
from quantum_safe.signatures.algorithms import parse_hybrid_name
classical, pqc = parse_hybrid_name(algo)
signer = HybridSign(classical=classical, pqc=pqc)
else:
signer = Sign(algorithm=algo)
sm = signer.sign(data, self.pqc_keypair.secret, context=b"qs-x509-cosig")
# Package the co-signature with metadata for distribution
bundle = _ser.dumps(
{
"v": 1,
"algo": algo,
"sig": sm.signature,
"context": b"qs-x509-cosig",
"cert_fp": self.pqc_keypair.public.fingerprint(),
}
)
return bundle
[docs]
@staticmethod
def verify_cosig(
cert_pem: bytes,
cosig_bundle: bytes,
pqc_public_key: PublicKey,
) -> None:
"""Verify a hybrid certificate's PQC co-signature.
Args:
cert_pem: PEM-encoded certificate.
cosig_bundle: Co-signature bundle from build().
pqc_public_key: The signer's PQC public key.
Raises:
VerificationError: if the co-signature is invalid.
KeyParseError: if the bundle is malformed.
"""
from quantum_safe.exceptions import KeyParseError
from quantum_safe.signatures.core import Sign
from quantum_safe.signatures.hybrid import HybridSign
from quantum_safe.types.signatures import SignedMessage
# Load the cert to get its DER bytes
try:
cert = x509.load_pem_x509_certificate(cert_pem, default_backend())
cert_der = cert.public_bytes(Encoding.DER)
except Exception as exc:
raise KeyParseError("pem", f"Failed to load certificate: {exc}") from exc
# Decode the bundle
try:
bundle = _ser.loads(cosig_bundle)
except Exception as exc:
raise KeyParseError("cbor", f"Failed to decode cosig bundle: {exc}") from exc
algo = bundle.get("algo", "")
sig = bytes(bundle.get("sig", b""))
context = bytes(bundle.get("context", b"qs-x509-cosig"))
if not algo or not sig:
raise KeyParseError("cbor", "cosig bundle missing algo or sig")
# Build the verifier instance using the normal constructor so that
# validate_hybrid_combination() runs and rejects unapproved pairs.
verifier: HybridSign | Sign
if "+" in algo:
from quantum_safe.signatures.algorithms import parse_hybrid_name
classical, pqc = parse_hybrid_name(algo)
verifier = HybridSign(classical=classical, pqc=pqc)
else:
verifier = Sign(algorithm=algo)
cosig_input = _COSIG_INFO_PREFIX + cert_der
sm = SignedMessage(
message=cosig_input,
signature=sig,
algorithm=algo,
context=context,
)
verifier.verify(sm, pqc_public_key)
[docs]
def generate_classical_keypair_for_cert(
algorithm: str = "Ed25519",
) -> Any: # noqa: ANN401
"""Generate a classical keypair suitable for certificate signing.
Args:
algorithm: "Ed25519", "P-256", or "P-384".
Returns:
A private key object from the cryptography library.
"""
if algorithm == "Ed25519":
return Ed25519PrivateKey.generate()
elif algorithm == "P-256":
return generate_private_key(SECP256R1(), default_backend())
elif algorithm == "P-384":
return generate_private_key(SECP384R1(), default_backend())
else:
raise ValueError(
f"Unsupported classical cert algorithm '{algorithm}'. "
f"Valid options: Ed25519, P-256, P-384"
)