diff --git a/authentik/enterprise/stages/mtls/stage.py b/authentik/enterprise/stages/mtls/stage.py index 650b4ea3ca..6aef0fe384 100644 --- a/authentik/enterprise/stages/mtls/stage.py +++ b/authentik/enterprise/stages/mtls/stage.py @@ -3,7 +3,14 @@ from urllib.parse import unquote_plus from cryptography.exceptions import InvalidSignature from cryptography.hazmat.primitives import hashes -from cryptography.x509 import Certificate, NameOID, ObjectIdentifier, load_pem_x509_certificate +from cryptography.x509 import ( + Certificate, + NameOID, + ObjectIdentifier, + UnsupportedGeneralNameType, + load_pem_x509_certificate, +) +from cryptography.x509.verification import PolicyBuilder, Store, VerificationError from django.utils.translation import gettext_lazy as _ from authentik.brands.models import Brand @@ -102,16 +109,22 @@ class MTLSStageView(ChallengeStageView): return None def validate_cert(self, authorities: list[CertificateKeyPair], certs: list[Certificate]): + authorities_cert = [x.certificate for x in authorities] for _cert in certs: - for ca in authorities: - try: - _cert.verify_directly_issued_by(ca.certificate) - return _cert - except (InvalidSignature, TypeError, ValueError) as exc: - self.logger.warning( - "Discarding cert not issued by authority", cert=_cert, authority=ca, exc=exc - ) - continue + try: + PolicyBuilder().store(Store(authorities_cert)).build_client_verifier().verify( + _cert, [] + ) + return _cert + except ( + InvalidSignature, + TypeError, + ValueError, + VerificationError, + UnsupportedGeneralNameType, + ) as exc: + self.logger.warning("Discarding invalid certificate", cert=_cert, exc=exc) + continue return None def check_if_user(self, cert: Certificate): @@ -141,7 +154,9 @@ class MTLSStageView(ChallengeStageView): "subject": cert.subject.rfc4514_string(), "issuer": cert.issuer.rfc4514_string(), "fingerprint_sha256": hexlify(cert.fingerprint(hashes.SHA256()), ":").decode("utf-8"), - "fingerprint_sha1": hexlify(cert.fingerprint(hashes.SHA256()), ":").decode("utf-8"), + "fingerprint_sha1": hexlify(cert.fingerprint(hashes.SHA1()), ":").decode( # nosec + "utf-8" + ), } def auth_user(self, user: User, cert: Certificate): diff --git a/authentik/enterprise/stages/mtls/tests/fixtures/cert_client.pem b/authentik/enterprise/stages/mtls/tests/fixtures/cert_client.pem index 85bb9da1d9..e279147d81 100644 --- a/authentik/enterprise/stages/mtls/tests/fixtures/cert_client.pem +++ b/authentik/enterprise/stages/mtls/tests/fixtures/cert_client.pem @@ -1,30 +1,31 @@ -----BEGIN CERTIFICATE----- -MIIFOzCCAyOgAwIBAgIUbnIMy+Ewi5RvK7OBDxWMCk7wi08wDQYJKoZIhvcNAQEL +MIIFWTCCA0GgAwIBAgIUDEnKCSmIXG/akySGes7bhOGrN/8wDQYJKoZIhvcNAQEL BQAwRjEaMBgGA1UEAwwRYXV0aGVudGlrIFRlc3QgQ0ExEjAQBgNVBAoMCWF1dGhl -bnRpazEUMBIGA1UECwwLU2VsZi1zaWduZWQwHhcNMjUwNDI3MTgzMTE3WhcNMjYw -NDIzMTgzMTE3WjARMQ8wDQYDVQQDDAZjbGllbnQwggIiMA0GCSqGSIb3DQEBAQUA -A4ICDwAwggIKAoICAQCdV+GEa7+7ito1i/z637OZW+0azv1kuF2aDwSzv+FJd+4L -6hCroRbVYTUFS3I3YwanOOZfau64xH0+pFM5Js8aREG68eqKBayx8vT27hyAOFhd -giEVmSQJfla4ogvPie1rJ0HVOL7CiR72HDPQvz+9k1iDX3xQ/4sdAb3XurN13e+M -Gtavhjiyqxmoo/H4WRd8BhD/BZQFWtaxWODDY8aKk5R7omw6Xf7aRv1BlHdE4Ucy -Wozvpsj2Kz0l61rRUhiMlE0D9dpijgaRYFB+M7R2casH3CdhGQbBHTRiqBkZa6iq -SDkTiTwNJQQJov8yPTsR+9P8OOuV6QN+DGm/FXJJFaPnsHw/HDy7EAbA1PcdbSyK -XvJ8nVjdNhCEGbLGVSwAQLO+78hChVIN5YH+QSrP84YBSxKZYArnf4z2e9drqAN3 -KmC26TkaUzkXnndnxOXBEIOSmyCdD4Dutg1XPE/bs8rA6rVGIR3pKXbCr29Z8hZn -Cn9jbxwDwTX865ljR1Oc3dnIeCWa9AS/uHaSMdGlbGbDrt4Bj/nyyfu8xc034K/0 -uPh3hF3FLWNAomRVZCvtuh/v7IEIQEgUbvQMWBhZJ8hu3HdtV8V9TIAryVKzEzGy -Q72UHuQyK0njRDTmA/T+jn7P8GWOuf9eNdzd0gH0gcEuhCZFxPPRvUAeDuC7DQID -AQABo1YwVDAdBgNVHSUEFjAUBggrBgEFBQcDAgYIKwYBBQUHAwEwFAYDVR0RAQH/ -BAowCIIGY2xpZW50MB0GA1UdDgQWBBQ5KZwTD8+4CqLnbM/cBSXg8XeLXTANBgkq -hkiG9w0BAQsFAAOCAgEABDkb3iyEOl1xKq1wxyRzf2L8qfPXAQw71FxMbgHArM+a -e44wJGO3mZgPH0trOaJ+tuN6erB5YbZfsoX+xFacwskj9pKyb4QJLr/ENmJZRgyL -wp5P6PB6IUJhvryvy/GxrG938YGmFtYQ+ikeJw5PWhB6218C1aZ9hsi94wZ1Zzrc -Ry0q0D4QvIEZ0X2HL1Harc7gerE3VqhgQ7EWyImM+lCRtNDduwDQnZauwhr2r6cW -XG4VTe1RCNsDA0xinXQE2Xf9voCd0Zf6wOOXJseQtrXpf+tG4N13cy5heF5ihed1 -hDxSeki0KjTM+18kVVfVm4fzxf1Zg0gm54UlzWceIWh9EtnWMUV08H0D1M9YNmW8 -hWTupk7M+jAw8Y+suHOe6/RLi0+fb9NSJpIpq4GqJ5UF2kerXHX0SvuAavoXyB0j -CQrUXkRScEKOO2KAbVExSG56Ff7Ee8cRUAQ6rLC5pQRACq/R0sa6RcUsFPXul3Yv -vbO2rTuArAUPkNVFknwkndheN4lOslRd1If02HunZETmsnal6p+nmuMWt2pQ2fDA -vIguG54FyQ1T1IbF/QhfTEY62CQAebcgutnqqJHt9qe7Jr6ev57hMrJDEjotSzkY -OhOVrcYqgLldr1nBqNVlIK/4VrDaWH8H5dNJ72gA9aMNVH4/bSTJhuO7cJkLnHw= +bnRpazEUMBIGA1UECwwLU2VsZi1zaWduZWQwHhcNMjUwNTE5MTIzODQ2WhcNMjYw +NTE1MTIzODQ2WjARMQ8wDQYDVQQDDAZjbGllbnQwggIiMA0GCSqGSIb3DQEBAQUA +A4ICDwAwggIKAoICAQCkPkS1V6l0gj0ulxMznkxkgrw4p9Tjd8teSsGZt02A2Eo6 +7D8FbJ7pp3d5fYW/TWuEKVBLWTID6rijW5EGcdgTM5Jxf/QR+aZTEK6umQxUd4yO +mOtp+xVS3KlcsSej2dFpeE5h5VkZizHpvh5xkoAP8W5VtQLOVF0hIeumHnJmaeLj ++mhK9PBFpO7k9SFrYYhd/uLrYbIdANihbIO2Q74rNEJHewhFNM7oNSjjEWzRd/7S +qNdQij9JGrVG7u8YJJscEQHqyHMYFVCEMjxmsge5BO6Vx5OWmUE3wXPzb5TbyTS4 ++yg88g9rYTUXrzz+poCyKpaur45qBsdw35lJ8nq69VJj2xJLGQDwoTgGSXRuPciC +3OilQI+Ma+j8qQGJxJ8WJxISlf1cuhp+V4ZUd1lawlM5hAXyXmHRlH4pun4y+g7O +O34+fE3pK25JjVCicMT/rC2A/sb95j/fHTzzJpbB70U0I50maTcIsOkyw6aiF//E +0ShTDz14x22SCMolUc6hxTDZvBB6yrcJHd7d9CCnFH2Sgo13QrtNJ/atXgm13HGh +wBzRwK38XUGl/J4pJaxAupTVCPriStUM3m0EYHNelRRUE91pbyeGT0rvOuv00uLw +Rj7K7hJZR8avTKWmKrVBVpq+gSojGW1DwBS0NiDNkZs0d/IjB1wkzczEgdZjXwID +AQABo3QwcjAfBgNVHSMEGDAWgBTa+Ns6QzqlNvnTGszkouQQtZnVJDAdBgNVHSUE +FjAUBggrBgEFBQcDAgYIKwYBBQUHAwEwEQYDVR0RBAowCIIGY2xpZW50MB0GA1Ud +DgQWBBT1xg5sXkypRBwvCxBuyfoanaiZ5jANBgkqhkiG9w0BAQsFAAOCAgEAvUAz +YwIjxY/0KHZDU8owdILVqKChzfLcy9OHNPyEI3TSOI8X6gNtBO+HE6r8aWGcC9vw +zzeIsNQ3UEjvRWi2r+vUVbiPTbFdZboNDSZv6ZmGHxwd85VsjXRGoXV6koCT/9zi +9/lCM1DwqwYSwBphMJdRVFRUMluSYk1oHflGeA18xgGuts4eFivJwhabGm1AdVVQ +/CYvqCuTxd/DCzWZBdyxYpDru64i/kyeJCt1pThKEFDWmpumFdBI4CxJ0OhxVSGp +dOXzK+Y6ULepxCvi6/OpSog52jQ6PnNd1ghiYtq7yO1T4GQz65M1vtHHVvQ3gfBE +AuKYQp6io7ypitRx+LpjsBQenyP4FFGfrq7pm90nLluOBOArfSdF0N+CP2wo/YFV +9BGf89OtvRi3BXCm2NXkE/Sc4We26tY8x7xNLOmNs8YOT0O3r/EQ690W9GIwRMx0 +m0r/RXWn5V3o4Jib9r8eH9NzaDstD8g9dECcGfM4fHoM/DAGFaRrNcjMsS1APP3L +jp7+BfBSXtrz9V6rVJ3CBLXlLK0AuSm7bqd1MJsGA9uMLpsVZIUA+KawcmPGdPU+ +NxdpBCtzyurQSUyaTLtVqSeP35gMAwaNzUDph8Uh+vHz+kRwgXS19OQvTaud5LJu +nQe4JNS+u5e2VDEBWUxt8NTpu6eShDN0iIEHtxA= -----END CERTIFICATE----- diff --git a/authentik/enterprise/stages/mtls/tests/test_stage.py b/authentik/enterprise/stages/mtls/tests/test_stage.py index caf8b828b1..4e8e5c52de 100644 --- a/authentik/enterprise/stages/mtls/tests/test_stage.py +++ b/authentik/enterprise/stages/mtls/tests/test_stage.py @@ -5,7 +5,12 @@ from django.urls import reverse from guardian.shortcuts import assign_perm from authentik.core.models import User -from authentik.core.tests.utils import create_test_brand, create_test_flow, create_test_user +from authentik.core.tests.utils import ( + create_test_brand, + create_test_cert, + create_test_flow, + create_test_user, +) from authentik.crypto.models import CertificateKeyPair from authentik.enterprise.stages.mtls.models import ( CertAttributes, @@ -127,6 +132,18 @@ class MTLSStageTests(FlowTestCase): self.assertEqual(res.status_code, 200) self.assertStageResponse(res, self.flow, component="ak-stage-access-denied") + def test_invalid_cert(self): + """Test invalid certificate""" + cert = create_test_cert() + with self.assertFlowFinishes() as plan: + res = self.client.get( + reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}), + headers={"X-Forwarded-TLS-Client-Cert": quote_plus(cert.certificate_data)}, + ) + self.assertEqual(res.status_code, 200) + self.assertStageResponse(res, self.flow, component="ak-stage-access-denied") + self.assertNotIn(PLAN_CONTEXT_PENDING_USER, plan().context) + def test_auth_no_user(self): """Test auth with no user""" User.objects.filter(username="client").delete() @@ -200,14 +217,12 @@ class MTLSStageTests(FlowTestCase): self.assertEqual( plan().context[PLAN_CONTEXT_CERTIFICATE], { - "fingerprint_sha1": ( - "08:d4:a4:79:25:ca:c3:51:28:88:bb:30:c2:96:c3:44:5a:eb:18:07:84:ca:b4:75:27:74:61:19:8a:6a:af:fc" - ), + "fingerprint_sha1": "52:39:ca:1e:3a:1f:78:3a:9f:26:3b:c2:84:99:48:68:99:99:81:8a", "fingerprint_sha256": ( - "08:d4:a4:79:25:ca:c3:51:28:88:bb:30:c2:96:c3:44:5a:eb:18:07:84:ca:b4:75:27:74:61:19:8a:6a:af:fc" + "c1:07:8b:7c:e9:02:57:87:1e:92:e5:81:83:21:bc:92:c7:47:65:e3:97:fb:05:97:6f:36:9e:b5:31:77:98:b7" ), "issuer": "OU=Self-signed,O=authentik,CN=authentik Test CA", - "serial_number": "630532384467334865093173111400266136879266564943", + "serial_number": "70153443448884702681996102271549704759327537151", "subject": "CN=client", }, ) diff --git a/authentik/providers/oauth2/constants.py b/authentik/providers/oauth2/constants.py index baaebbd607..2424df78a5 100644 --- a/authentik/providers/oauth2/constants.py +++ b/authentik/providers/oauth2/constants.py @@ -50,3 +50,4 @@ AMR_PASSWORD = "pwd" # nosec AMR_MFA = "mfa" AMR_OTP = "otp" AMR_WEBAUTHN = "user" +AMR_SMART_CARD = "sc" diff --git a/authentik/providers/oauth2/id_token.py b/authentik/providers/oauth2/id_token.py index 79ed0517b5..f9b358ab79 100644 --- a/authentik/providers/oauth2/id_token.py +++ b/authentik/providers/oauth2/id_token.py @@ -16,6 +16,7 @@ from authentik.providers.oauth2.constants import ( ACR_AUTHENTIK_DEFAULT, AMR_MFA, AMR_PASSWORD, + AMR_SMART_CARD, AMR_WEBAUTHN, ) from authentik.stages.password.stage import PLAN_CONTEXT_METHOD, PLAN_CONTEXT_METHOD_ARGS @@ -139,9 +140,10 @@ class IDToken: amr.append(AMR_PASSWORD) if method == "auth_webauthn_pwl": amr.append(AMR_WEBAUTHN) + if "certificate" in method_args: + amr.append(AMR_SMART_CARD) if "mfa_devices" in method_args: - if len(amr) > 0: - amr.append(AMR_MFA) + amr.append(AMR_MFA) if amr: id_token.amr = amr