enterprise/stages/mtls: improve certificate validation (#14582)

* improve certificate validation

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix fingerprint sha1

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* new cert with fixed attributes

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add sc amr support

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
Jens L.
2025-05-20 14:15:32 +02:00
committed by GitHub
parent 73a3f29001
commit abaf8d9544
5 changed files with 80 additions and 46 deletions

View File

@ -3,7 +3,14 @@ from urllib.parse import unquote_plus
from cryptography.exceptions import InvalidSignature from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives import hashes 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 django.utils.translation import gettext_lazy as _
from authentik.brands.models import Brand from authentik.brands.models import Brand
@ -102,15 +109,21 @@ class MTLSStageView(ChallengeStageView):
return None return None
def validate_cert(self, authorities: list[CertificateKeyPair], certs: list[Certificate]): def validate_cert(self, authorities: list[CertificateKeyPair], certs: list[Certificate]):
authorities_cert = [x.certificate for x in authorities]
for _cert in certs: for _cert in certs:
for ca in authorities:
try: try:
_cert.verify_directly_issued_by(ca.certificate) PolicyBuilder().store(Store(authorities_cert)).build_client_verifier().verify(
return _cert _cert, []
except (InvalidSignature, TypeError, ValueError) as exc:
self.logger.warning(
"Discarding cert not issued by authority", cert=_cert, authority=ca, exc=exc
) )
return _cert
except (
InvalidSignature,
TypeError,
ValueError,
VerificationError,
UnsupportedGeneralNameType,
) as exc:
self.logger.warning("Discarding invalid certificate", cert=_cert, exc=exc)
continue continue
return None return None
@ -141,7 +154,9 @@ class MTLSStageView(ChallengeStageView):
"subject": cert.subject.rfc4514_string(), "subject": cert.subject.rfc4514_string(),
"issuer": cert.issuer.rfc4514_string(), "issuer": cert.issuer.rfc4514_string(),
"fingerprint_sha256": hexlify(cert.fingerprint(hashes.SHA256()), ":").decode("utf-8"), "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): def auth_user(self, user: User, cert: Certificate):

View File

@ -1,30 +1,31 @@
-----BEGIN CERTIFICATE----- -----BEGIN CERTIFICATE-----
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
nQe4JNS+u5e2VDEBWUxt8NTpu6eShDN0iIEHtxA=
-----END CERTIFICATE----- -----END CERTIFICATE-----

View File

@ -5,7 +5,12 @@ from django.urls import reverse
from guardian.shortcuts import assign_perm from guardian.shortcuts import assign_perm
from authentik.core.models import User 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.crypto.models import CertificateKeyPair
from authentik.enterprise.stages.mtls.models import ( from authentik.enterprise.stages.mtls.models import (
CertAttributes, CertAttributes,
@ -127,6 +132,18 @@ class MTLSStageTests(FlowTestCase):
self.assertEqual(res.status_code, 200) self.assertEqual(res.status_code, 200)
self.assertStageResponse(res, self.flow, component="ak-stage-access-denied") 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): def test_auth_no_user(self):
"""Test auth with no user""" """Test auth with no user"""
User.objects.filter(username="client").delete() User.objects.filter(username="client").delete()
@ -200,14 +217,12 @@ class MTLSStageTests(FlowTestCase):
self.assertEqual( self.assertEqual(
plan().context[PLAN_CONTEXT_CERTIFICATE], plan().context[PLAN_CONTEXT_CERTIFICATE],
{ {
"fingerprint_sha1": ( "fingerprint_sha1": "52:39:ca:1e:3a:1f:78:3a:9f:26:3b:c2:84:99:48:68:99:99:81:8a",
"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_sha256": ( "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", "issuer": "OU=Self-signed,O=authentik,CN=authentik Test CA",
"serial_number": "630532384467334865093173111400266136879266564943", "serial_number": "70153443448884702681996102271549704759327537151",
"subject": "CN=client", "subject": "CN=client",
}, },
) )

View File

@ -50,3 +50,4 @@ AMR_PASSWORD = "pwd" # nosec
AMR_MFA = "mfa" AMR_MFA = "mfa"
AMR_OTP = "otp" AMR_OTP = "otp"
AMR_WEBAUTHN = "user" AMR_WEBAUTHN = "user"
AMR_SMART_CARD = "sc"

View File

@ -16,6 +16,7 @@ from authentik.providers.oauth2.constants import (
ACR_AUTHENTIK_DEFAULT, ACR_AUTHENTIK_DEFAULT,
AMR_MFA, AMR_MFA,
AMR_PASSWORD, AMR_PASSWORD,
AMR_SMART_CARD,
AMR_WEBAUTHN, AMR_WEBAUTHN,
) )
from authentik.stages.password.stage import PLAN_CONTEXT_METHOD, PLAN_CONTEXT_METHOD_ARGS from authentik.stages.password.stage import PLAN_CONTEXT_METHOD, PLAN_CONTEXT_METHOD_ARGS
@ -139,8 +140,9 @@ class IDToken:
amr.append(AMR_PASSWORD) amr.append(AMR_PASSWORD)
if method == "auth_webauthn_pwl": if method == "auth_webauthn_pwl":
amr.append(AMR_WEBAUTHN) amr.append(AMR_WEBAUTHN)
if "certificate" in method_args:
amr.append(AMR_SMART_CARD)
if "mfa_devices" in method_args: if "mfa_devices" in method_args:
if len(amr) > 0:
amr.append(AMR_MFA) amr.append(AMR_MFA)
if amr: if amr:
id_token.amr = amr id_token.amr = amr