acemq_amqp.devcerts
Certificates for a broker on a laptop, and only for a broker on a laptop::
pip install "acemq-amqp[crypto]"
python -m acemq_amqp.devcerts --directory certs --broker localhost
An authority, a broker certificate and a client certificate, written as PEM
alongside a rabbitmq.conf that points the broker at them. The alternative is
six openssl invocations with a hand-written extensions file, which is how
people end up developing against a plaintext broker instead — and then finding
out on the day TLS is turned on that nothing was ever tested through it.
Everything written here carries
~acemq_amqp.security.DEVELOPMENT_MARKER — ACEMQ DEVELOPMENT ONLY - DO
NOT TRUST — in its subject organisation, and the TLS code in
acemq_amqp.security refuses any certificate carrying it, whatever the
trust settings say and including ~acemq_amqp.security.without_verifying_the_broker().
That is not a warning in a docstring somebody has to read. It is the mechanism:
a generated authority's private key sits in the same directory as its
certificate and usually ends up in a repository, so a certificate that could
reach production would be an authority anybody who can read the repository can
issue against.
Java, Go, Ruby and .NET generate the same three certificates with the same marker
and enforce it the same way, so a broker set up by any of the five is usable from
all of them and none of them will speak to it without being told the certificates
are development ones. The file names match Go's acemq-certs exactly — ca.crt,
ca.key, server.crt, server.key, client.crt, client.key — so
this is a drop-in replacement for it in a script such as tls-broker.sh.
The keys are ECDSA on P-256, which is what Go writes. Java uses RSA-4096 and .NET RSA-2048; nothing reading these certificates cares, and P-256 keeps generation instant, which matters for something that runs at the top of a test script.
Needs the crypto extra, for the same reason
acemq_amqp.codecs.encrypted does: the standard library has hashes and
HMAC and does not have an X.509 builder.
1# Copyright 2026 AceMQ. 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# https://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14 15"""Certificates for a broker on a laptop, and only for a broker on a laptop:: 16 17 pip install "acemq-amqp[crypto]" 18 19 python -m acemq_amqp.devcerts --directory certs --broker localhost 20 21An authority, a broker certificate and a client certificate, written as PEM 22alongside a ``rabbitmq.conf`` that points the broker at them. The alternative is 23six ``openssl`` invocations with a hand-written extensions file, which is how 24people end up developing against a plaintext broker instead — and then finding 25out on the day TLS is turned on that nothing was ever tested through it. 26 27Everything written here carries 28:data:`~acemq_amqp.security.DEVELOPMENT_MARKER` — ``ACEMQ DEVELOPMENT ONLY - DO 29NOT TRUST`` — in its subject organisation, and **the TLS code in 30:mod:`acemq_amqp.security` refuses any certificate carrying it**, whatever the 31trust settings say and including :func:`~acemq_amqp.security.without_verifying_the_broker`. 32That is not a warning in a docstring somebody has to read. It is the mechanism: 33a generated authority's private key sits in the same directory as its 34certificate and usually ends up in a repository, so a certificate that could 35reach production would be an authority anybody who can read the repository can 36issue against. 37 38Java, Go, Ruby and .NET generate the same three certificates with the same marker 39and enforce it the same way, so a broker set up by any of the five is usable from 40all of them and none of them will speak to it without being told the certificates 41are development ones. The file names match Go's ``acemq-certs`` exactly — ``ca.crt``, 42``ca.key``, ``server.crt``, ``server.key``, ``client.crt``, ``client.key`` — so 43this is a drop-in replacement for it in a script such as ``tls-broker.sh``. 44 45The keys are ECDSA on P-256, which is what Go writes. Java uses RSA-4096 and 46.NET RSA-2048; nothing reading these certificates cares, and P-256 keeps 47generation instant, which matters for something that runs at the top of a test 48script. 49 50Needs the ``crypto`` extra, for the same reason 51:mod:`acemq_amqp.codecs.encrypted` does: the standard library has hashes and 52HMAC and does not have an X.509 builder. 53""" 54 55from __future__ import annotations 56 57import argparse 58import datetime as dt 59import ipaddress 60import os 61import secrets 62import sys 63from dataclasses import dataclass, field 64from pathlib import Path 65from typing import Any 66 67from .errors import AceMQError 68from .security import DEVELOPMENT_MARKER 69 70#: What the authority calls itself. The same string in Java, Go, Ruby and .NET. 71AUTHORITY_NAME = "AceMQ development CA" 72 73#: What the client certificate calls itself. Java, Go and Ruby all write this; 74#: .NET writes ``acemq-dev-client``, and nothing reads either. 75CLIENT_NAME = "acemq-client" 76 77#: How long a generated certificate lasts, in days. 78#: 79#: Thirty, matching Go and Java. Long enough not to interrupt a piece of work and 80#: short enough that a set of these cannot quietly become load-bearing: a 81#: development certificate that outlives the reason it was made is a development 82#: certificate somebody eventually deploys. 83DEFAULT_VALIDITY_DAYS = 30 84 85#: Where the broker will find the certificates *inside its container*, which is 86#: rarely where they were written on the host. 87DEFAULT_BROKER_CERTIFICATE_PATH = "/certs" 88 89 90@dataclass(frozen=True, slots=True) 91class GeneratedCertificates: 92 """What :func:`generate` wrote. 93 94 :param directory: where the files are 95 :param authority: the ``ca.crt`` path 96 :param authority_key: the ``ca.key`` path 97 :param server: the ``server.crt`` path 98 :param server_key: the ``server.key`` path 99 :param client: the ``client.crt`` path 100 :param client_key: the ``client.key`` path 101 :param broker_configuration: the ``rabbitmq.conf`` path, or ``None`` when one 102 was not asked for 103 :param expires: when all three certificates stop being valid, in UTC 104 :param broker_host: the name the broker certificate is valid for 105 :param marker: the string stamped into every subject, so a caller can print 106 it without importing it from somewhere else 107 """ 108 109 directory: Path 110 authority: Path 111 authority_key: Path 112 server: Path 113 server_key: Path 114 client: Path 115 client_key: Path 116 broker_configuration: Path | None 117 expires: dt.datetime 118 broker_host: str 119 marker: str = DEVELOPMENT_MARKER 120 files: tuple[Path, ...] = field(default_factory=tuple) 121 122 123def generate( 124 directory: str | os.PathLike[str] = "certs", 125 *, 126 broker_host: str = "localhost", 127 validity_days: int = DEFAULT_VALIDITY_DAYS, 128 broker_certificate_path: str = DEFAULT_BROKER_CERTIFICATE_PATH, 129 write_broker_configuration: bool = True, 130) -> GeneratedCertificates: 131 """Writes an authority, a broker certificate and a client certificate. 132 133 :param directory: where to write them. Created if it is not there 134 :param broker_host: the name the broker certificate is valid for. An IP 135 address is recognised and written as one, because a certificate that 136 names ``10.0.0.4`` in its DNS names is valid for a host called 137 ``10.0.0.4`` and for no address at all 138 :param validity_days: how long they last 139 :param broker_certificate_path: where the broker will find them, which for a 140 broker in a container is a path on the container's filesystem rather than 141 this one. Only used in the generated ``rabbitmq.conf`` 142 :param write_broker_configuration: whether to write that ``rabbitmq.conf`` 143 :returns: what was written 144 :raises AceMQError: when ``cryptography`` is not installed, when 145 ``validity_days`` is not at least a day, or when a file cannot be written 146 """ 147 if validity_days < 1: 148 raise AceMQError( 149 f"acemq: a certificate has to last at least a day and {validity_days} is not one" 150 ) 151 152 x509, ec, hashes, serialization = _cryptography() 153 154 where = Path(directory) 155 try: 156 where.mkdir(parents=True, exist_ok=True) 157 except OSError as unwritable: 158 raise AceMQError(f"acemq: cannot create {str(where)!r}: {unwritable}") from unwritable 159 160 now = dt.datetime.now(dt.timezone.utc) 161 # An hour of slack at the front, because a container's clock and the host's 162 # are not the same clock and a certificate that is not valid yet fails in a 163 # way that reads exactly like a certificate that is wrong. 164 valid_from = now - dt.timedelta(hours=1) 165 valid_until = now + dt.timedelta(days=validity_days) 166 167 authority_key = ec.generate_private_key(ec.SECP256R1()) 168 authority = ( 169 _builder(x509, AUTHORITY_NAME, AUTHORITY_NAME, valid_from, valid_until, authority_key) 170 .add_extension(x509.BasicConstraints(ca=True, path_length=0), critical=True) 171 .add_extension( 172 x509.KeyUsage( 173 digital_signature=True, 174 content_commitment=False, 175 key_encipherment=False, 176 data_encipherment=False, 177 key_agreement=False, 178 key_cert_sign=True, 179 crl_sign=True, 180 encipher_only=False, 181 decipher_only=False, 182 ), 183 critical=True, 184 ) 185 .sign(authority_key, hashes.SHA256()) 186 ) 187 188 server_key = ec.generate_private_key(ec.SECP256R1()) 189 server = _leaf( 190 x509, 191 hashes, 192 common_name=broker_host, 193 key=server_key, 194 authority=authority, 195 authority_key=authority_key, 196 valid_from=valid_from, 197 valid_until=valid_until, 198 purpose=x509.oid.ExtendedKeyUsageOID.SERVER_AUTH, 199 names=_names_for(x509, broker_host), 200 ) 201 202 client_key = ec.generate_private_key(ec.SECP256R1()) 203 client = _leaf( 204 x509, 205 hashes, 206 common_name=CLIENT_NAME, 207 key=client_key, 208 authority=authority, 209 authority_key=authority_key, 210 valid_from=valid_from, 211 valid_until=valid_until, 212 purpose=x509.oid.ExtendedKeyUsageOID.CLIENT_AUTH, 213 names=None, 214 ) 215 216 written: list[Path] = [] 217 for name, contents, mode in ( 218 ("ca.crt", _certificate_pem(serialization, authority), 0o644), 219 ("ca.key", _key_pem(serialization, authority_key), 0o600), 220 ("server.crt", _certificate_pem(serialization, server), 0o644), 221 ("server.key", _key_pem(serialization, server_key), 0o600), 222 ("client.crt", _certificate_pem(serialization, client), 0o644), 223 ("client.key", _key_pem(serialization, client_key), 0o600), 224 ): 225 written.append(_write(where / name, contents, mode)) 226 227 configuration: Path | None = None 228 if write_broker_configuration: 229 configuration = _write( 230 where / "rabbitmq.conf", 231 broker_configuration(broker_certificate_path).encode("utf-8"), 232 0o644, 233 ) 234 written.append(configuration) 235 236 return GeneratedCertificates( 237 directory=where, 238 authority=where / "ca.crt", 239 authority_key=where / "ca.key", 240 server=where / "server.crt", 241 server_key=where / "server.key", 242 client=where / "client.crt", 243 client_key=where / "client.key", 244 broker_configuration=configuration, 245 expires=valid_until, 246 broker_host=broker_host, 247 files=tuple(written), 248 ) 249 250 251def broker_configuration(certificate_path: str = DEFAULT_BROKER_CERTIFICATE_PATH) -> str: 252 """A ``rabbitmq.conf`` pointing the broker at the generated certificates. 253 254 ``verify_peer`` with ``fail_if_no_peer_cert = false``, which is the setting 255 that lets one broker serve both cases: a client presenting no certificate 256 logs in with a password, and one presenting a certificate has it checked 257 against the authority. Turning ``fail_if_no_peer_cert`` on is what makes it 258 mutual TLS, and is a one-line change here rather than a different broker. 259 260 The plaintext listener stays on so the same broker can be reached either way 261 while somebody is working out which of the two is broken. 262 263 :param certificate_path: where the broker will find the files, on its own 264 filesystem 265 :returns: the configuration file's contents 266 """ 267 return f"""# Written by acemq_amqp.devcerts. Development only. 268listeners.tcp.default = 5672 269listeners.ssl.default = 5671 270 271ssl_options.cacertfile = {certificate_path}/ca.crt 272ssl_options.certfile = {certificate_path}/server.crt 273ssl_options.keyfile = {certificate_path}/server.key 274 275# A client that presents a certificate has it checked; one that presents none 276# logs in with a password. Setting fail_if_no_peer_cert to true is what turns 277# this into mutual TLS. 278ssl_options.verify = verify_peer 279ssl_options.fail_if_no_peer_cert = false 280""" 281 282 283def _cryptography() -> tuple[Any, Any, Any, Any]: 284 """The four pieces of ``cryptography`` this module uses, imported lazily.""" 285 try: 286 from cryptography import x509 287 from cryptography.hazmat.primitives import hashes, serialization 288 from cryptography.hazmat.primitives.asymmetric import ec 289 except ImportError as missing: # pragma: no cover - depends on the install 290 raise AceMQError( 291 "acemq: generating development certificates needs cryptography, which is an " 292 'optional extra. Install it with pip install "acemq-amqp[crypto]"' 293 ) from missing 294 return x509, ec, hashes, serialization 295 296 297def _subject(x509: Any, common_name: str) -> Any: 298 """A distinguished name carrying the marker in its organisation. 299 300 ``O=`` rather than ``OU=``, which is where Go and .NET put it. Java uses 301 ``OU=`` and every library detects the marker by searching the whole 302 distinguished name, so the two are interchangeable in practice — but two of 303 the three write ``O=`` and the difference is not worth having. 304 """ 305 return x509.Name( 306 [ 307 x509.NameAttribute(x509.oid.NameOID.COMMON_NAME, common_name), 308 x509.NameAttribute(x509.oid.NameOID.ORGANIZATION_NAME, DEVELOPMENT_MARKER), 309 ] 310 ) 311 312 313def _builder( 314 x509: Any, 315 subject: str, 316 issuer: str, 317 valid_from: dt.datetime, 318 valid_until: dt.datetime, 319 key: Any, 320) -> Any: 321 return ( 322 x509.CertificateBuilder() 323 .subject_name(_subject(x509, subject)) 324 .issuer_name(_subject(x509, issuer)) 325 .public_key(key.public_key()) 326 .serial_number(int.from_bytes(secrets.token_bytes(16), "big") >> 1 or 1) 327 .not_valid_before(valid_from) 328 .not_valid_after(valid_until) 329 ) 330 331 332def _leaf( 333 x509: Any, 334 hashes: Any, 335 *, 336 common_name: str, 337 key: Any, 338 authority: Any, 339 authority_key: Any, 340 valid_from: dt.datetime, 341 valid_until: dt.datetime, 342 purpose: Any, 343 names: Any, 344) -> Any: 345 builder = ( 346 x509.CertificateBuilder() 347 .subject_name(_subject(x509, common_name)) 348 .issuer_name(authority.subject) 349 .public_key(key.public_key()) 350 .serial_number(int.from_bytes(secrets.token_bytes(16), "big") >> 1 or 1) 351 .not_valid_before(valid_from) 352 .not_valid_after(valid_until) 353 .add_extension(x509.BasicConstraints(ca=False, path_length=None), critical=True) 354 .add_extension( 355 x509.KeyUsage( 356 digital_signature=True, 357 content_commitment=False, 358 key_encipherment=True, 359 data_encipherment=False, 360 key_agreement=False, 361 key_cert_sign=False, 362 crl_sign=False, 363 encipher_only=False, 364 decipher_only=False, 365 ), 366 critical=True, 367 ) 368 # One purpose each, as Go and .NET write them: the broker certificate is 369 # for serving and the client one for authenticating. Java gives both to 370 # both, which works and says less about what each is for. 371 .add_extension(x509.ExtendedKeyUsage([purpose]), critical=False) 372 ) 373 if names is not None: 374 builder = builder.add_extension(names, critical=False) 375 return builder.sign(authority_key, hashes.SHA256()) 376 377 378def _names_for(x509: Any, broker_host: str) -> Any: 379 """The names the broker certificate is valid for. 380 381 An IP address goes in as an address rather than a DNS name — a certificate 382 listing ``10.0.0.4`` under DNS is valid for a *host called* ``10.0.0.4``, 383 which nothing is, and the connection fails for a reason that reads like the 384 certificate being wrong rather than like this. 385 386 ``localhost`` picks up both loopback addresses, because half the tooling 387 dials the name and the other half dials ``127.0.0.1``. ``rabbitmq`` comes 388 along too, which is what the broker is called on a Docker network and is what 389 Java adds for the same reason. 390 """ 391 try: 392 address = ipaddress.ip_address(broker_host) 393 except ValueError: 394 pass 395 else: 396 return x509.SubjectAlternativeName([x509.IPAddress(address)]) 397 398 names = [x509.DNSName(broker_host)] 399 if broker_host != "localhost": 400 names.append(x509.DNSName("localhost")) 401 if broker_host != "rabbitmq": 402 names.append(x509.DNSName("rabbitmq")) 403 names.append(x509.IPAddress(ipaddress.ip_address("127.0.0.1"))) 404 names.append(x509.IPAddress(ipaddress.ip_address("::1"))) 405 return x509.SubjectAlternativeName(names) 406 407 408def _certificate_pem(serialization: Any, certificate: Any) -> bytes: 409 written: bytes = certificate.public_bytes(serialization.Encoding.PEM) 410 return written 411 412 413def _key_pem(serialization: Any, key: Any) -> bytes: 414 written: bytes = key.private_bytes( 415 encoding=serialization.Encoding.PEM, 416 format=serialization.PrivateFormat.PKCS8, 417 # Unencrypted, because a passphrase held in the same script that reads 418 # the key protects nothing, and one held anywhere else turns a 419 # development broker into something with a setup step. 420 encryption_algorithm=serialization.NoEncryption(), 421 ) 422 return written 423 424 425def _write(path: Path, contents: bytes, mode: int) -> Path: 426 """Writes a file, then narrows it. Private keys end up ``0600``. 427 428 Narrowed after writing rather than by opening with the mode, because an 429 existing file keeps the permissions it already had and a key regenerated over 430 a world-readable one would quietly stay world-readable. 431 """ 432 try: 433 path.write_bytes(contents) 434 os.chmod(path, mode) 435 except OSError as unwritable: 436 raise AceMQError(f"acemq: cannot write {str(path)!r}: {unwritable}") from unwritable 437 return path 438 439 440def main(argv: list[str] | None = None) -> int: 441 """``python -m acemq_amqp.devcerts``. 442 443 :param argv: the arguments, or ``None`` to read :data:`sys.argv` 444 :returns: the process exit status 445 """ 446 parser = argparse.ArgumentParser( 447 prog="python -m acemq_amqp.devcerts", 448 description=( 449 "Write a development certificate authority, a broker certificate and a client " 450 "certificate. Everything is stamped " 451 f'"{DEVELOPMENT_MARKER}" and the AceMQ libraries refuse it however trust is ' 452 "configured, which is what keeps it off anything real." 453 ), 454 ) 455 parser.add_argument( 456 "--directory", "-d", default="certs", help="where to write them (default: certs)" 457 ) 458 parser.add_argument( 459 "--broker", 460 "-b", 461 default="localhost", 462 help="the name or address the broker certificate is valid for (default: localhost)", 463 ) 464 parser.add_argument( 465 "--days", 466 type=int, 467 default=DEFAULT_VALIDITY_DAYS, 468 help=f"how long they last (default: {DEFAULT_VALIDITY_DAYS})", 469 ) 470 parser.add_argument( 471 "--broker-certificate-path", 472 default=DEFAULT_BROKER_CERTIFICATE_PATH, 473 help=( 474 "where the broker will find them on its own filesystem, for the generated " 475 f"rabbitmq.conf (default: {DEFAULT_BROKER_CERTIFICATE_PATH})" 476 ), 477 ) 478 parser.add_argument( 479 "--no-broker-config", 480 action="store_true", 481 help="do not write rabbitmq.conf", 482 ) 483 arguments = parser.parse_args(argv) 484 485 try: 486 result = generate( 487 arguments.directory, 488 broker_host=arguments.broker, 489 validity_days=arguments.days, 490 broker_certificate_path=arguments.broker_certificate_path, 491 write_broker_configuration=not arguments.no_broker_config, 492 ) 493 except AceMQError as failure: 494 print(failure, file=sys.stderr) 495 return 1 496 497 for path in result.files: 498 print(path) 499 print( 500 f'valid for {result.broker_host} until {result.expires:%Y-%m-%d}. Stamped "' 501 f'{result.marker}", so every AceMQ library refuses these unless it is told to ' 502 "allow them.", 503 file=sys.stderr, 504 ) 505 return 0 506 507 508if __name__ == "__main__": # pragma: no cover - the entry point itself 509 raise SystemExit(main())
91@dataclass(frozen=True, slots=True) 92class GeneratedCertificates: 93 """What :func:`generate` wrote. 94 95 :param directory: where the files are 96 :param authority: the ``ca.crt`` path 97 :param authority_key: the ``ca.key`` path 98 :param server: the ``server.crt`` path 99 :param server_key: the ``server.key`` path 100 :param client: the ``client.crt`` path 101 :param client_key: the ``client.key`` path 102 :param broker_configuration: the ``rabbitmq.conf`` path, or ``None`` when one 103 was not asked for 104 :param expires: when all three certificates stop being valid, in UTC 105 :param broker_host: the name the broker certificate is valid for 106 :param marker: the string stamped into every subject, so a caller can print 107 it without importing it from somewhere else 108 """ 109 110 directory: Path 111 authority: Path 112 authority_key: Path 113 server: Path 114 server_key: Path 115 client: Path 116 client_key: Path 117 broker_configuration: Path | None 118 expires: dt.datetime 119 broker_host: str 120 marker: str = DEVELOPMENT_MARKER 121 files: tuple[Path, ...] = field(default_factory=tuple)
What generate() wrote.
Parameters
- directory: where the files are
- authority: the
ca.crtpath - authority_key: the
ca.keypath - server: the
server.crtpath - server_key: the
server.keypath - client: the
client.crtpath - client_key: the
client.keypath - broker_configuration: the
rabbitmq.confpath, orNonewhen one was not asked for - expires: when all three certificates stop being valid, in UTC
- broker_host: the name the broker certificate is valid for
- marker: the string stamped into every subject, so a caller can print it without importing it from somewhere else
124def generate( 125 directory: str | os.PathLike[str] = "certs", 126 *, 127 broker_host: str = "localhost", 128 validity_days: int = DEFAULT_VALIDITY_DAYS, 129 broker_certificate_path: str = DEFAULT_BROKER_CERTIFICATE_PATH, 130 write_broker_configuration: bool = True, 131) -> GeneratedCertificates: 132 """Writes an authority, a broker certificate and a client certificate. 133 134 :param directory: where to write them. Created if it is not there 135 :param broker_host: the name the broker certificate is valid for. An IP 136 address is recognised and written as one, because a certificate that 137 names ``10.0.0.4`` in its DNS names is valid for a host called 138 ``10.0.0.4`` and for no address at all 139 :param validity_days: how long they last 140 :param broker_certificate_path: where the broker will find them, which for a 141 broker in a container is a path on the container's filesystem rather than 142 this one. Only used in the generated ``rabbitmq.conf`` 143 :param write_broker_configuration: whether to write that ``rabbitmq.conf`` 144 :returns: what was written 145 :raises AceMQError: when ``cryptography`` is not installed, when 146 ``validity_days`` is not at least a day, or when a file cannot be written 147 """ 148 if validity_days < 1: 149 raise AceMQError( 150 f"acemq: a certificate has to last at least a day and {validity_days} is not one" 151 ) 152 153 x509, ec, hashes, serialization = _cryptography() 154 155 where = Path(directory) 156 try: 157 where.mkdir(parents=True, exist_ok=True) 158 except OSError as unwritable: 159 raise AceMQError(f"acemq: cannot create {str(where)!r}: {unwritable}") from unwritable 160 161 now = dt.datetime.now(dt.timezone.utc) 162 # An hour of slack at the front, because a container's clock and the host's 163 # are not the same clock and a certificate that is not valid yet fails in a 164 # way that reads exactly like a certificate that is wrong. 165 valid_from = now - dt.timedelta(hours=1) 166 valid_until = now + dt.timedelta(days=validity_days) 167 168 authority_key = ec.generate_private_key(ec.SECP256R1()) 169 authority = ( 170 _builder(x509, AUTHORITY_NAME, AUTHORITY_NAME, valid_from, valid_until, authority_key) 171 .add_extension(x509.BasicConstraints(ca=True, path_length=0), critical=True) 172 .add_extension( 173 x509.KeyUsage( 174 digital_signature=True, 175 content_commitment=False, 176 key_encipherment=False, 177 data_encipherment=False, 178 key_agreement=False, 179 key_cert_sign=True, 180 crl_sign=True, 181 encipher_only=False, 182 decipher_only=False, 183 ), 184 critical=True, 185 ) 186 .sign(authority_key, hashes.SHA256()) 187 ) 188 189 server_key = ec.generate_private_key(ec.SECP256R1()) 190 server = _leaf( 191 x509, 192 hashes, 193 common_name=broker_host, 194 key=server_key, 195 authority=authority, 196 authority_key=authority_key, 197 valid_from=valid_from, 198 valid_until=valid_until, 199 purpose=x509.oid.ExtendedKeyUsageOID.SERVER_AUTH, 200 names=_names_for(x509, broker_host), 201 ) 202 203 client_key = ec.generate_private_key(ec.SECP256R1()) 204 client = _leaf( 205 x509, 206 hashes, 207 common_name=CLIENT_NAME, 208 key=client_key, 209 authority=authority, 210 authority_key=authority_key, 211 valid_from=valid_from, 212 valid_until=valid_until, 213 purpose=x509.oid.ExtendedKeyUsageOID.CLIENT_AUTH, 214 names=None, 215 ) 216 217 written: list[Path] = [] 218 for name, contents, mode in ( 219 ("ca.crt", _certificate_pem(serialization, authority), 0o644), 220 ("ca.key", _key_pem(serialization, authority_key), 0o600), 221 ("server.crt", _certificate_pem(serialization, server), 0o644), 222 ("server.key", _key_pem(serialization, server_key), 0o600), 223 ("client.crt", _certificate_pem(serialization, client), 0o644), 224 ("client.key", _key_pem(serialization, client_key), 0o600), 225 ): 226 written.append(_write(where / name, contents, mode)) 227 228 configuration: Path | None = None 229 if write_broker_configuration: 230 configuration = _write( 231 where / "rabbitmq.conf", 232 broker_configuration(broker_certificate_path).encode("utf-8"), 233 0o644, 234 ) 235 written.append(configuration) 236 237 return GeneratedCertificates( 238 directory=where, 239 authority=where / "ca.crt", 240 authority_key=where / "ca.key", 241 server=where / "server.crt", 242 server_key=where / "server.key", 243 client=where / "client.crt", 244 client_key=where / "client.key", 245 broker_configuration=configuration, 246 expires=valid_until, 247 broker_host=broker_host, 248 files=tuple(written), 249 )
Writes an authority, a broker certificate and a client certificate.
Parameters
- directory: where to write them. Created if it is not there
- broker_host: the name the broker certificate is valid for. An IP
address is recognised and written as one, because a certificate that
names
10.0.0.4in its DNS names is valid for a host called10.0.0.4and for no address at all - validity_days: how long they last
- broker_certificate_path: where the broker will find them, which for a
broker in a container is a path on the container's filesystem rather than
this one. Only used in the generated
rabbitmq.conf - write_broker_configuration: whether to write that
rabbitmq.conf:returns: what was written
Raises
- AceMQError: when
cryptographyis not installed, whenvalidity_daysis not at least a day, or when a file cannot be written
252def broker_configuration(certificate_path: str = DEFAULT_BROKER_CERTIFICATE_PATH) -> str: 253 """A ``rabbitmq.conf`` pointing the broker at the generated certificates. 254 255 ``verify_peer`` with ``fail_if_no_peer_cert = false``, which is the setting 256 that lets one broker serve both cases: a client presenting no certificate 257 logs in with a password, and one presenting a certificate has it checked 258 against the authority. Turning ``fail_if_no_peer_cert`` on is what makes it 259 mutual TLS, and is a one-line change here rather than a different broker. 260 261 The plaintext listener stays on so the same broker can be reached either way 262 while somebody is working out which of the two is broken. 263 264 :param certificate_path: where the broker will find the files, on its own 265 filesystem 266 :returns: the configuration file's contents 267 """ 268 return f"""# Written by acemq_amqp.devcerts. Development only. 269listeners.tcp.default = 5672 270listeners.ssl.default = 5671 271 272ssl_options.cacertfile = {certificate_path}/ca.crt 273ssl_options.certfile = {certificate_path}/server.crt 274ssl_options.keyfile = {certificate_path}/server.key 275 276# A client that presents a certificate has it checked; one that presents none 277# logs in with a password. Setting fail_if_no_peer_cert to true is what turns 278# this into mutual TLS. 279ssl_options.verify = verify_peer 280ssl_options.fail_if_no_peer_cert = false 281"""
A rabbitmq.conf pointing the broker at the generated certificates.
verify_peer with fail_if_no_peer_cert = false, which is the setting
that lets one broker serve both cases: a client presenting no certificate
logs in with a password, and one presenting a certificate has it checked
against the authority. Turning fail_if_no_peer_cert on is what makes it
mutual TLS, and is a one-line change here rather than a different broker.
The plaintext listener stays on so the same broker can be reached either way while somebody is working out which of the two is broken.
Parameters
- certificate_path: where the broker will find the files, on its own filesystem :returns: the configuration file's contents
441def main(argv: list[str] | None = None) -> int: 442 """``python -m acemq_amqp.devcerts``. 443 444 :param argv: the arguments, or ``None`` to read :data:`sys.argv` 445 :returns: the process exit status 446 """ 447 parser = argparse.ArgumentParser( 448 prog="python -m acemq_amqp.devcerts", 449 description=( 450 "Write a development certificate authority, a broker certificate and a client " 451 "certificate. Everything is stamped " 452 f'"{DEVELOPMENT_MARKER}" and the AceMQ libraries refuse it however trust is ' 453 "configured, which is what keeps it off anything real." 454 ), 455 ) 456 parser.add_argument( 457 "--directory", "-d", default="certs", help="where to write them (default: certs)" 458 ) 459 parser.add_argument( 460 "--broker", 461 "-b", 462 default="localhost", 463 help="the name or address the broker certificate is valid for (default: localhost)", 464 ) 465 parser.add_argument( 466 "--days", 467 type=int, 468 default=DEFAULT_VALIDITY_DAYS, 469 help=f"how long they last (default: {DEFAULT_VALIDITY_DAYS})", 470 ) 471 parser.add_argument( 472 "--broker-certificate-path", 473 default=DEFAULT_BROKER_CERTIFICATE_PATH, 474 help=( 475 "where the broker will find them on its own filesystem, for the generated " 476 f"rabbitmq.conf (default: {DEFAULT_BROKER_CERTIFICATE_PATH})" 477 ), 478 ) 479 parser.add_argument( 480 "--no-broker-config", 481 action="store_true", 482 help="do not write rabbitmq.conf", 483 ) 484 arguments = parser.parse_args(argv) 485 486 try: 487 result = generate( 488 arguments.directory, 489 broker_host=arguments.broker, 490 validity_days=arguments.days, 491 broker_certificate_path=arguments.broker_certificate_path, 492 write_broker_configuration=not arguments.no_broker_config, 493 ) 494 except AceMQError as failure: 495 print(failure, file=sys.stderr) 496 return 1 497 498 for path in result.files: 499 print(path) 500 print( 501 f'valid for {result.broker_host} until {result.expires:%Y-%m-%d}. Stamped "' 502 f'{result.marker}", so every AceMQ library refuses these unless it is told to ' 503 "allow them.", 504 file=sys.stderr, 505 ) 506 return 0
python -m acemq_amqp.devcerts.
Parameters
- argv: the arguments, or
Noneto readsys.argv:returns: the process exit status