158 lines
5.8 KiB
Python
158 lines
5.8 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Generate a small local CA plus leaf certificates for Phase 2 HTTPS testing.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import ipaddress
|
|
from datetime import datetime, timedelta, timezone
|
|
from pathlib import Path
|
|
|
|
from cryptography import x509
|
|
from cryptography.hazmat.primitives import hashes, serialization
|
|
from cryptography.hazmat.primitives.asymmetric import rsa
|
|
from cryptography.x509.oid import ExtendedKeyUsageOID, NameOID
|
|
|
|
|
|
def build_name(common_name: str) -> x509.Name:
|
|
return x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, common_name)])
|
|
|
|
|
|
def new_private_key() -> rsa.RSAPrivateKey:
|
|
return rsa.generate_private_key(public_exponent=65537, key_size=2048)
|
|
|
|
|
|
def write_private_key(path: Path, key: rsa.RSAPrivateKey) -> None:
|
|
path.write_bytes(
|
|
key.private_bytes(
|
|
encoding=serialization.Encoding.PEM,
|
|
format=serialization.PrivateFormat.TraditionalOpenSSL,
|
|
encryption_algorithm=serialization.NoEncryption(),
|
|
)
|
|
)
|
|
|
|
|
|
def write_cert(path: Path, cert: x509.Certificate) -> None:
|
|
path.write_bytes(cert.public_bytes(serialization.Encoding.PEM))
|
|
|
|
|
|
def parse_sans(names: list[str]) -> list[x509.GeneralName]:
|
|
sans: list[x509.GeneralName] = []
|
|
seen = set()
|
|
for value in names:
|
|
if value in seen:
|
|
continue
|
|
seen.add(value)
|
|
try:
|
|
sans.append(x509.IPAddress(ipaddress.ip_address(value)))
|
|
except ValueError:
|
|
sans.append(x509.DNSName(value))
|
|
return sans
|
|
|
|
|
|
def issue_ca(common_name: str, valid_days: int) -> tuple[rsa.RSAPrivateKey, x509.Certificate]:
|
|
now = datetime.now(timezone.utc)
|
|
key = new_private_key()
|
|
subject = issuer = build_name(common_name)
|
|
cert = (
|
|
x509.CertificateBuilder()
|
|
.subject_name(subject)
|
|
.issuer_name(issuer)
|
|
.public_key(key.public_key())
|
|
.serial_number(x509.random_serial_number())
|
|
.not_valid_before(now - timedelta(minutes=5))
|
|
.not_valid_after(now + timedelta(days=valid_days))
|
|
.add_extension(x509.BasicConstraints(ca=True, path_length=None), critical=True)
|
|
.add_extension(x509.SubjectKeyIdentifier.from_public_key(key.public_key()), critical=False)
|
|
.add_extension(x509.AuthorityKeyIdentifier.from_issuer_public_key(key.public_key()), critical=False)
|
|
.add_extension(x509.KeyUsage(digital_signature=True, key_encipherment=False, key_cert_sign=True, crl_sign=True, content_commitment=False, data_encipherment=False, key_agreement=False, encipher_only=False, decipher_only=False), critical=True)
|
|
.sign(key, hashes.SHA256())
|
|
)
|
|
return key, cert
|
|
|
|
|
|
def issue_leaf(
|
|
ca_key: rsa.RSAPrivateKey,
|
|
ca_cert: x509.Certificate,
|
|
common_name: str,
|
|
san_values: list[str],
|
|
valid_days: int,
|
|
) -> tuple[rsa.RSAPrivateKey, x509.Certificate]:
|
|
now = datetime.now(timezone.utc)
|
|
key = new_private_key()
|
|
cert = (
|
|
x509.CertificateBuilder()
|
|
.subject_name(build_name(common_name))
|
|
.issuer_name(ca_cert.subject)
|
|
.public_key(key.public_key())
|
|
.serial_number(x509.random_serial_number())
|
|
.not_valid_before(now - timedelta(minutes=5))
|
|
.not_valid_after(now + timedelta(days=valid_days))
|
|
.add_extension(x509.BasicConstraints(ca=False, path_length=None), critical=True)
|
|
.add_extension(x509.SubjectAlternativeName(parse_sans(san_values)), critical=False)
|
|
.add_extension(x509.SubjectKeyIdentifier.from_public_key(key.public_key()), critical=False)
|
|
.add_extension(x509.AuthorityKeyIdentifier.from_issuer_public_key(ca_key.public_key()), critical=False)
|
|
.add_extension(x509.ExtendedKeyUsage([ExtendedKeyUsageOID.SERVER_AUTH]), critical=False)
|
|
.add_extension(x509.KeyUsage(digital_signature=True, key_encipherment=True, key_cert_sign=False, crl_sign=False, content_commitment=False, data_encipherment=False, key_agreement=False, encipher_only=False, decipher_only=False), critical=True)
|
|
.sign(ca_key, hashes.SHA256())
|
|
)
|
|
return key, cert
|
|
|
|
|
|
def emit_leaf_bundle(
|
|
out_dir: Path,
|
|
leaf_name: str,
|
|
ca_key: rsa.RSAPrivateKey,
|
|
ca_cert: x509.Certificate,
|
|
san_values: list[str],
|
|
valid_days: int,
|
|
) -> None:
|
|
key, cert = issue_leaf(ca_key, ca_cert, leaf_name, san_values, valid_days)
|
|
write_private_key(out_dir / f"{leaf_name}.key", key)
|
|
write_cert(out_dir / f"{leaf_name}.crt", cert)
|
|
|
|
|
|
def parse_args() -> argparse.Namespace:
|
|
parser = argparse.ArgumentParser(description="Generate local CA and Phase 2 service certificates")
|
|
parser.add_argument("--out-dir", default="tls/phase2")
|
|
parser.add_argument("--valid-days", type=int, default=30)
|
|
parser.add_argument("--ca-common-name", default="ChromeCard Phase2 Local CA")
|
|
parser.add_argument(
|
|
"--proxy-san",
|
|
action="append",
|
|
default=[],
|
|
help="Extra SAN for k_proxy certificate; may be repeated",
|
|
)
|
|
parser.add_argument(
|
|
"--server-san",
|
|
action="append",
|
|
default=[],
|
|
help="Extra SAN for k_server certificate; may be repeated",
|
|
)
|
|
return parser.parse_args()
|
|
|
|
|
|
def main() -> int:
|
|
args = parse_args()
|
|
out_dir = Path(args.out_dir)
|
|
out_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
ca_key, ca_cert = issue_ca(args.ca_common_name, args.valid_days)
|
|
write_private_key(out_dir / "ca.key", ca_key)
|
|
write_cert(out_dir / "ca.crt", ca_cert)
|
|
|
|
proxy_sans = ["localhost", "127.0.0.1", "k_proxy", *args.proxy_san]
|
|
server_sans = ["localhost", "127.0.0.1", "k_server", *args.server_san]
|
|
|
|
emit_leaf_bundle(out_dir, "k_proxy", ca_key, ca_cert, proxy_sans, args.valid_days)
|
|
emit_leaf_bundle(out_dir, "k_server", ca_key, ca_cert, server_sans, args.valid_days)
|
|
|
|
print(f"Generated CA and leaf certificates in {out_dir}")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|