This is an automated email from the ASF dual-hosted git repository. astitcher pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/qpid-proton.git
commit 34035ca47813fa0d9a6d472326300794fbbf8a39 Author: Andrew Stitcher <[email protected]> AuthorDate: Tue Dec 2 17:43:28 2025 -0500 PROTON-2909: Add python scripting to make test certs This code was written with the assistance of Cursor running the Claude 4.5 opus high model. --- tools/certs/certgen.py | 175 ++++++++++++++++++++++++++++++++++++++++ tools/certs/make_certs_basic.py | 63 +++++++++++++++ 2 files changed, 238 insertions(+) diff --git a/tools/certs/certgen.py b/tools/certs/certgen.py new file mode 100644 index 000000000..55adf2e52 --- /dev/null +++ b/tools/certs/certgen.py @@ -0,0 +1,175 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# + +"""Certificate generation helpers built on trustme. + +Provides a simplified interface for generating test certificates using the +trustme library (MIT/Apache 2.0 licensed). Adds PKCS12 export functionality +that trustme does not provide directly. +""" +from __future__ import annotations + +from pathlib import Path +from typing import List, Optional + +import trustme +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.serialization import pkcs12 +from cryptography.x509 import load_pem_x509_certificate + + +class CertificateAuthority: + """A certificate authority that can issue certificates.""" + + def __init__(self, organization_name: str = "Trust Me Inc."): + self._ca = trustme.CA( + organization_name=organization_name, + key_type=trustme.KeyType.RSA, + ) + + @property + def cert_pem(self) -> bytes: + """The CA certificate in PEM format.""" + return self._ca.cert_pem.bytes() + + @property + def private_key_pem(self) -> bytes: + """The CA private key in PEM format (unencrypted).""" + return self._ca.private_key_pem.bytes() + + def write_cert_pem(self, path: Path) -> None: + """Write the CA certificate to a PEM file.""" + self._ca.cert_pem.write_to_path(path) + + def issue_cert( + self, + common_name: Optional[str] = None, + san_dns: Optional[List[str]] = None, + ) -> Certificate: + """Issue a certificate signed by this CA. + + Args: + common_name: The common name for the certificate. + san_dns: Optional list of DNS names for Subject Alternative Name. + Note: DNS names must be valid (no underscores). + + Returns: + A Certificate object. + """ + identities = san_dns or [] + leaf = self._ca.issue_cert( + *identities, + common_name=common_name, + key_type=trustme.KeyType.RSA, + ) + return Certificate(leaf, self._ca.cert_pem.bytes()) + + +class Certificate: + """An issued certificate with its private key.""" + + def __init__(self, leaf: trustme.LeafCert, ca_cert_pem: bytes): + self._leaf = leaf + self._ca_cert_pem = ca_cert_pem + + @property + def cert_pem(self) -> bytes: + """The certificate in PEM format.""" + return self._leaf.cert_chain_pems[0].bytes() + + @property + def private_key_pem(self) -> bytes: + """The private key in PEM format (unencrypted).""" + return self._leaf.private_key_pem.bytes() + + def write_cert_pem(self, path: Path) -> None: + """Write the certificate to a PEM file.""" + self._leaf.cert_chain_pems[0].write_to_path(path) + + def write_private_key_pem( + self, + path: Path, + password: Optional[bytes] = None, + ) -> None: + """Write the private key to a PEM file. + + Args: + path: Destination file path. + password: Optional password to encrypt the key. + """ + if password: + # Load the unencrypted key and re-serialize with encryption + key = serialization.load_pem_private_key( + self._leaf.private_key_pem.bytes(), + password=None, + ) + enc = serialization.BestAvailableEncryption(password) + data = key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=enc, + ) + path.write_bytes(data) + else: + self._leaf.private_key_pem.write_to_path(path) + + def write_pkcs12( + self, + path: Path, + name: str, + password: Optional[bytes] = None, + include_key: bool = True, + include_ca: bool = False, + ) -> None: + """Write the certificate (and optionally key/CA) to a PKCS12 file. + + Args: + path: Destination file path (.p12). + name: Friendly name for the certificate. + password: Optional password to encrypt the bundle. + include_key: If True, include the private key. + include_ca: If True, include the CA certificate. + """ + cert = load_pem_x509_certificate(self.cert_pem) + + key = None + if include_key: + key = serialization.load_pem_private_key( + self._leaf.private_key_pem.bytes(), + password=None, + ) + + cas = None + if include_ca: + cas = [load_pem_x509_certificate(self._ca_cert_pem)] + + enc = ( + serialization.NoEncryption() + if password is None + else serialization.BestAvailableEncryption(password) + ) + + p12_data = pkcs12.serialize_key_and_certificates( + name.encode() if name else None, + key, + cert, + cas, + enc, + ) + path.write_bytes(p12_data) diff --git a/tools/certs/make_certs_basic.py b/tools/certs/make_certs_basic.py new file mode 100644 index 000000000..163fa994c --- /dev/null +++ b/tools/certs/make_certs_basic.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# + +"""Generate small 'tserver/tclient' cert bundle for testing. + +Usage: make_certs_basic.py [OUT_DIR] + +Generates certificates using the trustme library (MIT/Apache 2.0 licensed). +""" +from __future__ import annotations + +import sys +from pathlib import Path + +from certgen import CertificateAuthority + + +def main(argv: list[str]) -> int: + out = Path(argv[1]) if len(argv) > 1 else Path(".") + out.mkdir(parents=True, exist_ok=True) + + # Create CA + ca = CertificateAuthority(organization_name="Small.CA.com") + ca.write_cert_pem(out / "ca-certificate.pem") + + # Server certificate - use common_name only (SANs require valid DNS names) + # The C tests verify against "test_server" which has underscore (invalid DNS char) + server = ca.issue_cert(common_name="test_server") + server.write_cert_pem(out / "tserver-certificate.pem") + server.write_private_key_pem(out / "tserver-private-key.pem", password=b"tserverpw") + server.write_pkcs12(out / "tserver-certificate.p12", "tserver", include_key=False, include_ca=True) + server.write_pkcs12(out / "tserver-full.p12", "tserver", password=b"tserverpw", include_key=True) + + # Client certificate + client = ca.issue_cert(common_name="test_client") + client.write_cert_pem(out / "tclient-certificate.pem") + client.write_private_key_pem(out / "tclient-private-key.pem", password=b"tclientpw") + client.write_pkcs12(out / "tclient-certificate.p12", "tclient", include_key=False, include_ca=True) + client.write_pkcs12(out / "tclient-full.p12", "tclient", password=b"tclientpw", include_key=True) + + print("Basic certificate bundle generated in:", str(out)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv)) --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
