Skip to content

Commit d8a408d

Browse files
authored
Extract principal from certificate RDN (#137230) (#138388)
Add support for RDN attribute value principal extraction in PKI Realm. This allows extracting the principal from a specific RDN attribute (e.g., CN, OU) in the client certificate, improving flexibility in identifying users based on certificate details.
1 parent 6198e8a commit d8a408d

File tree

7 files changed

+340
-18
lines changed

7 files changed

+340
-18
lines changed

docs/changelog/137230.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pr: 137230
2+
summary: Principal Extraction from Certificate RDN Attribute Value in PKI Realm
3+
area: Security
4+
type: bug
5+
issues: []

libs/ssl-config/src/main/java/org/elasticsearch/common/ssl/DerParser.java

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -36,21 +36,22 @@ public final class DerParser {
3636
private static final int CONSTRUCTED = 0x20;
3737

3838
// Tag and data types
39-
static final class Type {
40-
static final int INTEGER = 0x02;
41-
static final int OCTET_STRING = 0x04;
42-
static final int OBJECT_OID = 0x06;
43-
static final int SEQUENCE = 0x10;
44-
static final int NUMERIC_STRING = 0x12;
45-
static final int PRINTABLE_STRING = 0x13;
46-
static final int VIDEOTEX_STRING = 0x15;
47-
static final int IA5_STRING = 0x16;
48-
static final int GRAPHIC_STRING = 0x19;
49-
static final int ISO646_STRING = 0x1A;
50-
static final int GENERAL_STRING = 0x1B;
51-
static final int UTF8_STRING = 0x0C;
52-
static final int UNIVERSAL_STRING = 0x1C;
53-
static final int BMP_STRING = 0x1E;
39+
public static final class Type {
40+
public static final int INTEGER = 0x02;
41+
public static final int OCTET_STRING = 0x04;
42+
public static final int OBJECT_OID = 0x06;
43+
public static final int SEQUENCE = 0x10;
44+
public static final int SET = 0x11;
45+
public static final int NUMERIC_STRING = 0x12;
46+
public static final int PRINTABLE_STRING = 0x13;
47+
public static final int VIDEOTEX_STRING = 0x15;
48+
public static final int IA5_STRING = 0x16;
49+
public static final int GRAPHIC_STRING = 0x19;
50+
public static final int ISO646_STRING = 0x1A;
51+
public static final int GENERAL_STRING = 0x1B;
52+
public static final int UTF8_STRING = 0x0C;
53+
public static final int UNIVERSAL_STRING = 0x1C;
54+
public static final int BMP_STRING = 0x1E;
5455
}
5556

5657
private InputStream derInputStream;

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/pki/PkiRealmSettings.java

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66
*/
77
package org.elasticsearch.xpack.core.security.authc.pki;
88

9+
import com.unboundid.ldap.sdk.LDAPException;
10+
import com.unboundid.ldap.sdk.schema.AttributeTypeDefinition;
11+
import com.unboundid.ldap.sdk.schema.Schema;
12+
913
import org.elasticsearch.common.settings.SecureString;
1014
import org.elasticsearch.common.settings.Setting;
1115
import org.elasticsearch.core.TimeValue;
@@ -29,6 +33,33 @@ public final class PkiRealmSettings {
2933
key -> new Setting<>(key, DEFAULT_USERNAME_PATTERN, s -> Pattern.compile(s, Pattern.CASE_INSENSITIVE), Setting.Property.NodeScope)
3034
);
3135

36+
public static final Setting.AffixSetting<String> USERNAME_RDN_OID_SETTING = Setting.affixKeySetting(
37+
RealmSettings.realmSettingPrefix(TYPE),
38+
"username_rdn_oid",
39+
key -> Setting.simpleString(key, Setting.Property.NodeScope)
40+
);
41+
42+
public static final Setting.AffixSetting<String> USERNAME_RDN_NAME_SETTING = Setting.affixKeySetting(
43+
RealmSettings.realmSettingPrefix(TYPE),
44+
"username_rdn_name",
45+
key -> new Setting<>(key, (String) null, s -> {
46+
if (s == null) {
47+
return "";
48+
}
49+
Schema schema;
50+
try {
51+
schema = Schema.getDefaultStandardSchema();
52+
} catch (LDAPException e) {
53+
throw new IllegalStateException("Unexpected error occurred obtaining default LDAP schema", e);
54+
}
55+
AttributeTypeDefinition atd = schema.getAttributeType(s);
56+
if (atd == null) {
57+
throw new IllegalArgumentException("Unknown RDN name [" + s + "] for setting [" + key + "]");
58+
}
59+
return atd.getOID();
60+
}, Setting.Property.NodeScope)
61+
);
62+
3263
private static final TimeValue DEFAULT_TTL = TimeValue.timeValueMinutes(20);
3364
public static final Setting.AffixSetting<TimeValue> CACHE_TTL_SETTING = Setting.affixKeySetting(
3465
RealmSettings.realmSettingPrefix(TYPE),
@@ -75,6 +106,8 @@ private PkiRealmSettings() {}
75106
public static Set<Setting.AffixSetting<?>> getSettings() {
76107
Set<Setting.AffixSetting<?>> settings = new HashSet<>();
77108
settings.add(USERNAME_PATTERN_SETTING);
109+
settings.add(USERNAME_RDN_OID_SETTING);
110+
settings.add(USERNAME_RDN_NAME_SETTING);
78111
settings.add(CACHE_TTL_SETTING);
79112
settings.add(CACHE_MAX_USERS_SETTING);
80113
settings.add(DELEGATION_ENABLED_SETTING);

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/pki/PkiRealm.java

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import org.elasticsearch.common.cache.Cache;
1313
import org.elasticsearch.common.cache.CacheBuilder;
1414
import org.elasticsearch.common.hash.MessageDigests;
15+
import org.elasticsearch.common.settings.SettingsException;
1516
import org.elasticsearch.common.ssl.SslConfiguration;
1617
import org.elasticsearch.common.ssl.SslTrustConfig;
1718
import org.elasticsearch.common.util.concurrent.ReleasableLock;
@@ -51,6 +52,7 @@
5152

5253
import javax.net.ssl.X509ExtendedTrustManager;
5354
import javax.net.ssl.X509TrustManager;
55+
import javax.security.auth.x500.X500Principal;
5456

5557
import static org.elasticsearch.core.Strings.format;
5658

@@ -76,6 +78,7 @@ public class PkiRealm extends Realm implements CachingRealm {
7678

7779
private final X509TrustManager trustManager;
7880
private final Pattern principalPattern;
81+
private final String principalRdnOid;
7982
private final UserRoleMapper roleMapper;
8083
private final Cache<BytesKey, User> cache;
8184
private DelegatedAuthorizationSupport delegatedRealms;
@@ -91,6 +94,18 @@ public PkiRealm(RealmConfig config, ResourceWatcherService watcherService, UserR
9194
this.delegationEnabled = config.getSetting(PkiRealmSettings.DELEGATION_ENABLED_SETTING);
9295
this.trustManager = trustManagers(config);
9396
this.principalPattern = config.getSetting(PkiRealmSettings.USERNAME_PATTERN_SETTING);
97+
String rdnOid = config.getSetting(PkiRealmSettings.USERNAME_RDN_OID_SETTING);
98+
String rdnOidFromName = config.getSetting(PkiRealmSettings.USERNAME_RDN_NAME_SETTING);
99+
if (false == rdnOid.isEmpty() && false == rdnOidFromName.isEmpty()) {
100+
throw new SettingsException(
101+
"Both ["
102+
+ config.getConcreteSetting(PkiRealmSettings.USERNAME_RDN_OID_SETTING).getKey()
103+
+ "] and ["
104+
+ config.getConcreteSetting(PkiRealmSettings.USERNAME_RDN_NAME_SETTING).getKey()
105+
+ "] are set. Only one of these settings can be configured."
106+
);
107+
}
108+
this.principalRdnOid = false == rdnOid.isEmpty() ? rdnOid : (false == rdnOidFromName.isEmpty() ? rdnOidFromName : null);
94109
this.roleMapper = roleMapper;
95110
this.roleMapper.clearRealmCacheOnChange(this);
96111
this.cache = CacheBuilder.<BytesKey, User>builder()
@@ -133,7 +148,7 @@ public X509AuthenticationToken token(ThreadContext context) {
133148
// validation). In this case the principal should be set by the realm that completes the authentication. But in the common case,
134149
// where a single PKI realm is configured, there is no risk of eagerly parsing the principal before authentication and it also
135150
// maintains BWC.
136-
String parsedPrincipal = getPrincipalFromSubjectDN(principalPattern, token, logger);
151+
String parsedPrincipal = getPrincipalFromToken(token);
137152
if (parsedPrincipal == null) {
138153
return null;
139154
}
@@ -164,7 +179,7 @@ public void authenticate(AuthenticationToken authToken, ActionListener<Authentic
164179
// parse the principal again after validating the cert chain, and do not rely on the token.principal one, because that could
165180
// be set by a different realm that failed trusted chain validation. We SHOULD NOT parse the principal BEFORE this step, but
166181
// we do it for BWC purposes. Changing this is a breaking change.
167-
final String principal = getPrincipalFromSubjectDN(principalPattern, token, logger);
182+
final String principal = getPrincipalFromToken(token);
168183
if (principal == null) {
169184
logger.debug(
170185
() -> format(
@@ -231,6 +246,24 @@ public void lookupUser(String username, ActionListener<User> listener) {
231246
listener.onResponse(null);
232247
}
233248

249+
String getPrincipalFromToken(X509AuthenticationToken token) {
250+
return principalRdnOid != null
251+
? getPrincipalFromRdnAttribute(principalRdnOid, token, logger)
252+
: getPrincipalFromSubjectDN(principalPattern, token, logger);
253+
}
254+
255+
static String getPrincipalFromRdnAttribute(String principalRdnOid, X509AuthenticationToken token, Logger logger) {
256+
X500Principal certPrincipal = token.credentials()[0].getSubjectX500Principal();
257+
String principal = RdnFieldExtractor.extract(certPrincipal.getEncoded(), principalRdnOid);
258+
if (principal == null) {
259+
logger.debug(
260+
() -> format("the extracted principal from DN [%s] using RDN OID [%s] is empty", certPrincipal.toString(), principalRdnOid)
261+
);
262+
return null;
263+
}
264+
return principal;
265+
}
266+
234267
static String getPrincipalFromSubjectDN(Pattern principalPattern, X509AuthenticationToken token, Logger logger) {
235268
String dn = token.credentials()[0].getSubjectX500Principal().toString();
236269
Matcher matcher = principalPattern.matcher(dn);
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
package org.elasticsearch.xpack.security.authc.pki;
9+
10+
import org.elasticsearch.common.ssl.DerParser;
11+
12+
import java.io.IOException;
13+
14+
/**
15+
* Utility class to extract RDN field values from X500 principal DER encoding.
16+
*/
17+
public class RdnFieldExtractor {
18+
19+
public static String extract(byte[] encoded, String oid) {
20+
try {
21+
return doExtract(encoded, oid);
22+
} catch (IOException | IllegalStateException e) {
23+
return null; // invalid encoding
24+
}
25+
}
26+
27+
private static String doExtract(byte[] encoded, String oid) throws IOException {
28+
DerParser parser = new DerParser(encoded);
29+
30+
DerParser.Asn1Object dnSequence = parser.readAsn1Object(DerParser.Type.SEQUENCE);
31+
DerParser sequenceParser = dnSequence.getParser();
32+
33+
String value = null;
34+
35+
while (true) {
36+
try {
37+
DerParser.Asn1Object rdnSet = sequenceParser.readAsn1Object(DerParser.Type.SET); // throws IOException on EOF
38+
DerParser setParser = rdnSet.getParser();
39+
40+
while (true) {
41+
try {
42+
DerParser.Asn1Object attrSeq = setParser.readAsn1Object(DerParser.Type.SEQUENCE); // throws IOException on EOF
43+
DerParser attrParser = attrSeq.getParser();
44+
45+
String attrOid = attrParser.readAsn1Object().getOid();
46+
DerParser.Asn1Object attrValue = attrParser.readAsn1Object();
47+
if (oid.equals(attrOid)) {
48+
value = attrValue.getString(); // retain last (most-significant) occurrence
49+
}
50+
} catch (IOException e) {
51+
break; // RDN SET EOF
52+
}
53+
}
54+
} catch (IOException e) {
55+
break; // DN SEQUENCE EOF
56+
}
57+
}
58+
59+
return value;
60+
}
61+
62+
}

x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/pki/PkiRealmTests.java

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import org.elasticsearch.common.settings.SecureString;
1414
import org.elasticsearch.common.settings.Setting;
1515
import org.elasticsearch.common.settings.Settings;
16+
import org.elasticsearch.common.settings.SettingsException;
1617
import org.elasticsearch.common.ssl.SslConfigException;
1718
import org.elasticsearch.common.util.CollectionUtils;
1819
import org.elasticsearch.common.util.concurrent.ThreadContext;
@@ -230,7 +231,7 @@ private AuthenticationResult<User> authenticate(X509AuthenticationToken token, P
230231
}
231232

232233
public void testCustomUsernamePatternMatches() throws Exception {
233-
final Settings settings = Settings.builder()
234+
Settings settings = Settings.builder()
234235
.put(globalSettings)
235236
.put("xpack.security.authc.realms.pki.my_pki.username_pattern", "OU=(.*?),")
236237
.build();
@@ -249,6 +250,74 @@ public void testCustomUsernamePatternMatches() throws Exception {
249250
assertThat(user.roles().length, is(0));
250251
}
251252

253+
public void testRdnOidMatches() throws Exception {
254+
Settings settings = Settings.builder()
255+
.put(globalSettings)
256+
.put("xpack.security.authc.realms.pki.my_pki.username_rdn_oid", "2.5.4.11")
257+
.build();
258+
ThreadContext threadContext = new ThreadContext(settings);
259+
X509Certificate certificate = readCert(getDataPath("/org/elasticsearch/xpack/security/transport/ssl/certs/simple/testnode.crt"));
260+
UserRoleMapper roleMapper = buildRoleMapper();
261+
PkiRealm realm = buildRealm(roleMapper, settings);
262+
threadContext.putTransient(PkiRealm.PKI_CERT_HEADER_NAME, new X509Certificate[] { certificate });
263+
264+
X509AuthenticationToken token = realm.token(threadContext);
265+
User user = authenticate(token, realm).getValue();
266+
assertThat(user, is(notNullValue()));
267+
assertThat(user.principal(), is("elasticsearch"));
268+
}
269+
270+
public void testRdnOidNameMatches() throws Exception {
271+
Settings settings = Settings.builder()
272+
.put(globalSettings)
273+
.put("xpack.security.authc.realms.pki.my_pki.username_rdn_name", "OU")
274+
.build();
275+
ThreadContext threadContext = new ThreadContext(settings);
276+
X509Certificate certificate = readCert(getDataPath("/org/elasticsearch/xpack/security/transport/ssl/certs/simple/testnode.crt"));
277+
UserRoleMapper roleMapper = buildRoleMapper();
278+
PkiRealm realm = buildRealm(roleMapper, settings);
279+
threadContext.putTransient(PkiRealm.PKI_CERT_HEADER_NAME, new X509Certificate[] { certificate });
280+
281+
X509AuthenticationToken token = realm.token(threadContext);
282+
User user = authenticate(token, realm).getValue();
283+
assertThat(user, is(notNullValue()));
284+
assertThat(user.principal(), is("elasticsearch"));
285+
}
286+
287+
public void testRdnOidNameNotMatches() throws Exception {
288+
Settings settings = Settings.builder()
289+
.put(globalSettings)
290+
.put("xpack.security.authc.realms.pki.my_pki.username_rdn_name", "UID")
291+
.build();
292+
ThreadContext threadContext = new ThreadContext(settings);
293+
X509Certificate certificate = readCert(getDataPath("/org/elasticsearch/xpack/security/transport/ssl/certs/simple/testnode.crt"));
294+
UserRoleMapper roleMapper = buildRoleMapper();
295+
PkiRealm realm = buildRealm(roleMapper, settings);
296+
threadContext.putTransient(PkiRealm.PKI_CERT_HEADER_NAME, new X509Certificate[] { certificate });
297+
298+
X509AuthenticationToken token = realm.token(threadContext);
299+
assertThat(token, is(nullValue()));
300+
}
301+
302+
public void testRdnOidNameUnknown() {
303+
Settings settings = Settings.builder()
304+
.put(globalSettings)
305+
.put("xpack.security.authc.realms.pki.my_pki.username_rdn_name", "UNKNOWN_OID_NAME")
306+
.build();
307+
UserRoleMapper roleMapper = buildRoleMapper();
308+
assertThrows(IllegalArgumentException.class, () -> buildRealm(roleMapper, settings));
309+
}
310+
311+
public void testRedundantRdnOidSettings() {
312+
Settings settings = Settings.builder()
313+
.put(globalSettings)
314+
.put("xpack.security.authc.realms.pki.my_pki.username_rdn_oid", "2.5.4.3")
315+
.put("xpack.security.authc.realms.pki.my_pki.username_rdn_name", "UID")
316+
.build();
317+
UserRoleMapper roleMapper = buildRoleMapper();
318+
assertThrows(SettingsException.class, () -> buildRealm(roleMapper, settings));
319+
}
320+
252321
public void testCustomUsernamePatternMismatchesAndNullToken() throws Exception {
253322
final Settings settings = Settings.builder()
254323
.put(globalSettings)

0 commit comments

Comments
 (0)