You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@qpid.apache.org by as...@apache.org on 2025/12/03 17:58:02 UTC
(qpid-proton) 01/04: PROTON-2909: Add python scripting to make test certs
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 <as...@apache.org>
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: commits-unsubscribe@qpid.apache.org
For additional commands, e-mail: commits-help@qpid.apache.org