Use RDN Parsing

Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com>
This commit is contained in:
Josh Cummings 2026-01-05 12:52:39 -07:00
parent 9ad7981e36
commit 88118afb8f
3 changed files with 96 additions and 14 deletions

View File

@ -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<Rdn> 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;
}
}

View File

@ -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));

View File

@ -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);
}
}