diff --git a/.ci/certs/ca.p12 b/.ci/certs/ca.p12 new file mode 100644 index 0000000..5900f69 Binary files /dev/null and b/.ci/certs/ca.p12 differ diff --git a/.ci/certs/client.p12 b/.ci/certs/client.p12 index 78f604e..b6b4a0f 100644 Binary files a/.ci/certs/client.p12 and b/.ci/certs/client.p12 differ diff --git a/.ci/certs/client_certificate.pem b/.ci/certs/client_certificate.pem index 6a00436..a217a9b 100644 --- a/.ci/certs/client_certificate.pem +++ b/.ci/certs/client_certificate.pem @@ -1,5 +1,5 @@ -----BEGIN CERTIFICATE----- -MIID7zCCAtegAwIBAgIBAjANBgkqhkiG9w0BAQsFADBMMTswOQYDVQQDDDJUTFNH +MIIDvDCCAqSgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBMMTswOQYDVQQDDDJUTFNH ZW5TZWxmU2lnbmVkdFJvb3RDQSAyMDIzLTA5LTExVDIwOjUxOjM5LjYwMzAxMzEN MAsGA1UEBwwEJCQkJDAeFw0yMzA5MTExODUxNDBaFw0zMzA5MDgxODUxNDBaMDYx IzAhBgNVBAMMGmdzYW50b21hZ2c2TFZETS52bXdhcmUuY29tMQ8wDQYDVQQKDAZj @@ -9,16 +9,14 @@ p8kuK93PxM0qsYxvCj5fywaGI2mL9sNibrs6CFtvPL+Rj57LSt5UJHSaH3LmY0CE bV2OdBEuYEBR7eGtzmpupmA+PptHF/U0hTmfIaet6sVLjvJTmD2/3LcztNm/8ksH iqeHgJDUE+ERWUVl7AEcBo1rHDJw+z/jsKEtKbmqoNxsfcdb2UdZw9cJkB5ojKMr l73m35s9uIWZxf2iNd3/tqos7cXMLJcTpwr4x6n6F+PsMhBK5sVTw+kFkq+iyxsu -nSVpVT6nAgMBAAGjgfEwge4wCQYDVR0TBAIwADALBgNVHQ8EBAMCBaAwEwYDVR0l -BAwwCgYIKwYBBQUHAwIwTAYDVR0RBEUwQ4IaZ3NhbnRvbWFnZzZMVkRNLnZtd2Fy -ZS5jb22CGmdzYW50b21hZ2c2TFZETS52bXdhcmUuY29tgglsb2NhbGhvc3QwMQYD -VR0fBCowKDAmoCSgIoYgaHR0cDovL2NybC1zZXJ2ZXI6ODAwMC9iYXNpYy5jcmww -HQYDVR0OBBYEFF+biSCzxAazbay1NaTfGDWawU6dMB8GA1UdIwQYMBaAFCjyUnXF -pZI5+zz7PJxHxDuulqqZMA0GCSqGSIb3DQEBCwUAA4IBAQCREnq62BDzp61MRlzL -lsheI/13hkLutFl+OJAoNGcSgprys7d0zwQJGakCO5o05Csi1pQmP0MCKSyPN2Xb -CTEb1qeDBt3FQkgSzXUCAjVL2wvWoL1nIZaAkD5XDjDvGr5Yd4Eczc7WYwujlT5B -JausVa/ShyYatuiTfgPI7UKASW625fkdi+h30OxQ6vnP+X3FUjOV5NO5/GSrlyFN -Fk0M1YqcypUa9meFooDo2aSMTF8zUuZKsOhFLO9B1z7Io/iAiACdPvjdZWjcpJmI -m+gUWeyMH/R4ql6VlPaitUus+CUWkWtdNuQIZEH8HKR1CIOeCW3xwmIJCK9rnbvI -oGb4 +nSVpVT6nAgMBAAGjgb4wgbswTAYDVR0RBEUwQ4IaZ3NhbnRvbWFnZzZMVkRNLnZt +d2FyZS5jb22CGmdzYW50b21hZ2c2TFZETS52bXdhcmUuY29tgglsb2NhbGhvc3Qw +CQYDVR0TBAIwADAfBgNVHSMEGDAWgBQo8lJ1xaWSOfs8+zycR8Q7rpaqmTAdBgNV +HQ4EFgQUX5uJILPEBrNtrLU1pN8YNZrBTp0wCwYDVR0PBAQDAgWgMBMGA1UdJQQM +MAoGCCsGAQUFBwMCMA0GCSqGSIb3DQEBCwUAA4IBAQAbfphg4JZuCmxoAzXhbDdB +48K98aPVv4ABa6L8BrJcWcXo9H/q0puC2+4LtLNAX7M/hri+UcIWDt2nG+9qkUYN +HXZCfpgZo99FbpfmPlk/m7mBqB6YBFYx94VzcHGTipsOVEfSmOMybf+XW8KPJrXd +ZhsbrAu1ev3k85P8jkv7psc+TKG+HjCG3Rqiy73gMpNvWRQriEO2yOa0sx5Pct+l +x0KBlXNLQc37gddV08BgcTn6fsS72FbjMh0GXbG1mZMCA4DxpRlhwkWqaHCbcfFq +3LKBCvuz/ysAEeJoWug8LKHly8p1Gpelot+tj/lFEYryOr6whJI5xoN6VxNHp67S -----END CERTIFICATE----- diff --git a/.ci/certs/server.p12 b/.ci/certs/server.p12 index 561ebee..cc47a6c 100644 Binary files a/.ci/certs/server.p12 and b/.ci/certs/server.p12 differ diff --git a/.ci/certs/server_certificate.pem b/.ci/certs/server_certificate.pem index dcab80d..f24e28e 100644 --- a/.ci/certs/server_certificate.pem +++ b/.ci/certs/server_certificate.pem @@ -1,24 +1,23 @@ -----BEGIN CERTIFICATE----- -MIID7zCCAtegAwIBAgIBATANBgkqhkiG9w0BAQsFADBMMTswOQYDVQQDDDJUTFNH -ZW5TZWxmU2lnbmVkdFJvb3RDQSAyMDIzLTA5LTExVDIwOjUxOjM5LjYwMzAxMzEN -MAsGA1UEBwwEJCQkJDAeFw0yMzA5MTExODUxNDBaFw0zMzA5MDgxODUxNDBaMDYx -IzAhBgNVBAMMGmdzYW50b21hZ2c2TFZETS52bXdhcmUuY29tMQ8wDQYDVQQKDAZz -ZXJ2ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC6H9gnMoGCmgDN -GXpqgLiIJBmDvbo64P+FsPOvKEYFNKj/Poz2UVVY96kOJRDTBXW3p42C0GCll/2z -/4RcOwN4Jcf4TIU+IsytOyQ39FYNVMDJpMzH4dQPYlvx9euyIqxUccTYCiXtHkrd -xw5cV3gs7HPQLcklQtBgoVNnlf1fPQcPgYPa5x95+oEki2yWhScXa9EP3W6G+KXE -guCi1enoIZ3+MfxbEkfdm+C9Yo47vh6LXcokyKpiuOYk2TGrfaw5JQb1tRwb4BOQ -ORriMCHi6+TkQf58yQ5GRZvJ5sjBeJgLtmCvRJXbdZXcw25jKXPwz74qS1Q728kD -c2k7lgKvAgMBAAGjgfEwge4wCQYDVR0TBAIwADALBgNVHQ8EBAMCBaAwEwYDVR0l -BAwwCgYIKwYBBQUHAwEwTAYDVR0RBEUwQ4IaZ3NhbnRvbWFnZzZMVkRNLnZtd2Fy -ZS5jb22CGmdzYW50b21hZ2c2TFZETS52bXdhcmUuY29tgglsb2NhbGhvc3QwHQYD -VR0OBBYEFG5VGCQucC7FqyOJOTzIYtclS9/SMB8GA1UdIwQYMBaAFCjyUnXFpZI5 -+zz7PJxHxDuulqqZMDEGA1UdHwQqMCgwJqAkoCKGIGh0dHA6Ly9jcmwtc2VydmVy -OjgwMDAvYmFzaWMuY3JsMA0GCSqGSIb3DQEBCwUAA4IBAQB3nWIIa+9Oo29gU0us -fvryYJo92A/mEGIBpixX2i4eQoPhgTSJvFWN3QCHDexbnccM6tRksQmKwn5Rrf+P -DdM8BiTLP/jOQWJXChZro8xpHLmNjlOGletsQ7wo7/p5hvD6Y7pB6FK6LdLcbwbI -Rmvy8olsfOMewEyyWLbKB7e7+iwDIO5lxxgNWXKspO+Kx7wgVeS3j2OhLaOBj1N4 -a+YAXVVaN3IkkdHwUHBTPfuvguXCD8fZxVW5RkYDiweeHAMuwpu3o2rd7y2dGzG7 -u5mLzNazq4Ki/FTSZMkMAloN4/vfXQfGUO4UJcGXB/c3XO9XURsF2N1k0T9ThIUh -bhmL +MIIDzzCCAregAwIBAgIUeRECMRSdARujrzjF1SEBlULbo4IwDQYJKoZIhvcNAQEL +BQAwTDE7MDkGA1UEAwwyVExTR2VuU2VsZlNpZ25lZHRSb290Q0EgMjAyMy0wOS0x +MVQyMDo1MTozOS42MDMwMTMxDTALBgNVBAcMBCQkJCQwHhcNMjUwMjI2MTUyNDA1 +WhcNMjYwMjI2MTUyNDA1WjA2MSMwIQYDVQQDDBpnc2FudG9tYWdnNkxWRE0udm13 +YXJlLmNvbTEPMA0GA1UECgwGc2VydmVyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEAuh/YJzKBgpoAzRl6aoC4iCQZg726OuD/hbDzryhGBTSo/z6M9lFV +WPepDiUQ0wV1t6eNgtBgpZf9s/+EXDsDeCXH+EyFPiLMrTskN/RWDVTAyaTMx+HU +D2Jb8fXrsiKsVHHE2Aol7R5K3ccOXFd4LOxz0C3JJULQYKFTZ5X9Xz0HD4GD2ucf +efqBJItsloUnF2vRD91uhvilxILgotXp6CGd/jH8WxJH3ZvgvWKOO74ei13KJMiq +YrjmJNkxq32sOSUG9bUcG+ATkDka4jAh4uvk5EH+fMkORkWbyebIwXiYC7Zgr0SV +23WV3MNuYylz8M++KktUO9vJA3NpO5YCrwIDAQABo4G+MIG7MEwGA1UdEQRFMEOC +GmdzYW50b21hZ2c2TFZETS52bXdhcmUuY29tghpnc2FudG9tYWdnNkxWRE0udm13 +YXJlLmNvbYIJbG9jYWxob3N0MAkGA1UdEwQCMAAwHwYDVR0jBBgwFoAUKPJSdcWl +kjn7PPs8nEfEO66WqpkwHQYDVR0OBBYEFG5VGCQucC7FqyOJOTzIYtclS9/SMAsG +A1UdDwQEAwIFoDATBgNVHSUEDDAKBggrBgEFBQcDATANBgkqhkiG9w0BAQsFAAOC +AQEAemxaoori7vi3+drMpOSSmeqG/lmtupuoX7ysbYlhZenRPGAOiGTEMaafwp7E +bsBFG+vHajY9bLMesVCbKXPJS5KzvAHq4nIQeI7ravUAKUb4wGin6Hsqma6ac7vc +0U8jVGKStr+Ax9Xvu1YbZLIPyrWXL9d8pDsVogGySXKYU9fBfiohcwvdZTnXnHaY +FkpMcYORJywNulz53jHspIjKTf+jCG/LC4pWsyidmNsDUZlDePVB5wR/ycFUGzdi +OjGRZUvQ2+UNmbLdmzfqIHursON8I/7t22ogeCbY/hX1S29zMpNjzZ4XagWDtLUf +BDnD2qWuKVvdPm9YiRckNlWxNQ== -----END CERTIFICATE----- diff --git a/examples/getting_started/getting_started.py b/examples/getting_started/getting_started.py index 2fb7c2e..589f2e5 100644 --- a/examples/getting_started/getting_started.py +++ b/examples/getting_started/getting_started.py @@ -1,7 +1,7 @@ # type: ignore -from rabbitmq_amqp_python_client import ( # SSlConfigurationContext,; SslConfigurationContext,; ClientCert, +from rabbitmq_amqp_python_client import ( # PosixSSlConfigurationContext,; PosixClientCert, AddressHelper, AMQPMessagingHandler, Connection, @@ -68,7 +68,7 @@ def create_connection(environment: Environment) -> Connection: # client_key = ".ci/certs/client_key.pem" # connection = Connection( # "amqps://guest:guest@localhost:5671/", - # ssl_context=SslConfigurationContext( + # ssl_context=PosixSslConfigurationContext( # ca_cert=ca_cert_file, # client_cert=ClientCert(client_cert=client_cert, client_key=client_key), # ), diff --git a/examples/streams/example_with_streams.py b/examples/streams/example_with_streams.py index 65a4157..8cd41c7 100644 --- a/examples/streams/example_with_streams.py +++ b/examples/streams/example_with_streams.py @@ -1,6 +1,6 @@ # type: ignore -from rabbitmq_amqp_python_client import ( # SSlConfigurationContext,; SslConfigurationContext,; ClientCert, +from rabbitmq_amqp_python_client import ( # PosixSSlConfigurationContext,; PosixClientCert, AddressHelper, AMQPMessagingHandler, Connection, @@ -72,9 +72,9 @@ def create_connection(environment: Environment) -> Connection: # client_key = ".ci/certs/client_key.pem" # connection = Connection( # "amqps://guest:guest@localhost:5671/", - # ssl_context=SslConfigurationContext( + # ssl_context=PosixSslConfigurationContext( # ca_cert=ca_cert_file, - # client_cert=ClientCert(client_cert=client_cert, client_key=client_key), + # client_cert=PosixClientCert(client_cert=client_cert, client_key=client_key), # ), # ) connection.dial() diff --git a/examples/tls/tls_example.py b/examples/tls/tls_example.py index 80d419c..c6eb0af 100644 --- a/examples/tls/tls_example.py +++ b/examples/tls/tls_example.py @@ -1,18 +1,27 @@ # type: ignore +import sys +from traceback import print_exception - -from rabbitmq_amqp_python_client import ( # SSlConfigurationContext,; SslConfigurationContext,; ClientCert, +from rabbitmq_amqp_python_client import ( AddressHelper, AMQPMessagingHandler, - ClientCert, Connection, + CurrentUserStore, Environment, Event, ExchangeSpecification, ExchangeToQueueBindingSpecification, + LocalMachineStore, Message, + PKCS12Store, + PosixClientCert, + PosixSslConfigurationContext, QuorumQueueSpecification, - SslConfigurationContext, + WinClientCert, + WinSslConfigurationContext, +) +from rabbitmq_amqp_python_client.ssl_configuration import ( + FriendlyName, ) messages_to_publish = 100 @@ -74,20 +83,75 @@ def main() -> None: exchange_name = "test-exchange" queue_name = "example-queue" routing_key = "routing-key" + ca_p12_store = ".ci/certs/ca.p12" ca_cert_file = ".ci/certs/ca_certificate.pem" client_cert = ".ci/certs/client_certificate.pem" client_key = ".ci/certs/client_key.pem" + client_p12_store = ".ci/certs/client.p12" + uri = "amqps://guest:guest@localhost:5671/" + + if sys.platform == "win32": + ca_stores = [ + # names for the current user and local machine are not + # case-sensitive + CurrentUserStore(name="Root"), + LocalMachineStore(name="Root"), + PKCS12Store(path=ca_p12_store), + ] + client_stores = [ + # `personal` is treated as an alias for `my` by qpid proton + # Recommended read: + # https://github.com/apache/qpid-proton/blob/2847000fbb3732e80537e3c3ff5e097bb95bfae0/c/src/ssl/PLATFORM_NOTES.md + CurrentUserStore(name="Personal"), + LocalMachineStore(name="my"), + PKCS12Store(path=client_p12_store), + ] + + for ca_store, client_store in zip(ca_stores, client_stores): + ssl_context = WinSslConfigurationContext( + ca_store=ca_store, + client_cert=WinClientCert( + store=client_store, + # qpid proton uses Windows constant CERT_NAME_FRIENDLY_DISPLAY_TYPE + # to retrieve the value which is compare to the one we provide + # If certificates have no friendly name Windows falls back to + # CERT_NAME_SIMPLE_DISPLAY_TYPE which has further fallbacks + # https://learn.microsoft.com/en-us/windows/win32/api/wincrypt/nf-wincrypt-certgetnamestringa + disambiguation_method=FriendlyName("1"), + password=None, + ), + ) + environment = Environment( + uri, + ssl_context=ssl_context, + ) + + try: + print("connection to amqp server") + connection = create_connection(environment) + break + except Exception as e: + print_exception(e) + continue + else: + raise RuntimeError( + "connection failed. working directory should be project root" + ) + else: + environment = Environment( + uri, + ssl_context=PosixSslConfigurationContext( + ca_cert=ca_cert_file, + client_cert=PosixClientCert( + client_cert=client_cert, + client_key=client_key, + password=None, + ), + ), + ) - environment = Environment( - "amqps://guest:guest@localhost:5671/", - ssl_context=SslConfigurationContext( - ca_cert=ca_cert_file, - client_cert=ClientCert(client_cert=client_cert, client_key=client_key), - ), - ) - - print("connection to amqp server") - connection = create_connection(environment) + print("connection to amqp server") + connection = create_connection(environment) management = connection.management() diff --git a/rabbitmq_amqp_python_client/__init__.py b/rabbitmq_amqp_python_client/__init__.py index bc4afb7..72742eb 100644 --- a/rabbitmq_amqp_python_client/__init__.py +++ b/rabbitmq_amqp_python_client/__init__.py @@ -32,8 +32,13 @@ StreamSpecification, ) from .ssl_configuration import ( - ClientCert, - SslConfigurationContext, + CurrentUserStore, + LocalMachineStore, + PKCS12Store, + PosixClientCert, + PosixSslConfigurationContext, + WinClientCert, + WinSslConfigurationContext, ) try: @@ -69,8 +74,13 @@ "AMQPMessagingHandler", "ArgumentOutOfRangeException", "ValidationCodeException", - "SslConfigurationContext", - "ClientCert", + "PosixSslConfigurationContext", + "WinSslConfigurationContext", + "PosixClientCert", + "WinClientCert", + "LocalMachineStore", + "CurrentUserStore", + "PKCS12Store", "ConnectionClosed", "StreamOptions", "OffsetSpecification", diff --git a/rabbitmq_amqp_python_client/connection.py b/rabbitmq_amqp_python_client/connection.py index 553e9e6..49c0064 100644 --- a/rabbitmq_amqp_python_client/connection.py +++ b/rabbitmq_amqp_python_client/connection.py @@ -1,5 +1,13 @@ import logging -from typing import Annotated, Callable, Optional, TypeVar +from typing import ( + Annotated, + Callable, + Optional, + TypeVar, + Union, +) + +import typing_extensions from .address_helper import validate_address from .consumer import Consumer @@ -10,7 +18,15 @@ from .qpid.proton._handlers import MessagingHandler from .qpid.proton._transport import SSLDomain from .qpid.proton.utils import BlockingConnection -from .ssl_configuration import SslConfigurationContext +from .ssl_configuration import ( + CurrentUserStore, + FriendlyName, + LocalMachineStore, + PKCS12Store, + PosixSslConfigurationContext, + Unambiguous, + WinSslConfigurationContext, +) logger = logging.getLogger(__name__) @@ -34,7 +50,9 @@ def __init__( uri: Optional[str] = None, # multi-node mode uris: Optional[list[str]] = None, - ssl_context: Optional[SslConfigurationContext] = None, + ssl_context: Union[ + PosixSslConfigurationContext, WinSslConfigurationContext, None + ] = None, on_disconnection_handler: Optional[CB] = None, # type: ignore ): """ @@ -60,7 +78,9 @@ def __init__( self._conn: BlockingConnection self._management: Management self._on_disconnection_handler = on_disconnection_handler - self._conf_ssl_context: Optional[SslConfigurationContext] = ssl_context + self._conf_ssl_context: Union[ + PosixSslConfigurationContext, WinSslConfigurationContext, None + ] = ssl_context self._ssl_domain = None self._connections = [] # type: ignore self._index: int = -1 @@ -80,17 +100,47 @@ def dial(self) -> None: logger.debug("Enabling SSL") self._ssl_domain = SSLDomain(SSLDomain.MODE_CLIENT) - if self._ssl_domain is not None: - self._ssl_domain.set_trusted_ca_db(self._conf_ssl_context.ca_cert) + assert self._ssl_domain + + if isinstance(self._conf_ssl_context, PosixSslConfigurationContext): + ca_cert = self._conf_ssl_context.ca_cert + elif isinstance(self._conf_ssl_context, WinSslConfigurationContext): + ca_cert = self._win_store_to_cert(self._conf_ssl_context.ca_store) + else: + typing_extensions.assert_never(self._conf_ssl_context) + self._ssl_domain.set_trusted_ca_db(ca_cert) + # for mutual authentication if self._conf_ssl_context.client_cert is not None: logger.debug("Enabling mutual authentication as well") - if self._ssl_domain is not None: - self._ssl_domain.set_credentials( - self._conf_ssl_context.client_cert.client_cert, - self._conf_ssl_context.client_cert.client_key, - self._conf_ssl_context.client_cert.password, + + if isinstance(self._conf_ssl_context, PosixSslConfigurationContext): + client_cert = self._conf_ssl_context.client_cert.client_cert + client_key = self._conf_ssl_context.client_cert.client_key + password = self._conf_ssl_context.client_cert.password + elif isinstance(self._conf_ssl_context, WinSslConfigurationContext): + client_cert = self._win_store_to_cert( + self._conf_ssl_context.client_cert.store ) + disambiguation_method = ( + self._conf_ssl_context.client_cert.disambiguation_method + ) + if isinstance(disambiguation_method, Unambiguous): + client_key = None + elif isinstance(disambiguation_method, FriendlyName): + client_key = disambiguation_method.name + else: + typing_extensions.assert_never(disambiguation_method) + + password = self._conf_ssl_context.client_cert.password + else: + typing_extensions.assert_never(self._conf_ssl_context) + + self._ssl_domain.set_credentials( + client_cert, + client_key, + password, + ) self._conn = BlockingConnection( url=self._addr, urls=self._addrs, @@ -100,6 +150,19 @@ def dial(self) -> None: self._open() logger.debug("Connection to the server established") + def _win_store_to_cert( + self, store: Union[LocalMachineStore, CurrentUserStore, PKCS12Store] + ) -> str: + if isinstance(store, LocalMachineStore): + ca_cert = f"lmss:{store.name}" + elif isinstance(store, CurrentUserStore): + ca_cert = f"ss:{store.name}" + elif isinstance(store, PKCS12Store): + ca_cert = store.path + else: + typing_extensions.assert_never(store) + return ca_cert + def _open(self) -> None: self._management = Management(self._conn) self._management.open() diff --git a/rabbitmq_amqp_python_client/environment.py b/rabbitmq_amqp_python_client/environment.py index 2a04b42..ad7be8b 100644 --- a/rabbitmq_amqp_python_client/environment.py +++ b/rabbitmq_amqp_python_client/environment.py @@ -1,9 +1,18 @@ # For the moment this is just a Connection pooler to keep compatibility with other clients import logging -from typing import Annotated, Callable, Optional, TypeVar +from typing import ( + Annotated, + Callable, + Optional, + TypeVar, + Union, +) from .connection import Connection -from .ssl_configuration import SslConfigurationContext +from .ssl_configuration import ( + PosixSslConfigurationContext, + WinSslConfigurationContext, +) logger = logging.getLogger(__name__) @@ -28,7 +37,9 @@ def __init__( uri: Optional[str] = None, # multi-node mode uris: Optional[list[str]] = None, - ssl_context: Optional[SslConfigurationContext] = None, + ssl_context: Union[ + PosixSslConfigurationContext, WinSslConfigurationContext, None + ] = None, on_disconnection_handler: Optional[CB] = None, # type: ignore ): """ diff --git a/rabbitmq_amqp_python_client/qpid/proton/_transport.py b/rabbitmq_amqp_python_client/qpid/proton/_transport.py index aeefb1d..d4a1702 100644 --- a/rabbitmq_amqp_python_client/qpid/proton/_transport.py +++ b/rabbitmq_amqp_python_client/qpid/proton/_transport.py @@ -826,7 +826,7 @@ def __init__(self, mode: int) -> None: def _check(self, err: int) -> int: if err < 0: exc = EXCEPTIONS.get(err, SSLException) - raise exc("SSL failure.") + raise exc("SSL failure.", err) else: return err diff --git a/rabbitmq_amqp_python_client/ssl_configuration.py b/rabbitmq_amqp_python_client/ssl_configuration.py index e272988..eea30b8 100644 --- a/rabbitmq_amqp_python_client/ssl_configuration.py +++ b/rabbitmq_amqp_python_client/ssl_configuration.py @@ -1,15 +1,57 @@ from dataclasses import dataclass -from typing import Optional +from typing import Optional, Union @dataclass -class ClientCert: +class PosixClientCert: client_cert: str client_key: str password: Optional[str] = None @dataclass -class SslConfigurationContext: +class Unambiguous: + """Use the only certificate in the store.""" + + ... + + +@dataclass +class FriendlyName: + """Use the first certificate with a matching friendly name.""" + + name: str + + +@dataclass +class LocalMachineStore: + name: str + + +@dataclass +class CurrentUserStore: + name: str + + +@dataclass +class PKCS12Store: + path: str + + +@dataclass +class WinClientCert: + store: Union[LocalMachineStore, CurrentUserStore, PKCS12Store] + disambiguation_method: Union[Unambiguous, FriendlyName] + password: Optional[str] = None + + +@dataclass +class PosixSslConfigurationContext: ca_cert: str - client_cert: Optional[ClientCert] = None + client_cert: Union[PosixClientCert, WinClientCert, None] = None + + +@dataclass +class WinSslConfigurationContext: + ca_store: Union[LocalMachineStore, CurrentUserStore, PKCS12Store] + client_cert: Union[PosixClientCert, WinClientCert, None] = None diff --git a/tests/conftest.py b/tests/conftest.py index d772f78..4df3ff6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,3 +1,5 @@ +import os +import sys from typing import Optional import pytest @@ -5,12 +7,20 @@ from rabbitmq_amqp_python_client import ( AddressHelper, AMQPMessagingHandler, - ClientCert, Environment, Event, - SslConfigurationContext, + PKCS12Store, + PosixClientCert, + PosixSslConfigurationContext, + WinClientCert, + WinSslConfigurationContext, symbol, ) +from rabbitmq_amqp_python_client.ssl_configuration import ( + FriendlyName, +) + +os.chdir(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) @pytest.fixture() @@ -36,17 +46,31 @@ def connection(pytestconfig): @pytest.fixture() -def connection_ssl(pytestconfig): - ca_cert_file = ".ci/certs/ca_certificate.pem" - client_cert = ".ci/certs/client_certificate.pem" - client_key = ".ci/certs/client_key.pem" +def ssl_context(pytestconfig): + if sys.platform == "win32": + return WinSslConfigurationContext( + ca_store=PKCS12Store(path=".ci/certs/ca.p12"), + client_cert=WinClientCert( + store=PKCS12Store(path=".ci/certs/client.p12"), + disambiguation_method=FriendlyName(name="1"), + ), + ) + else: + return PosixSslConfigurationContext( + ca_cert=".ci/certs/ca_certificate.pem", + client_cert=PosixClientCert( + client_cert=".ci/certs/client_certificate.pem", + client_key=".ci/certs/client_key.pem", + ), + ) + + +@pytest.fixture() +def connection_ssl(pytestconfig, ssl_context): environment = Environment( "amqps://guest:guest@localhost:5671/", - ssl_context=SslConfigurationContext( - ca_cert=ca_cert_file, - client_cert=ClientCert(client_cert=client_cert, client_key=client_key), - ), + ssl_context=ssl_context, ) connection = environment.connection() connection.dial() diff --git a/tests/test_connection.py b/tests/test_connection.py index ec8b8ac..e006fd7 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -1,10 +1,8 @@ import time from rabbitmq_amqp_python_client import ( - ClientCert, ConnectionClosed, Environment, - SslConfigurationContext, StreamSpecification, ) @@ -31,17 +29,10 @@ def test_environment_context_manager() -> None: connection.dial() -def test_connection_ssl() -> None: - ca_cert_file = ".ci/certs/ca_certificate.pem" - client_cert = ".ci/certs/client_certificate.pem" - client_key = ".ci/certs/client_key.pem" - +def test_connection_ssl(ssl_context) -> None: environment = Environment( "amqps://guest:guest@localhost:5671/", - ssl_context=SslConfigurationContext( - ca_cert=ca_cert_file, - client_cert=ClientCert(client_cert=client_cert, client_key=client_key), - ), + ssl_context=ssl_context, ) connection = environment.connection()