-
Notifications
You must be signed in to change notification settings - Fork 2.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Allow to configure certificate role mapping attribute
- Loading branch information
1 parent
5d72d9b
commit d9d5ea5
Showing
39 changed files
with
711 additions
and
87 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
183 changes: 183 additions & 0 deletions
183
...untime/src/main/java/io/quarkus/vertx/http/runtime/security/CertificateRoleAttribute.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,183 @@ | ||
package io.quarkus.vertx.http.runtime.security; | ||
|
||
import static io.quarkus.vertx.http.runtime.security.HttpSecurityUtils.COMMON_NAME; | ||
import static io.quarkus.vertx.http.runtime.security.HttpSecurityUtils.getRdnValue; | ||
|
||
import java.nio.charset.StandardCharsets; | ||
import java.security.cert.CertificateParsingException; | ||
import java.security.cert.X509Certificate; | ||
import java.util.HashSet; | ||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.Set; | ||
import java.util.function.Function; | ||
|
||
import javax.security.auth.x500.X500Principal; | ||
|
||
import org.jboss.logging.Logger; | ||
|
||
import io.quarkus.runtime.configuration.ConfigurationException; | ||
import io.vertx.ext.auth.impl.asn.ASN1; | ||
|
||
public record CertificateRoleAttribute(Function<X509Certificate, Set<String>> rolesMapper) { | ||
|
||
private static final Logger log = Logger.getLogger(CertificateRoleAttribute.class); | ||
private static final String SAN_PREFIX = "SAN_"; | ||
private static final String DN_PREFIX = "DN_"; | ||
|
||
CertificateRoleAttribute(String configValue, Map<String, Set<String>> roles) { | ||
this(of(configValue.toUpperCase(), Map.copyOf(roles))); | ||
} | ||
|
||
private static Function<X509Certificate, Set<String>> of(String configValue, Map<String, Set<String>> roles) { | ||
if (configValue.contains(SAN_PREFIX)) { | ||
|
||
return new Function<X509Certificate, Set<String>>() { | ||
@Override | ||
public Set<String> apply(X509Certificate certificate) { | ||
return extractRolesFromCertSan(certificate, SAN.valueOf(configValue).generalNameType, roles); | ||
} | ||
}; | ||
} else if (configValue.startsWith(DN_PREFIX)) { | ||
|
||
String rdnType = configValue.substring(DN_PREFIX.length()); | ||
return new Function<X509Certificate, Set<String>>() { | ||
@Override | ||
public Set<String> apply(X509Certificate certificate) { | ||
return extractRolesFromCertRdn(certificate, roles, rdnType); | ||
} | ||
}; | ||
} else { | ||
|
||
throw new ConfigurationException("Invalid certificate role attribute '%s'".formatted(configValue), | ||
Set.of("quarkus.http.auth.certificate-role-attribute")); | ||
} | ||
} | ||
|
||
private static Set<String> extractRolesFromCertRdn(X509Certificate certificate, Map<String, Set<String>> roles, | ||
String rdnType) { | ||
X500Principal principal = certificate.getSubjectX500Principal(); | ||
if (principal == null || principal.getName() == null) { | ||
return Set.of(); | ||
} | ||
Set<String> matchedRoles; | ||
if (COMMON_NAME.equals(rdnType)) { | ||
matchedRoles = roles.get(principal.getName()); | ||
if (matchedRoles != null) { | ||
return matchedRoles; | ||
} | ||
} | ||
String rdnValue = getRdnValue(principal, rdnType); | ||
if (rdnValue != null) { | ||
matchedRoles = roles.get(rdnValue); | ||
if (matchedRoles != null) { | ||
return matchedRoles; | ||
} | ||
} | ||
return Set.of(); | ||
} | ||
|
||
private enum SAN { | ||
|
||
/** | ||
* Subject Alternative Name field Other Name. | ||
* Please note that only simple case of UTF8 identifier mapping is support. | ||
* For example, you can map 'other-identifier' to the SecurityIdentity roles. | ||
* If you use 'openssl' tool, supported Other name definition would look like this: | ||
* <code>subjectAltName=otherName:1.2.3.4;UTF8:other-identifier</code> | ||
*/ | ||
SAN_ANY(0), | ||
/** | ||
* Subject Alternative Name field RFC 822 Name. | ||
*/ | ||
SAN_RFC822(1), | ||
/** | ||
* Subject Alternative Name field DNS Name. | ||
*/ | ||
SAN_DNS(2), | ||
/** | ||
* Subject Alternative Name field x400 Address. | ||
*/ | ||
SAN_X400(3), | ||
/** | ||
* Subject Alternative Name field Directory Name. | ||
*/ | ||
SAN_DIRECTORY(4), | ||
/** | ||
* Subject Alternative Name field EDI Party Name. | ||
*/ | ||
SAN_EDI(5), | ||
/** | ||
* Subject Alternative Name field Uniform Resource Identifier (URI). | ||
*/ | ||
SAN_URI(6), | ||
/** | ||
* Subject Alternative Name field IP Address (IP). | ||
*/ | ||
SAN_IP(7), | ||
/** | ||
* Subject Alternative Name field Registered Object Identifier (OID). | ||
*/ | ||
SAN_OID(8); | ||
|
||
private final int generalNameType; | ||
|
||
SAN(int generalNameType) { | ||
this.generalNameType = generalNameType; | ||
} | ||
} | ||
|
||
private static Set<String> extractRolesFromCertSan(X509Certificate certificate, int generalNameType, | ||
Map<String, Set<String>> roles) { | ||
final Set<String> result = new HashSet<>(); | ||
try { | ||
var sanList = certificate.getSubjectAlternativeNames(); | ||
if (sanList != null && !sanList.isEmpty()) { | ||
for (List<?> objects : sanList) { | ||
if (objects != null && objects.size() >= 2) { | ||
if (objects.get(0) instanceof Integer thatGeneralNameType) { | ||
if (thatGeneralNameType == generalNameType) { | ||
|
||
// special handling for Other name | ||
if (thatGeneralNameType == 0 && objects.get(1) instanceof byte[] byteArr) { | ||
var asn1 = ASN1.parseASN1(byteArr); | ||
if (asn1.is(ASN1.SEQUENCE) && asn1.length() == 2) { | ||
|
||
var otherIdentifier = asn1.object(1); | ||
while (otherIdentifier.length() == 1 | ||
&& otherIdentifier.is(ASN1.CONTEXT_SPECIFIC)) { | ||
// there can be one extra context specific ASN with OpenJDK 17, hence loop | ||
otherIdentifier = otherIdentifier.object(0); | ||
} | ||
|
||
if (otherIdentifier.is(ASN1.UTF8_STRING)) { | ||
var value = new String(otherIdentifier.binary(0), StandardCharsets.UTF_8); | ||
if (roles.containsKey(value)) { | ||
result.addAll(roles.get(value)); | ||
break; | ||
} | ||
} | ||
} | ||
} | ||
|
||
for (int i = 1; i < objects.size(); i++) { | ||
if (objects.get(i) instanceof String name) { | ||
if (roles.containsKey(name)) { | ||
result.addAll(roles.get(name)); | ||
} | ||
} | ||
} | ||
} | ||
continue; | ||
} | ||
} | ||
log.tracef("Cannot map SecurityIdentity roles from '%s' due to unsupported format", objects); | ||
break; | ||
} | ||
} | ||
} catch (CertificateParsingException e) { | ||
log.tracef("Cannot map SecurityIdentity roles as certificate parsing failed"); | ||
} | ||
return Set.copyOf(result); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.