| """
|
| crypto/keys.py
|
| RSA key lifecycle: generate, self-sign X.509, serialise/load PEM.
|
| Pure stdlib + cryptography library. No lxml, no xmlsec.
|
| """
|
|
|
| import datetime
|
| from dataclasses import dataclass
|
|
|
| from cryptography import x509
|
| from cryptography.hazmat.primitives import hashes, serialization
|
| from cryptography.hazmat.primitives.asymmetric import rsa
|
| from cryptography.x509.oid import NameOID
|
|
|
|
|
| @dataclass
|
| class KeyPair:
|
| private_key: rsa.RSAPrivateKey
|
| public_key: rsa.RSAPublicKey
|
| certificate: x509.Certificate
|
| entity_id: str
|
|
|
|
|
| def generate_key_pair(entity_id: str, key_size: int = 2048) -> KeyPair:
|
| private_key = rsa.generate_private_key(
|
| public_exponent=65537,
|
| key_size=key_size,
|
| )
|
| public_key = private_key.public_key()
|
|
|
| subject = issuer = x509.Name([
|
| x509.NameAttribute(NameOID.COMMON_NAME, entity_id),
|
| ])
|
|
|
| now = datetime.datetime.utcnow()
|
| cert = (
|
| x509.CertificateBuilder()
|
| .subject_name(subject)
|
| .issuer_name(issuer)
|
| .public_key(public_key)
|
| .serial_number(x509.random_serial_number())
|
| .not_valid_before(now)
|
| .not_valid_after(now + datetime.timedelta(days=3650))
|
| .add_extension(
|
| x509.KeyUsage(
|
| digital_signature=True,
|
| content_commitment=False,
|
| key_encipherment=True,
|
| data_encipherment=False,
|
| key_agreement=False,
|
| key_cert_sign=False,
|
| crl_sign=False,
|
| encipher_only=False,
|
| decipher_only=False,
|
| ),
|
| critical=True,
|
| )
|
| .sign(private_key, hashes.SHA256())
|
| )
|
|
|
| return KeyPair(
|
| private_key=private_key,
|
| public_key=public_key,
|
| certificate=cert,
|
| entity_id=entity_id,
|
| )
|
|
|
|
|
| def save_pem(key_pair: KeyPair, private_path: str, cert_path: str) -> None:
|
| with open(private_path, 'wb') as f:
|
| f.write(key_pair.private_key.private_bytes(
|
| encoding=serialization.Encoding.PEM,
|
| format=serialization.PrivateFormat.PKCS8,
|
| encryption_algorithm=serialization.NoEncryption(),
|
| ))
|
| with open(cert_path, 'wb') as f:
|
| f.write(key_pair.certificate.public_bytes(serialization.Encoding.PEM))
|
|
|
|
|
| def load_private_key(path: str) -> rsa.RSAPrivateKey:
|
| with open(path, 'rb') as f:
|
| return serialization.load_pem_private_key(f.read(), password=None)
|
|
|
|
|
| def load_certificate(path: str) -> x509.Certificate:
|
| with open(path, 'rb') as f:
|
| return x509.load_pem_x509_certificate(f.read())
|
|
|
|
|
| def cert_to_b64(cert: x509.Certificate) -> str:
|
| import base64
|
| der = cert.public_bytes(serialization.Encoding.DER)
|
| return base64.b64encode(der).decode('ascii')
|
|
|