From 88118afb8f9357ae7cc215500a441e89fee701c3 Mon Sep 17 00:00:00 2001 From: Josh Cummings <3627351+jzheaux@users.noreply.github.com> Date: Mon, 5 Jan 2026 12:52:39 -0700 Subject: [PATCH] Use RDN Parsing Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com> --- .../x509/SubjectX500PrincipalExtractor.java | 44 ++++++++++------ .../SubjectX500PrincipalExtractorTests.java | 16 ++++++ .../preauth/x509/X509TestUtils.java | 50 +++++++++++++++++++ 3 files changed, 96 insertions(+), 14 deletions(-) diff --git a/web/src/main/java/org/springframework/security/web/authentication/preauth/x509/SubjectX500PrincipalExtractor.java b/web/src/main/java/org/springframework/security/web/authentication/preauth/x509/SubjectX500PrincipalExtractor.java index 73146b677b..c955fb72fe 100644 --- a/web/src/main/java/org/springframework/security/web/authentication/preauth/x509/SubjectX500PrincipalExtractor.java +++ b/web/src/main/java/org/springframework/security/web/authentication/preauth/x509/SubjectX500PrincipalExtractor.java @@ -17,9 +17,11 @@ package org.springframework.security.web.authentication.preauth.x509; import java.security.cert.X509Certificate; -import java.util.regex.Matcher; -import java.util.regex.Pattern; +import java.util.List; +import javax.naming.InvalidNameException; +import javax.naming.ldap.LdapName; +import javax.naming.ldap.Rdn; import javax.security.auth.x500.X500Principal; import org.apache.commons.logging.Log; @@ -47,14 +49,13 @@ public final class SubjectX500PrincipalExtractor implements X509PrincipalExtract private final Log logger = LogFactory.getLog(getClass()); - private static final Pattern EMAIL_SUBJECT_DN_PATTERN = Pattern.compile("OID.1.2.840.113549.1.9.1=(.*?)(?:,|$)", - Pattern.CASE_INSENSITIVE); + private static final String EMAIL_SUBJECT_DN_TYPE = "OID.1.2.840.113549.1.9.1"; - private static final Pattern CN_SUBJECT_DN_PATTERN = Pattern.compile("CN=(.*?)(?:,|$)", Pattern.CASE_INSENSITIVE); + private static final String CN_SUBJECT_DN_TYPE = "CN"; private MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor(); - private Pattern subjectDnPattern = CN_SUBJECT_DN_PATTERN; + private String subjectDnType = CN_SUBJECT_DN_TYPE; private String x500PrincipalFormat = X500Principal.RFC2253; @@ -64,16 +65,31 @@ public final class SubjectX500PrincipalExtractor implements X509PrincipalExtract X500Principal principal = clientCert.getSubjectX500Principal(); String subjectDN = principal.getName(this.x500PrincipalFormat); this.logger.debug(LogMessage.format("Subject DN is '%s'", subjectDN)); - Matcher matcher = this.subjectDnPattern.matcher(subjectDN); - if (!matcher.find()) { - throw new BadCredentialsException(this.messages.getMessage("SubjectX500PrincipalExtractor.noMatching", - new Object[] { subjectDN }, "No matching pattern was found in subject DN: {0}")); - } - String principalName = matcher.group(1); + String principalName = getSubject(subjectDN); this.logger.debug(LogMessage.format("Extracted Principal name is '%s'", principalName)); return principalName; } + private List getDns(String subjectDn) { + try { + return new LdapName(subjectDn).getRdns(); + } + catch (InvalidNameException ex) { + throw new BadCredentialsException("Failed to parse client certificate", ex); + } + } + + private String getSubject(String subjectDn) { + for (Rdn rdn : getDns(subjectDn)) { + String type = rdn.getType(); + if (this.subjectDnType.equals(type)) { + return String.valueOf(rdn.getValue()); + } + } + throw new BadCredentialsException(this.messages.getMessage("SubjectX500PrincipalExtractor.noMatching", + new Object[] { subjectDn }, "No matching pattern was found in subject DN: {0}")); + } + @Override public void setMessageSource(MessageSource messageSource) { Assert.notNull(messageSource, "messageSource cannot be null"); @@ -104,11 +120,11 @@ public final class SubjectX500PrincipalExtractor implements X509PrincipalExtract */ public void setExtractPrincipalNameFromEmail(boolean extractPrincipalNameFromEmail) { if (extractPrincipalNameFromEmail) { - this.subjectDnPattern = EMAIL_SUBJECT_DN_PATTERN; + this.subjectDnType = EMAIL_SUBJECT_DN_TYPE; this.x500PrincipalFormat = X500Principal.RFC1779; } else { - this.subjectDnPattern = CN_SUBJECT_DN_PATTERN; + this.subjectDnType = CN_SUBJECT_DN_TYPE; this.x500PrincipalFormat = X500Principal.RFC2253; } } diff --git a/web/src/test/java/org/springframework/security/web/authentication/preauth/x509/SubjectX500PrincipalExtractorTests.java b/web/src/test/java/org/springframework/security/web/authentication/preauth/x509/SubjectX500PrincipalExtractorTests.java index 3e14a3d914..d5bc6635b0 100644 --- a/web/src/test/java/org/springframework/security/web/authentication/preauth/x509/SubjectX500PrincipalExtractorTests.java +++ b/web/src/test/java/org/springframework/security/web/authentication/preauth/x509/SubjectX500PrincipalExtractorTests.java @@ -53,6 +53,22 @@ public class SubjectX500PrincipalExtractorTests { assertThat(principal).isEqualTo("Duke"); } + @Test + void extractWhenDnEmbeddedInCnThenExtractsPrincipalName() throws Exception { + Object principal = this.extractor.extractPrincipal(X509TestUtils.buildTestCertficateWithEmbeddedDn()); + + assertThat(principal).isEqualTo("luke"); + } + + @Test + void extractWhenEmailDnEmbeddedInCnThenExtractsEmail() throws Exception { + this.extractor.setExtractPrincipalNameFromEmail(true); + + Object principal = this.extractor.extractPrincipal(X509TestUtils.buildTestCertficateWithEmbeddedEmailDn()); + + assertThat(principal).isEqualTo("luke@monkeymachine"); + } + @Test void setMessageSourceWhenNullThenThrowsException() { assertThatIllegalArgumentException().isThrownBy(() -> this.extractor.setMessageSource(null)); diff --git a/web/src/test/java/org/springframework/security/web/authentication/preauth/x509/X509TestUtils.java b/web/src/test/java/org/springframework/security/web/authentication/preauth/x509/X509TestUtils.java index 3c9c844424..66c392b77f 100644 --- a/web/src/test/java/org/springframework/security/web/authentication/preauth/x509/X509TestUtils.java +++ b/web/src/test/java/org/springframework/security/web/authentication/preauth/x509/X509TestUtils.java @@ -135,4 +135,54 @@ public final class X509TestUtils { return (X509Certificate) cf.generateCertificate(in); } + public static X509Certificate buildTestCertficateWithEmbeddedDn() throws Exception { + String cert = "-----BEGIN CERTIFICATE-----\n" + + "MIIDDTCCAfWgAwIBAgIJANSyvk4gJhqPMA0GCSqGSIb3DQEBCwUAMEYxDTALBgNV\n" + + "BAMMBGx1a2UxETAPBgNVBAsMCENOPWR1a2UsMRUwEwYDVQQKDAxFeGFtcGxlIENv\n" + + "cnAxCzAJBgNVBAYTAlVTMB4XDTI2MDEwNDE5MjY0N1oXDTI3MDEwNTE5MjY0N1ow\n" + + "RjENMAsGA1UEAwwEbHVrZTERMA8GA1UECwwIQ049ZHVrZSwxFTATBgNVBAoMDEV4\n" + + "YW1wbGUgQ29ycDELMAkGA1UEBhMCVVMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw\n" + + "ggEKAoIBAQDU9fY74nEFbBKfIef7CK02J/BJb42sIF9kD8eHN5OvEwLQBeTh30it\n" + + "E7LLalXyOXeUFkPe1N1ZhGdVak9udsIqULSvQaWqTbN+IrAGklZAxuXYTC1GbhMF\n" + + "AkGWWM55J2SNqVGQaHzZUn6VPxWaDft6nZR0DxuvXMYM5kVG6VErdB3ygGUv8cjQ\n" + + "QBKAYpsZeRldnauRPt2dImmGTagvSuJVyr8X/AioE2Rl0guii456AKw+QSvRiZ+g\n" + + "w08Y8C9nDyzQmurqpdYYkp0X+4yqm1iVowMX+tSPvHnlqJdvVzaW2b0yRzrrT6ao\n" + + "UCgw25slR1P1IcyzqPKWQIoQRnYIaX1bAgMBAAEwDQYJKoZIhvcNAQELBQADggEB\n" + + "AIos+nr8DFM6bAt9AI/79O/12hcN7gVv4F3P4Vz6NRRkkvsb9WMN8fLLDEsEJ/BQ\n" + + "eQkAVnhlmAe++vrqy8OTHoQ7F5C3K0zrr19NLNoyNFTkXkFgnm4ZhYinSbusuIb7\n" + + "LPYoyCnEEiMdl0VMWWSWcOvZpipbvTtH3CiVxTqXLjFFNraEAyUN50kXjo/zuHpK\n" + + "HzTS1BAu0li9GdV3Da2ELdDx90zaUym7dDIejY4YUlXYIJ5UUYS61fqtgOHGLLdb\n" + + "UXGAr5gqEe7OrQ9D4ebg9w5ciTb7g1H2CmirjTf/rkii8AojmsGFKIfGVe3gY6EB\n" + "o9eF5FV9V9leo5yLo25ev08=\n" + + "-----END CERTIFICATE-----"; + ByteArrayInputStream in = new ByteArrayInputStream(cert.getBytes()); + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + return (X509Certificate) cf.generateCertificate(in); + } + + public static X509Certificate buildTestCertficateWithEmbeddedEmailDn() throws Exception { + String cert = "-----BEGIN CERTIFICATE-----\n" + + "MIIDfDCCAmSgAwIBAgIIXHoOUFeZ29MwDQYJKoZIhvcNAQELBQAwfjEhMB8GCSqG\n" + + "SIb3DQEJARYSbHVrZUBtb25rZXltYWNoaW5lMTUwMwYDVQQLDCxPSUQuMS4yLjg0\n" + + "MC4xMTM1NDkuMS45LjE9ZHVrZUBnb3JpbGxhZ2FkZ2V0LDEVMBMGA1UECgwMRXhh\n" + + "bXBsZSBDb3JwMQswCQYDVQQGEwJVUzAeFw0yNjAxMDQxOTMxMDhaFw0yNzAxMDUx\n" + + "OTMxMDhaMH4xITAfBgkqhkiG9w0BCQEWEmx1a2VAbW9ua2V5bWFjaGluZTE1MDMG\n" + + "A1UECwwsT0lELjEuMi44NDAuMTEzNTQ5LjEuOS4xPWR1a2VAZ29yaWxsYWdhZGdl\n" + + "dCwxFTATBgNVBAoMDEV4YW1wbGUgQ29ycDELMAkGA1UEBhMCVVMwggEiMA0GCSqG\n" + + "SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDBuIQWnj+uvvG+4ZIyFMs4dSbiBavubCmC\n" + + "hudrHr93hP19QbPulbHRTVCUqEi8efvq+J9jmMdPd7tziuDX02PeG9uljp9+c5Ir\n" + + "pw9/oMoTRkF7K4PK1JxLN4tcgxjxVA4QkS+MjKLPeHrYyGCjKspcHbi+zBiQ9Xqp\n" + + "yHWq6N5XPd6mEj2gh0zamnsJCeUCOX4SJbcp3MFtcYzhguHAeVhy9Jv+EAMJejDn\n" + + "YIZmMUdP6Ykf2zTzs/4L3bRZb0oS5WvfeRdJB6SKg8mNO/jdGX87krSio//cRdDy\n" + + "TGQK+YCVDf8GyLLavYZW56AJbZxL3MWgHYilQjj4p+Kw/PWpaBVvAgMBAAEwDQYJ\n" + + "KoZIhvcNAQELBQADggEBAKVTMIo8JO0H0HRrpsEDP17E2pnfMJV4g70BwClUMMek\n" + + "wNIWZn+6XPR8oObzzjnVWXjrovMkmmyFk0vWIpF68MPyiQ++5fwdzOZiQtUP177n\n" + + "9ulAtLoIJld3olGeL9VsCZGp3J2PqiDe613zd+bkSUG1lQYC2awozWqJEdvwJJtf\n" + + "j9nlhyMsARKEEu3tFGJsCHST3XhbhFKOraf/GZ21xW650R7ap0ZNaEiB16M2a5Oe\n" + + "WXasgUukIo82Z8+yK4IITeCcr0aA1fJxwhU8J6qfYWloaoirSYj487HRnPPv3X/b\n" + + "RxZynIjtGKygT6T1dRaWennmoitqfprJnEO2tlhLwP0=\n" + "-----END CERTIFICATE-----"; + ByteArrayInputStream in = new ByteArrayInputStream(cert.getBytes()); + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + return (X509Certificate) cf.generateCertificate(in); + } + }