From 708623d6d1dd80b3ca4ba1f76a88ff785985b4bc Mon Sep 17 00:00:00 2001 From: Johan Lundberg Date: Thu, 30 Nov 2023 12:02:24 +0100 Subject: [PATCH] implement CA cert auth flow --- src/auth_server/api.py | 7 +- src/auth_server/cert_utils.py | 183 ++++++++++++++-- src/auth_server/config.py | 3 +- src/auth_server/db/transaction_state.py | 3 +- src/auth_server/flows.py | 40 ++-- src/auth_server/models/claims.py | 1 + src/auth_server/tests/data/ca/README | 12 ++ src/auth_server/tests/data/ca/bolag_a.crt | 36 ++++ src/auth_server/tests/data/ca/bolag_b.crt | 36 ++++ src/auth_server/tests/data/ca/bolag_c.crt | 36 ++++ src/auth_server/tests/data/ca/bolag_e.crt | 36 ++++ .../data/ca/ca_cert/ExpiTrust-Test-CA-v8.crt | Bin 0 -> 1370 bytes .../SwedishPublicSectorFunctionCAv3SAT.crt | Bin 0 -> 1868 bytes .../ca/ca_cert/testsithseidfunctioncav1.cer | Bin 0 -> 1664 bytes src/auth_server/tests/test_ca_flow.py | 201 ++++++++++++++++++ 15 files changed, 557 insertions(+), 37 deletions(-) create mode 100644 src/auth_server/tests/data/ca/README create mode 100644 src/auth_server/tests/data/ca/bolag_a.crt create mode 100644 src/auth_server/tests/data/ca/bolag_b.crt create mode 100644 src/auth_server/tests/data/ca/bolag_c.crt create mode 100644 src/auth_server/tests/data/ca/bolag_e.crt create mode 100644 src/auth_server/tests/data/ca/ca_cert/ExpiTrust-Test-CA-v8.crt create mode 100644 src/auth_server/tests/data/ca/ca_cert/SwedishPublicSectorFunctionCAv3SAT.crt create mode 100644 src/auth_server/tests/data/ca/ca_cert/testsithseidfunctioncav1.cer create mode 100644 src/auth_server/tests/test_ca_flow.py diff --git a/src/auth_server/api.py b/src/auth_server/api.py index 43279e4..24973e1 100644 --- a/src/auth_server/api.py +++ b/src/auth_server/api.py @@ -8,7 +8,7 @@ from auth_server.config import AuthServerConfig, ConfigurationError, FlowName, load_config from auth_server.context import ContextRequestRoute -from auth_server.flows import BaseAuthFlow, ConfigFlow, InteractionFlow, MDQFlow, TestFlow, TLSFEDFlow +from auth_server.flows import BaseAuthFlow, CAFlow, ConfigFlow, InteractionFlow, MDQFlow, TestFlow, TLSFEDFlow from auth_server.log import init_logging from auth_server.middleware import JOSEMiddleware from auth_server.routers.interaction import interaction_router @@ -28,10 +28,11 @@ def __init__(self): # Load flows self.builtin_flow: Dict[FlowName, Type[BaseAuthFlow]] = { - FlowName.TESTFLOW: TestFlow, - FlowName.INTERACTIONFLOW: InteractionFlow, + FlowName.CAFLOW: CAFlow, FlowName.CONFIGFLOW: ConfigFlow, + FlowName.INTERACTIONFLOW: InteractionFlow, FlowName.MDQFLOW: MDQFlow, + FlowName.TESTFLOW: TestFlow, FlowName.TLSFEDFLOW: TLSFEDFlow, } self.auth_flows = self.load_flows(config=config) diff --git a/src/auth_server/cert_utils.py b/src/auth_server/cert_utils.py index e9547ea..b25c326 100644 --- a/src/auth_server/cert_utils.py +++ b/src/auth_server/cert_utils.py @@ -1,15 +1,18 @@ # -*- coding: utf-8 -*- __author__ = "lundberg" +from base64 import b64encode from datetime import datetime +from enum import Enum from functools import lru_cache from pathlib import Path from typing import Optional from cryptography.exceptions import InvalidSignature +from cryptography.hazmat.bindings._rust import ObjectIdentifier from cryptography.hazmat.primitives._serialization import Encoding from cryptography.hazmat.primitives.hashes import SHA256 -from cryptography.x509 import Certificate, load_der_x509_certificate, load_pem_x509_certificate +from cryptography.x509 import Certificate, ExtensionNotFound, load_der_x509_certificate, load_pem_x509_certificate from loguru import logger from pki_tools import Certificate as PKIToolCertificate from pki_tools import Chain @@ -18,77 +21,217 @@ from auth_server.config import ConfigurationError, load_config +OID_ORGANIZATION_NAME = ObjectIdentifier("2.5.4.10") +OID_COMMON_NAME = ObjectIdentifier("2.5.4.3") +OID_SERIAL_NUMBER = ObjectIdentifier("2.5.4.5") +OID_ENHANCED_KEY_USAGE_CLIENT_AUTHENTICATION = ObjectIdentifier("1.3.6.1.5.5.7.3.2") + + +class SupportedOrgIdCA(str, Enum): + EFOS = "Swedish Social Insurance Agency" + EXPITRUST = "Expisoft AB" + SITHS = "Inera AB" + def cert_within_validity_period(cert: Certificate) -> bool: """ check if certificate is within the validity period """ - cert_fingerprint = cert.fingerprint(SHA256()) + cert_fingerprint = rfc8705_fingerprint(cert) now = datetime.utcnow() if now < cert.not_valid_before: - logger.error(f"Certificate {cert_fingerprint!r} not valid before {cert.not_valid_before}") + logger.error(f"Certificate {cert_fingerprint} not valid before {cert.not_valid_before}") return False if now > cert.not_valid_after: - logger.error(f"Certificate {cert_fingerprint!r} not valid after {cert.not_valid_after}") + logger.error(f"Certificate {cert_fingerprint} not valid after {cert.not_valid_after}") return False return True -def cert_signed_by_ca(cert: Certificate) -> Optional[Certificate]: +def cert_signed_by_ca(cert: Certificate) -> Optional[str]: """ check if the cert is signed by any on our loaded CA certs """ - cert_fingerprint = cert.fingerprint(SHA256()) - for ca_cert in load_ca_certs(): + cert_fingerprint = rfc8705_fingerprint(cert) + for ca_name, ca_cert in load_ca_certs().items(): try: cert.verify_directly_issued_by(ca_cert) - logger.debug(f"Certificate {cert_fingerprint!r} signed by CA cert {ca_cert.fingerprint(SHA256())!r}") - return ca_cert + logger.debug(f"Certificate {cert_fingerprint} signed by CA cert {ca_name}") + return ca_name except (ValueError, TypeError, InvalidSignature): continue - logger.error(f"Certificate {cert_fingerprint!r} did NOT match any loaded CA cert") + logger.error(f"Certificate {cert_fingerprint} did NOT match any loaded CA cert") return None -async def is_cert_revoked(cert: Certificate, ca_cert: Certificate) -> bool: +async def is_cert_revoked(cert: Certificate, ca_name: str) -> bool: """ check if cert is revoked """ + ca_cert = load_ca_certs().get(ca_name) + if ca_cert is None: + raise ConfigurationError(f"CA cert {ca_name} not found") try: return is_revoked( cert=PKIToolCertificate.from_cryptography(cert=cert), chain=Chain.from_cryptography([ca_cert]) ) except PKIToolsError as e: - logger.error(f"Certificate {cert.fingerprint(SHA256())!r} failed revoke check: {e}") + logger.error(f"Certificate {rfc8705_fingerprint(cert)} failed revoke check: {e}") return True +def get_org_id_from_cert(cert: Certificate, ca_name: str) -> Optional[str]: + ca_cert = load_ca_certs().get(ca_name) + if not ca_cert: + raise ConfigurationError(f"CA cert {ca_name} not found") + try: + ca_org_name = ca_cert.issuer.get_attributes_for_oid(OID_ORGANIZATION_NAME)[0].value + except IndexError: + logger.error(f"CA certificate {ca_name} has no org name") + return None + try: + supported_ca = SupportedOrgIdCA(ca_org_name) + except ValueError: + logger.info(f"CA {ca_name} is not supported for org id extraction") + return None + + if supported_ca is SupportedOrgIdCA.EXPITRUST: + return get_org_id_expitrust(cert=cert) + elif supported_ca is SupportedOrgIdCA.EFOS: + return get_org_id_efos(cert=cert) + elif supported_ca is SupportedOrgIdCA.SITHS: + return get_org_id_siths(cert=cert) + else: + logger.info(f"CA {ca_name} / {ca_org_name} is not implemented for org id extraction") + return None + + +def get_org_id_expitrust(cert: Certificate) -> Optional[str]: + """ + The org number is just the serial number of the certificate. + """ + cert_fingerprint = rfc8705_fingerprint(cert) + try: + ret = cert.subject.get_attributes_for_oid(OID_SERIAL_NUMBER)[0].value + if isinstance(ret, bytes): + ret = ret.decode("utf-8") + return ret + except IndexError: + logger.error(f"certificate {cert_fingerprint} has no subject serial number") + return None + + +def get_org_id_siths(cert: Certificate) -> Optional[str]: + """ + The org number is the first part of the serial number of the certificate with a prefix of SE. + ex. SE5565594230-AAAA -> 5565594230 + """ + cert_fingerprint = rfc8705_fingerprint(cert) + + # Check that the certificate has enhancedKeyUsage clientAuthentication + try: + cert.extensions.get_extension_for_oid(OID_ENHANCED_KEY_USAGE_CLIENT_AUTHENTICATION) + except ExtensionNotFound: + logger.error(f"certificate {cert_fingerprint} has no enhancedKeyUsage clientAuthentication") + return None + + # Check that the certificate has a subject serial number and parse the org id + try: + serial_number = cert.subject.get_attributes_for_oid(OID_SERIAL_NUMBER)[0].value + if isinstance(serial_number, bytes): + serial_number = serial_number.decode("utf-8") + org_id, _ = serial_number.split("-") + return org_id.removeprefix("SE") + except IndexError: + logger.error(f"certificate {cert_fingerprint} has no subject serial number") + return None + + +def get_org_id_efos(cert: Certificate): + """ + The org number is the first part of the serial number of the certificate with a prefix of EFOS16. + ex. EFOS165565594230-012345 -> 5565594230 + """ + cert_fingerprint = rfc8705_fingerprint(cert) + # Check that the certificate has a subject serial number and parse the org id + try: + serial_number = cert.subject.get_attributes_for_oid(OID_SERIAL_NUMBER)[0].value + if isinstance(serial_number, bytes): + serial_number = serial_number.decode("utf-8") + org_id, _ = serial_number.split("-") + return org_id.removeprefix("EFOS16") + except IndexError: + logger.error(f"certificate {cert_fingerprint} has no subject serial number") + return None + + +def get_subject_cn(cert: Certificate) -> Optional[str]: + cert_fingerprint = rfc8705_fingerprint(cert) + try: + ret = cert.subject.get_attributes_for_oid(OID_COMMON_NAME)[0].value + if isinstance(ret, bytes): + ret = ret.decode("utf-8") + return ret + except IndexError: + logger.error(f"certificate {cert_fingerprint} has no subject common name") + return None + + +def get_issuer_cn(ca_name: str) -> Optional[str]: + ca_cert = load_ca_certs().get(ca_name) + if ca_cert is None: + logger.error(f"CA {ca_name} not found") + return None + try: + ret = ca_cert.subject.get_attributes_for_oid(OID_COMMON_NAME)[0].value + if isinstance(ret, bytes): + ret = ret.decode("utf-8") + return ret + except IndexError: + logger.error(f"CA {ca_name} has no subject common name") + return None + + @lru_cache() -def load_ca_certs() -> list[Certificate]: +def load_ca_certs() -> dict[str, Certificate]: config = load_config() if config.ca_certs_path is None: raise ConfigurationError("no CA certs path specified in config") - certs = [] + certs = {} path = Path(config.ca_certs_path) - for crt in path.glob("**/*.crt"): + for crt in path.glob("**/*.c*"): # match .crt and .cer files + if crt.is_dir(): + continue try: with open(crt, "rb") as f: content = f.read() try: - certs.append(load_pem_x509_certificate(content)) + cert = load_pem_x509_certificate(content) except ValueError: - certs.append(load_der_x509_certificate(content)) + cert = load_der_x509_certificate(content) + if cert_within_validity_period(cert): + certs[cert.subject.rfc4514_string()] = cert except (IOError, ValueError) as e: logger.error(f"Failed to load CA cert {crt}: {e}") + logger.info(f"Loaded {len(certs)} CA certs") + logger.debug(f"Certs loaded: {certs.keys()}") return certs -def load_cert_from_str(cert: str) -> Certificate: +def load_pem_from_str(cert: str) -> Certificate: if not cert.startswith("-----BEGIN CERTIFICATE-----"): cert = f"-----BEGIN CERTIFICATE-----\n{cert}\n-----END CERTIFICATE-----" return load_pem_x509_certificate(cert.encode()) -def serialize_certificate(cert: Certificate) -> str: - return cert.public_bytes(encoding=Encoding.PEM).decode("utf-8") +def serialize_certificate(cert: Certificate, encoding: Encoding = Encoding.PEM) -> str: + public_bytes = cert.public_bytes(encoding=encoding) + if encoding == Encoding.DER: + return b64encode(public_bytes).decode("ascii") + else: + return public_bytes.decode("ascii") + + +def rfc8705_fingerprint(cert: Certificate): + return b64encode(cert.fingerprint(algorithm=SHA256())).decode("ascii") diff --git a/src/auth_server/config.py b/src/auth_server/config.py index 8068419..e5a0182 100644 --- a/src/auth_server/config.py +++ b/src/auth_server/config.py @@ -30,9 +30,10 @@ class Environment(str, Enum): class FlowName(str, Enum): + CAFLOW = "CAFlow" CONFIGFLOW = "ConfigFlow" - MDQFLOW = "MDQFlow" INTERACTIONFLOW = "InteractionFlow" + MDQFLOW = "MDQFlow" TESTFLOW = "TestFlow" TLSFEDFLOW = "TLSFEDFlow" diff --git a/src/auth_server/db/transaction_state.py b/src/auth_server/db/transaction_state.py index fd2fa43..4b24d10 100644 --- a/src/auth_server/db/transaction_state.py +++ b/src/auth_server/db/transaction_state.py @@ -99,8 +99,9 @@ class TLSFEDState(TransactionState): class CAState(TransactionState): auth_source: AuthSource = AuthSource.CA + issuer_common_name: Optional[str] = None + client_common_name: Optional[str] = None organization_id: Optional[str] = None - ca: Optional[str] = None class TransactionStateDB(BaseDB): diff --git a/src/auth_server/flows.py b/src/auth_server/flows.py index dc1f80b..04bf3b3 100644 --- a/src/auth_server/flows.py +++ b/src/auth_server/flows.py @@ -9,7 +9,15 @@ from jwcrypto.jwk import JWK from loguru import logger -from auth_server.cert_utils import cert_signed_by_ca, cert_within_validity_period, is_cert_revoked +from auth_server.cert_utils import ( + cert_signed_by_ca, + cert_within_validity_period, + get_issuer_cn, + get_org_id_from_cert, + get_subject_cn, + is_cert_revoked, + load_pem_from_str, +) from auth_server.config import AuthServerConfig from auth_server.context import ContextRequest from auth_server.db.transaction_state import ( @@ -553,7 +561,7 @@ async def create_claims(self) -> MDQClaims: source = self.config.mdq_server # Default source to mdq server if registrationAuthority is not set base_claims = await super().create_claims() - return MDQClaims(**base_claims.dict(exclude_none=True), entity_id=entity_id, scopes=scopes, source=source) + return MDQClaims(**base_claims.model_dump(exclude_none=True), entity_id=entity_id, scopes=scopes, source=source) class TLSFEDFlow(OnlyMTLSProofFlow): @@ -614,25 +622,33 @@ async def validate_proof(self) -> Optional[GrantResponse]: if not self.state.proof_ok: raise NextFlowException(status_code=401, detail="client certificate does not match grant request") - if not cert_within_validity_period(cert=self.request.context.client_cert): - raise NextFlowException(status_code=401, detail="client certificate expired") + client_cert = load_pem_from_str(self.request.context.client_cert) + if not cert_within_validity_period(cert=client_cert): + raise StopTransactionException(status_code=401, detail="client certificate expired or not yet valid") + + ca_name = cert_signed_by_ca(cert=client_cert) + if ca_name is None: + raise StopTransactionException(status_code=401, detail="client certificate not signed by CA") - ca_cert = cert_signed_by_ca(cert=self.request.context.client_cert) - if ca_cert is None: - raise NextFlowException(status_code=401, detail="client certificate not signed by CA") + if await is_cert_revoked(cert=client_cert, ca_name=ca_name) is True: + raise StopTransactionException(status_code=401, detail="client certificate revoked") - if await is_cert_revoked(cert=self.request.context.client_cert, ca_cert=ca_cert) is True: - raise NextFlowException(status_code=401, detail="client certificate revoked") + # set client CN and issuer CN in state for use in claims + self.state.client_common_name = get_subject_cn(cert=client_cert) + self.state.issuer_common_name = get_issuer_cn(ca_name=ca_name) + # try to get an organization id from the client certificate + self.state.organization_id = get_org_id_from_cert(cert=client_cert, ca_name=ca_name) return None async def create_claims(self) -> CAClaims: - if not self.state.organization_id: - raise NextFlowException(status_code=400, detail="missing organization id") + if self.config.ca_certs_mandatory_org_id and self.state.organization_id is None: + raise StopTransactionException(status_code=401, detail="missing organization id in client certificate") base_claims = await super().create_claims() return CAClaims( **base_claims.model_dump(exclude_none=True), organization_id=self.state.organization_id, - source=self.state.ca, + common_name=self.state.client_common_name, + source=self.state.issuer_common_name, ) diff --git a/src/auth_server/models/claims.py b/src/auth_server/models/claims.py index e891b87..adab3a2 100644 --- a/src/auth_server/models/claims.py +++ b/src/auth_server/models/claims.py @@ -22,6 +22,7 @@ class ConfigClaims(Claims): class CAClaims(Claims): + common_name: str organization_id: Optional[str] = None diff --git a/src/auth_server/tests/data/ca/README b/src/auth_server/tests/data/ca/README new file mode 100644 index 0000000..8a992eb --- /dev/null +++ b/src/auth_server/tests/data/ca/README @@ -0,0 +1,12 @@ +Test certs: +https://eid.expisoft.se/expitrust-test-certifikat/ + +Convert p12 to x509 PEM certificate AND key format, use these commands: + + openssl pkcs12 -in path.p12 -out newfile.crt.pem -clcerts -nokeys + openssl pkcs12 -in path.p12 -out newfile.key.pem -nocerts -nodes + +Test CA certs: +https://eid.expisoft.se/expitrust-test-certifikat/ +https://inera.atlassian.net/wiki/spaces/IAM/pages/289082989/PKI-struktur+och+rotcertifikat +https://repository.efos.se/ diff --git a/src/auth_server/tests/data/ca/bolag_a.crt b/src/auth_server/tests/data/ca/bolag_a.crt new file mode 100644 index 0000000..083c4a3 --- /dev/null +++ b/src/auth_server/tests/data/ca/bolag_a.crt @@ -0,0 +1,36 @@ +Bag Attributes + friendlyName: auth + localKeyID: 01 01 01 01 +subject=C = SE, O = Bolag A, CN = Bolag A + serialNumber = 165560000167 +issuer=C = SE, O = Expisoft AB, CN = ExpiTrust Test CA v8 +-----BEGIN CERTIFICATE----- +MIIFaTCCA1GgAwIBAgICBOwwDQYJKoZIhvcNAQELBQAwQjELMAkGA1UEBhMCU0Ux +FDASBgNVBAoTC0V4cGlzb2Z0IEFCMR0wGwYDVQQDExRFeHBpVHJ1c3QgVGVzdCBD +QSB2ODAeFw0yMzAxMDIxNTEyMzhaFw0yNTAxMDIxNTEyMzhaMEYxCzAJBgNVBAYT +AlNFMRAwDgYDVQQKEwdCb2xhZyBBMSUwDgYDVQQDEwdCb2xhZyBBMBMGA1UEBRMM +MTY1NTYwMDAwMTY3MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsdSA +88iUeAOe1O2nDWVSXrT3+YVjQPg35MysBx1kmxvwpANSXDwFauyKHFl44jqN/UzV +siBAi2ICdoRQfrEs66XEU0zNiz61ZDg4P/L2C14IN1aRWF//8MQo2zrQs44iy/HU +1oXB3SfMo4KIbPgJwQd7JHnqYiOtfLO/iMH5an+fCtbj3+pwhtb64lfkdVQx+Ntq +eEygmzl1SCZBmdqUR4vupajhY42hlKM/6+U6DY/Cbvo3KwJ1GsuQKD5Iww/92bVx +6loJY0/hAmEsXFX8DGHQCxvivmbFP7cL2qA9e+xp03cqvW/qBMuXDmBUgwRaXcAT +mBxADHi5R1MlkMuVkQIDAQABo4IBYzCCAV8wHwYDVR0jBBgwFoAURHCpiqHBraqR +s0LNTz6+wG3iXKQwHQYDVR0OBBYEFAoBVE/ejxgmB5rj+lxWtVQr0e4cMA4GA1Ud +DwEB/wQEAwIFoDAVBgNVHSAEDjAMMAoGCCqFcDYJAiIDMB0GA1UdJQQWMBQGCCsG +AQUFBwMBBggrBgEFBQcDAjCBnQYDVR0fBIGVMIGSMEegRaBDhkFodHRwOi8vY2Rw +Mi5jZXJ0c2VydmljZS5zZS9jZHAvZWlkL0V4cGlUcnVzdCUyMFRlc3QlMjBDQSUy +MHY4LmNybDBHoEWgQ4ZBaHR0cDovL2NkcDEuY2VydHNlcnZpY2Uuc2UvY2RwL2Vp +ZC9FeHBpVHJ1c3QlMjBUZXN0JTIwQ0ElMjB2OC5jcmwwNwYIKwYBBQUHAQEEKzAp +MCcGCCsGAQUFBzABhhtodHRwOi8vb2NzcC5jZXJ0c2VydmljZS5zZS8wDQYJKoZI +hvcNAQELBQADggIBABfA3RvhYB5fmrZXyoZA47rtDWfR4TAaT/Ddy3iHaVaEOsQ/ +57Zg/IRKXjIB7Us0Vu7Tt2ld5z5coBTHOJtXWUZnn7Olm5tmnnseE2wjHhEqEJCH +oJEa2+4rO7MDtechAQRYnNmWF0bU+WoOFWcNbHTORc5umsHdM3XTTdXADOnS0y6e +Rpn0W/TDncLx9QLLV3A83GmHyZR1SBDT/VZ0oJiO+2xFJHhDL0oTDzHseRaOEfJi +4BhCIjXoZnckYB/vyz9TwR8J2hy1a32NyVinMMssW4Chqalry20oMPB/hk3+D3Bl +oiWcQEuWHwHV/Bhwgm4KFn6ywQsLDEH97HizNfokZ02BKg/Hrn/FKnBxvXmwX3I5 +LC9tLz015hX9pvulqaERKIRR2aohghRr+jz17u3MOeGUEBJbVaC8+wQBmW3/E7Er +2oYcjNYCbG5vZWcO0P7Kv18esWg4yYDz6l0dVVeHx9TSegcd5LUc22uoM4OsH1fE +ST2z3QwcMQC6q4OFxmKutrMat7MXaQA75MTc693gdxqqWuj3Qydm5qO7lcfXkEJh +ltw4+FAUEZZzNFjV1j+KO4DL17VUA2srXhVKLrncSfzCnbBxV/QFGlQFA9lFvTQr +4lVpN55UOwGdKndpoSvICKQgkit+rvbmAmEnHfEkYvsDWOi1fLG7VP8ZtAfD +-----END CERTIFICATE----- diff --git a/src/auth_server/tests/data/ca/bolag_b.crt b/src/auth_server/tests/data/ca/bolag_b.crt new file mode 100644 index 0000000..09934db --- /dev/null +++ b/src/auth_server/tests/data/ca/bolag_b.crt @@ -0,0 +1,36 @@ +Bag Attributes + friendlyName: auth + localKeyID: 01 01 01 01 +subject=C = SE, O = Bolag B, CN = Bolag B + serialNumber = 165560000282 +issuer=C = SE, O = Expisoft AB, CN = ExpiTrust Test CA v8 +-----BEGIN CERTIFICATE----- +MIIFaTCCA1GgAwIBAgICBPMwDQYJKoZIhvcNAQELBQAwQjELMAkGA1UEBhMCU0Ux +FDASBgNVBAoTC0V4cGlzb2Z0IEFCMR0wGwYDVQQDExRFeHBpVHJ1c3QgVGVzdCBD +QSB2ODAeFw0yMzAxMDIxNTQ2MjlaFw0yNTAxMDIxNTQ2MjlaMEYxCzAJBgNVBAYT +AlNFMRAwDgYDVQQKEwdCb2xhZyBCMSUwDgYDVQQDEwdCb2xhZyBCMBMGA1UEBRMM +MTY1NTYwMDAwMjgyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtS+M +1eBuZz+9oD+it7R4JWStmqUPr4h+e/txby5YjzjdXyuDfI5vtqZPZM+/n3tkJA5a +r3fb0jrT7gcsiFFgnJB3Zfp0+smtJ14YGvum19GHOfonIm4dCryu0wEj1WhcU4cZ +1Gkjl2tCpKHbu2T6uiG6VmuWFucQFiStpPGyBpkBiuPCnUWamXodFp8xq1oCo7Wo +DEDmek7vdW5YNKY7snxfTE5EmMv3KS4twH8bOffUXMKkJmaU60fSu0vpgN6ycLWl +pfDszASn74ewBU6S8ENFWlGCCGQOYAmBKmMWgu7h6j0026ploGPYkHLiQxWn57qK +/gVw8ygTw2zl1sWfNQIDAQABo4IBYzCCAV8wHwYDVR0jBBgwFoAURHCpiqHBraqR +s0LNTz6+wG3iXKQwHQYDVR0OBBYEFMF2RKAIo0vsWOu3paRUH/Wl8K9BMA4GA1Ud +DwEB/wQEAwIFoDAVBgNVHSAEDjAMMAoGCCqFcDYJAiIDMB0GA1UdJQQWMBQGCCsG +AQUFBwMBBggrBgEFBQcDAjCBnQYDVR0fBIGVMIGSMEegRaBDhkFodHRwOi8vY2Rw +Mi5jZXJ0c2VydmljZS5zZS9jZHAvZWlkL0V4cGlUcnVzdCUyMFRlc3QlMjBDQSUy +MHY4LmNybDBHoEWgQ4ZBaHR0cDovL2NkcDEuY2VydHNlcnZpY2Uuc2UvY2RwL2Vp +ZC9FeHBpVHJ1c3QlMjBUZXN0JTIwQ0ElMjB2OC5jcmwwNwYIKwYBBQUHAQEEKzAp +MCcGCCsGAQUFBzABhhtodHRwOi8vb2NzcC5jZXJ0c2VydmljZS5zZS8wDQYJKoZI +hvcNAQELBQADggIBACLfiLCu5h4uefhKiQxj6XplNJmPC3ZG8uKjoW/z9QzuOs0n +FJr7avJbyYAYJiV2B+eYwNWvVmCY8BGEuarbJHpHOxA0k3GAqV2pCFT1/VBPRAxd +xnT9SuKr7OQzSztSQkvn3DBlK3WKjRDQtW7LprKZNl4czEoJb3tLxOKvfmTReWCg +gb2Em2xXG2pARzTeSuAlO/ZDGp/1O8lrmXuMHZFfP+BwoXonlngOXHhQKE/r60j/ +OY1yEXS47Ft02LzervY6ufE59zGK3wQ80vuIaHVzkyUpPutkwdtc9Qnintkds27X +8vh+WUZH23q25YUy9EyUT6WLDoyiih0krU/vFND6DOdCgx3HyY/3ygeDwyd1jupK +pxa+ZdI3QfrUQNJadGU0e+2SSebDYIxJFSg49CSR81r4Z6NBM3PkXYWU9OleMYmj +xhb3PMVErrUYCEu7DgWgZOmcaOGrDi1kLxzbO1wvVHISmyWrkZCWcrgiMbjQey+8 +zCrMYJyJ3ug5NrtgH8HQCGS2ZUl0XsvvB+XWg8JJfuLX5mO12Tp6xxPykzUWJnsy +mgVhFVEpepDqz3OjqIk5P3/QwOncQuYcAqPvD+xUd1aIDX5w0tsvA0G3aNE+aDlP +OtWAAFaehCABSfTpTDfzca0M210q32gZr25Yu58rwAlEaRFbI6a5X2CLz3pP +-----END CERTIFICATE----- diff --git a/src/auth_server/tests/data/ca/bolag_c.crt b/src/auth_server/tests/data/ca/bolag_c.crt new file mode 100644 index 0000000..dffcdac --- /dev/null +++ b/src/auth_server/tests/data/ca/bolag_c.crt @@ -0,0 +1,36 @@ +Bag Attributes + friendlyName: auth + localKeyID: 01 01 01 01 +subject=C = SE, O = Bolag C, CN = Bolag C + serialNumber = 165560001124 +issuer=C = SE, O = Expisoft AB, CN = ExpiTrust Test CA v8 +-----BEGIN CERTIFICATE----- +MIIFaTCCA1GgAwIBAgICBPgwDQYJKoZIhvcNAQELBQAwQjELMAkGA1UEBhMCU0Ux +FDASBgNVBAoTC0V4cGlzb2Z0IEFCMR0wGwYDVQQDExRFeHBpVHJ1c3QgVGVzdCBD +QSB2ODAeFw0yMzAxMDIxNjAzMDZaFw0yMzAxMDMxNjAzMDlaMEYxCzAJBgNVBAYT +AlNFMRAwDgYDVQQKEwdCb2xhZyBDMSUwDgYDVQQDEwdCb2xhZyBDMBMGA1UEBRMM +MTY1NTYwMDAxMTI0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAunhn +WRZbHu53JNwENiQQANbm0rkeNvbz4rMdAYxlevyFo5gM7fQ5PNqm9JRPUtypeZYR +VwkDuYdmx4rdsVIdCr9DMGjADkjQzmDakUen3O5Q7AbtGT/tRuhqjfFYYDActbu/ +Vxealxgv7rCy/9XoMIB/3cTeDoxH/MDWdo84KCDKGVVSivKVi2nDBJsurpmqklQo +bZ5VO7vQyOVXDaPJiq0QnQumtE5xzqZ2Zx7dKM007lTjhMCV5zMiYbiHDaAQdlZI +M7KGjiBzxqAzMrNhZibckGcu0Ihvu42LKfhzWRuo8EezKX3rjKIsxk4JzCAw+4+l +wWdvc2nh0rlBBiETxwIDAQABo4IBYzCCAV8wHwYDVR0jBBgwFoAURHCpiqHBraqR +s0LNTz6+wG3iXKQwHQYDVR0OBBYEFP5g2Sn2nbTr/PAFccd4p1KY+1QaMA4GA1Ud +DwEB/wQEAwIFoDAVBgNVHSAEDjAMMAoGCCqFcDYJAiIDMB0GA1UdJQQWMBQGCCsG +AQUFBwMBBggrBgEFBQcDAjCBnQYDVR0fBIGVMIGSMEegRaBDhkFodHRwOi8vY2Rw +Mi5jZXJ0c2VydmljZS5zZS9jZHAvZWlkL0V4cGlUcnVzdCUyMFRlc3QlMjBDQSUy +MHY4LmNybDBHoEWgQ4ZBaHR0cDovL2NkcDEuY2VydHNlcnZpY2Uuc2UvY2RwL2Vp +ZC9FeHBpVHJ1c3QlMjBUZXN0JTIwQ0ElMjB2OC5jcmwwNwYIKwYBBQUHAQEEKzAp +MCcGCCsGAQUFBzABhhtodHRwOi8vb2NzcC5jZXJ0c2VydmljZS5zZS8wDQYJKoZI +hvcNAQELBQADggIBAHMnn3iLSX0eSjqwHMBOkqkr+78CCtwCzZlpBbSChy6HumDk +7VGS61HBgVl/zpRNKC6ovkMO4wEzHsTVNIAt4LezUeYun0Ahpd09VUkIYQKpSEJM +yQXgfZpgf7Da1HNj60BtGcA/13vARij9JV0Kqlct44961yfuWYLKzgq+Jdi8lJuP +DM+cxnjSnCZrX6Al5HD0/kGHYvuLu5LgF8+8M2AWmHx9ssoPKG6WhK7dTqdBpJmu +HsIWPGBcLUJAJ28srylU0Bq2RPup17wARNRVP89UJ7eBiL3/3uUD5S7/5zfBgzcn +JjLjHAPc+XmhS64+gTF3vZMQbGQu++i/DAMGNTId5e2+y5tc2cMDaZ84s2f+fZ9W +s8yiTfS3HfhnwHt7TLe4TXaIdUCA6TzZLwmi4bOGDb8MeCybkdWilFG4ypEHdhb5 ++SnIaFjURuhjTNob+XPLkkNgw/BwLxAC6xvh34JOsuoXZpM/54Asx7jgPyLEQ5kx +hYcq+SWvIkwm2PjthRxJwRRkl50kyfgoQpqG6rLZ4IUYnI4YInLiIC9s0HS2hYbj +QBD49yd4ylanVM+6s5BMpo9Eu97jH2/1moRtHrN+GOBALssCj1GA/C2GM4uuJ63O +QRadKQ8DzXJXG1Ra7IUIVk6zWXFYa+uIQfUd4oirmN8gNzi/bfd3bziN69QR +-----END CERTIFICATE----- diff --git a/src/auth_server/tests/data/ca/bolag_e.crt b/src/auth_server/tests/data/ca/bolag_e.crt new file mode 100644 index 0000000..c109c77 --- /dev/null +++ b/src/auth_server/tests/data/ca/bolag_e.crt @@ -0,0 +1,36 @@ +Bag Attributes + friendlyName: auth + localKeyID: 01 01 01 01 +subject=C = SE, O = Bolag E, CN = Bolag E + serialNumber = 194801301872 +issuer=C = SE, O = Expisoft AB, CN = ExpiTrust Test CA v8 +-----BEGIN CERTIFICATE----- +MIIFaTCCA1GgAwIBAgICBPYwDQYJKoZIhvcNAQELBQAwQjELMAkGA1UEBhMCU0Ux +FDASBgNVBAoTC0V4cGlzb2Z0IEFCMR0wGwYDVQQDExRFeHBpVHJ1c3QgVGVzdCBD +QSB2ODAeFw0yNTAxMDExNTU3NDJaFw0yNTAxMDIxNTU3NDhaMEYxCzAJBgNVBAYT +AlNFMRAwDgYDVQQKEwdCb2xhZyBFMSUwDgYDVQQDEwdCb2xhZyBFMBMGA1UEBRMM +MTk0ODAxMzAxODcyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxI+g +1VaCX333baJD+J0ItgyYfZRpqFnfoTe3r61LY/EoPSRF/KekPOBFmALjSRG3Vo8P +i1tabDGCTeRZMO1KbV+ONdfSay0dckqmJK3cuK52uqzVaaI6AxteUCWskK2H5t/N +T+Eebq8IhGQ+7cwvgntHrl1ERZPfUuO/22h1+JF/zBgSjvGKGQ9lAM0qW2M1vjJj +Nl+hoyy8xNUJT77Kooh1r2Prd0uZHlfl9udzplORU2dVwW3Jtc7fCKtCzFdWUf6Z +RS9Gy85XqTbi/8CYr9+5t4V95IuhocOWS2IJxIC0awGDqN0poYSLCc82S51I7W6K +5mWbum4PWWnJdBJqFwIDAQABo4IBYzCCAV8wHwYDVR0jBBgwFoAURHCpiqHBraqR +s0LNTz6+wG3iXKQwHQYDVR0OBBYEFLLZzQ9x3LxvVrjbNbrjA5PVn+leMA4GA1Ud +DwEB/wQEAwIFoDAVBgNVHSAEDjAMMAoGCCqFcDYJAiIDMB0GA1UdJQQWMBQGCCsG +AQUFBwMBBggrBgEFBQcDAjCBnQYDVR0fBIGVMIGSMEegRaBDhkFodHRwOi8vY2Rw +Mi5jZXJ0c2VydmljZS5zZS9jZHAvZWlkL0V4cGlUcnVzdCUyMFRlc3QlMjBDQSUy +MHY4LmNybDBHoEWgQ4ZBaHR0cDovL2NkcDEuY2VydHNlcnZpY2Uuc2UvY2RwL2Vp +ZC9FeHBpVHJ1c3QlMjBUZXN0JTIwQ0ElMjB2OC5jcmwwNwYIKwYBBQUHAQEEKzAp +MCcGCCsGAQUFBzABhhtodHRwOi8vb2NzcC5jZXJ0c2VydmljZS5zZS8wDQYJKoZI +hvcNAQELBQADggIBAEENx8DIVkZ/Wm4YXR/axY+EhaHr8wqINyiuLhWZs0+zxcsm +4TdRw3TZi2iLDsHqLY67SxZ+QmtOiELDxLug9xNPwZArZLHxuJQX2ek0N/gMogvZ +no7enf/mb7BU3ievAeIkhQ+BDe4lMD7qYITGAKijQa0z3zZQ/aCjx8Vu1LWY8l/Z +/2BrfJs4FPPVpLtwzhr0Q+UuxY2scnNQDTQLZrC/2atDVXekfeQ50JrlOLP8nBFG +HpljGeE2lCHCPGSKV1w1l3jICLyPeQcBmdqfrNILv+a9nYGTCLjy//qhizRxaUMp +nS299guu0/ajtOHIBtFf6vnVLe0NrvPdM7U8d2fUBiJKrby5R2/i6u4DxUdI6+mG +weLYp3nqkqwCe3XP0QHDqtdhPLajW+Fn6HjX0DpvvP2cc1wIvhYOYKtjEySKtmH9 +Hh4fz49NZEv8Ptpzl3Y6hdb25eTrzdpgAzLdG49eRZVFoRlLIzpugpVM2p6/aZOx +jo2DxWKtAphrQkEw3UlTUed2OIFqQEy/vRmurnOMah9AqJH/vtw//bueGzUwYJkQ +Dg3ERgADzMrNvmtj4PKvUbCkjBMXq1zmBQLWp9PCO9DIZr7I6dh68oRphLBcKDje +29QCyTq0cOQT/jx0K1Lk65VfQXneusBLvjXxc2dRt59OkU0Z+k+7Q9aOr20M +-----END CERTIFICATE----- diff --git a/src/auth_server/tests/data/ca/ca_cert/ExpiTrust-Test-CA-v8.crt b/src/auth_server/tests/data/ca/ca_cert/ExpiTrust-Test-CA-v8.crt new file mode 100644 index 0000000000000000000000000000000000000000..95eb44b56dd008846999c57839ebf8e031eb5302 GIT binary patch literal 1370 zcmXqLVhuBBVzyhr%*4pV#KioLsm4Ug5iJ!rsiHVD;iHVWnNybax9Y&8r zCL9ipd*y8xq1!u0@DR_1kYBGZ-Ik2esZw8MocHhdZ5L)UucMp2o*pwjKRxo8(we)^ z3Ii6uK6ykY@9?v%x$CYx=$gO1xU~0=z2@N`>o~47e*VQYEz`Q_U+&Gx!kH=2fft`{ zZxolA8{w0c5^Hz7Cp1y%{;?Ff-9Qu4H+|de4Ww_pVJ0wb;LOivO`g z7bm@XwtL2vlF0{t?K-{arxwE`wRT)$ud=*cFV19da`OrU%6IF z__fv~1H}X4i@mZ_E-)0OxHIX`eyO+OO7@nw{2#oFWGY-7p0Ui@Cs8GB=yGoA{exRv zy&RtPeO;5g`=FlEF`s=mguGWy>N@Z$=9K%rn>u2)w>1nm&VQQGvcB-dg46>K=AMjP zbE{p=JNFlx{KBv&?A@rageGJf;s%Br^qFRczpvdds|-B_%6c6Is; zm+-18-rio#oq03tnV1>w2QCgX2r=LRCRJHs7FGjhM#ldJq9C3E3!edx0T&yGR%?M7 zCzBGhfh3m3>Ndwn2d_0QZ} zjrN^Cyu`z?aDGke_ky)wPpyL*+)htn^Ta);2v^m_^5YxEvvg;MgIgdB0o>2I#`?}mGc3Ijs z@7I+xs(GSUT}hslq;V|Nq15K^-pFtUsco|s?pwC4#aw#I+2~RBMa(22R;Hud&(PsAP1tIf{Z>2@C zerEVQzkNUFQHHxh`3E`{TWot*bgKMmfxh6WzsuwPf0Ee#RYg0-Ve&&?T d?>4PAzuqM(@Tz*}`vdiJ5-ndw1@5Y}2LS3BNx}dC literal 0 HcmV?d00001 diff --git a/src/auth_server/tests/data/ca/ca_cert/SwedishPublicSectorFunctionCAv3SAT.crt b/src/auth_server/tests/data/ca/ca_cert/SwedishPublicSectorFunctionCAv3SAT.crt new file mode 100644 index 0000000000000000000000000000000000000000..1af376f4d48902cfe332d009cd810659debc0d09 GIT binary patch literal 1868 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 None: + self.datadir = Path(__file__).with_name("data") + self.config: Dict[str, Any] = { + "testing": "true", + "log_level": "DEBUG", + "keystore_path": f"{self.datadir}/testing_jwks.json", + "ca_certs_path": f"{self.datadir}/ca/ca_cert/", + "signing_key_id": "test-kid", + "auth_token_issuer": "http://testserver", + "auth_token_audience": "some_audience", + "auth_flows": json.dumps(["CAFlow"]), + } + environ.update(self.config) + self.app = init_auth_server_api() + self.client = TestClient(self.app) + + def _update_app_config(self, config: Optional[Dict] = None): + if config is not None: + environ.clear() + environ.update(config) + self._clear_lru_cache() + self.app = init_auth_server_api() + self.client = TestClient(self.app) + + def _load_cert(self, filename: str) -> Certificate: + with open(f"{self.datadir}/ca/{filename}", "rb") as f: + cert = x509.load_pem_x509_certificate(data=f.read()) + return cert + + def _get_access_token_claims(self, access_token: Dict, client: Optional[TestClient]) -> Dict[str, Any]: + if client is None: + client = self.client + response = client.get("/.well-known/jwk.json") + assert response.status_code == 200 + token = jwt.JWT(key=jwk.JWK(**response.json()), jwt=access_token["value"]) + return json.loads(token.claims) + + @staticmethod + def _clear_lru_cache(): + # Clear lru_cache to allow config update + load_config.cache_clear() + load_jwks.cache_clear() + get_signing_key.cache_clear() + get_tls_fed_metadata.cache_clear() + + def tearDown(self) -> None: + self.app = None # type: ignore + self.client = None # type: ignore + self._clear_lru_cache() + # Clear environment variables + environ.clear() + + def test_load_ca_certs(self): + ca_certs = load_ca_certs() + assert len(ca_certs) == 3 + + def test_cert_signed_by_ca(self): + parameters = [ + ("bolag_a.crt", "CN=ExpiTrust Test CA v8,O=Expisoft AB,C=SE"), + ("bolag_b.crt", "CN=ExpiTrust Test CA v8,O=Expisoft AB,C=SE"), + ("bolag_c.crt", "CN=ExpiTrust Test CA v8,O=Expisoft AB,C=SE"), + ("bolag_e.crt", "CN=ExpiTrust Test CA v8,O=Expisoft AB,C=SE"), + ] + for cert_name, expected_ca_name in parameters: + cert = self._load_cert(filename=cert_name) + ca_name = cert_signed_by_ca(cert) + assert ca_name == expected_ca_name + + def test_cert_within_validity_period(self): + parameters = [ + ("bolag_a.crt", True), + ("bolag_b.crt", True), + ("bolag_c.crt", False), + ("bolag_e.crt", False), + ] + for cert_name, within_validity_period in parameters: + cert = self._load_cert(filename=cert_name) + assert ( + cert_within_validity_period(cert) is within_validity_period + ), f"{cert_name} should be {not within_validity_period}" + + def _do_mtls_transaction(self, cert: Certificate) -> Response: + client_cert_str = serialize_certificate(cert=cert) + req = GrantRequest( + client=Client(key=Key(proof=Proof(method=ProofMethod.MTLS), cert_S256=rfc8705_fingerprint(cert=cert))), + access_token=[AccessTokenRequest(flags=[AccessTokenFlags.BEARER])], + ) + client_header = {"Client-Cert": client_cert_str} + return self.client.post("/transaction", json=req.model_dump(exclude_none=True), headers=client_header) + + def test_mtls_transaction(self): + parameters = [ + ("bolag_a.crt", True, "165560000167"), + ("bolag_b.crt", False, "client certificate revoked"), + ("bolag_c.crt", False, "client certificate expired or not yet valid"), + ("bolag_e.crt", False, "client certificate expired or not yet valid"), + ] + for cert_name, expect_success, expected_result in parameters: + cert = self._load_cert(filename=cert_name) + response = self._do_mtls_transaction(cert=cert) + + if expect_success: + assert response.status_code == 200 + assert "access_token" in response.json() + access_token = response.json()["access_token"] + assert AccessTokenFlags.BEARER.value in access_token["flags"] + assert access_token["value"] is not None + # Verify token and check claims + claims = self._get_access_token_claims(access_token=access_token, client=self.client) + assert claims["auth_source"] == AuthSource.CA + assert claims is not None + assert claims["organization_id"] == expected_result, f"{cert_name} has wrong org id" + assert claims["common_name"] == get_subject_cn(cert=cert), f"{cert_name} has wrong common name" + assert claims["source"] is not None + else: + assert response.status_code == 401 + assert response.json()["detail"] == expected_result + + +class TestAuthServerAsync(IsolatedAsyncioTestCase): + def setUp(self) -> None: + self.datadir = Path(__file__).with_name("data") + self.config: Dict[str, Any] = { + "testing": "true", + "log_level": "DEBUG", + "keystore_path": f"{self.datadir}/testing_jwks.json", + "ca_certs_path": f"{self.datadir}/ca/ca_cert/", + "signing_key_id": "test-kid", + "auth_token_issuer": "http://testserver", + "auth_token_audience": "some_audience", + "auth_flows": json.dumps(["TestFlow"]), + } + environ.update(self.config) + self.app = init_auth_server_api() + self.client = TestClient(self.app) + + def _load_cert(self, filename: str) -> Certificate: + with open(f"{self.datadir}/ca/{filename}", "rb") as f: + cert = x509.load_pem_x509_certificate(data=f.read()) + return cert + + async def test_cert_is_revoked(self): + parameters = [ + ("bolag_a.crt", "CN=ExpiTrust Test CA v8,O=Expisoft AB,C=SE", False), + ("bolag_b.crt", "CN=ExpiTrust Test CA v8,O=Expisoft AB,C=SE", True), + ] + for cert_name, ca_name, revoked_status in parameters: + cert = self._load_cert(filename=cert_name) + assert await is_cert_revoked(cert, ca_name) is revoked_status, f"{cert_name} should be {not revoked_status}"