X500Utils.java
/*
* Copyright © 2025 Indiana University
* All rights reserved.
*
* BSD 3-Clause License
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* - Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* - Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* - Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package edu.iu.auth.config;
import java.io.ByteArrayOutputStream;
import java.nio.CharBuffer;
import java.security.Principal;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Queue;
import java.util.function.Consumer;
import java.util.function.Supplier;
import javax.security.auth.x500.X500Principal;
import edu.iu.IuIterable;
import edu.iu.IuObject;
import edu.iu.IuText;
/**
* Provides basic metadata inspection utilities for {@link X500Principal}.
*/
public final class X500Utils {
/**
* Maps required X500 OID values to standard short names
*/
static Map<String, String> X500_OIDMAP = Map.of( //
"2.5.4.3", "CN", //
"2.5.4.7", "L", //
"2.5.4.8", "ST", //
"2.5.4.10", "O", //
"2.5.4.11", "OU", //
"2.5.4.6", "C", //
"2.5.4.9", "STREET", //
"0.9.2342.19200300.100.1.25", "DC", //
"0.9.2342.19200300.100.1.1", "UID");
/**
* Determines the common name of a principal.
*
* @param principal principal
* @return parsed CN value from {@link X500Principal}; else
* {@link Principal#getName()}
*/
public static String getCommonName(X500Principal principal) {
final var name = principal.getName();
String cn = null;
final var nameBuilder = new StringBuilder();
for (final var rdn : parse(name)) {
cn = IuObject.first(cn, rdn.get("cn"), rdn.get("CN"), rdn.get("uid"), rdn.get("UID"));
final var dc = IuObject.first(rdn.get("dc"), rdn.get("DC"));
if (dc != null) {
if (nameBuilder.length() == 0)
nameBuilder.append('@');
else
nameBuilder.append('.');
nameBuilder.append(dc);
}
}
IuObject.convert(cn, a -> nameBuilder.insert(0, a));
return nameBuilder.length() == 0 ? null : nameBuilder.toString();
}
/**
* Parses an X.500 Distinguished Name (DN)
*
* <p>
* The purpose of this method is to facilitate inspection of specific standard
* attributes used with principal system and user identifying certificates.
* Certificates that provide {@link X500Principal} instances <em>should</em> be
* verified as trusted using the delivered JCE provider prior to passing a raw
* DN to this method. Use {@link X500Principal#getName()} to validate the input
* value for this method if retrieved from a user-provided certificate.
* </p>
*
* <p>
* The response iterates Relative Distinguished Names mappings as defined for
* X.500 by <a href="https://datatracker.ietf.org/doc/html/rfc4514">RFC-4514
* LDAP</a>.
* </p>
*
* <p>
* This utility method only implements a simple parser of the ABNF grammar
* defined in
* <a href="https://datatracker.ietf.org/doc/html/rfc4514#section-4">RFC-4514
* Section 4</a>; it is not intended as part of larger implementation. Since JCE
* X500 functionality is implemented in non-exported packages of java.base,
* access to the security layer's parser is not allowed in a modular
* environment. The LDAP parser in javax.naming is not appropriate for this use.
* </p>
*
* @param name serialized X.500 DN
* @return parsed DN
* @see #getCommonName(X500Principal)
*/
public static Iterable<Map<String, String>> parse(String name) {
if (name.isEmpty())
return IuIterable.empty();
final Queue<Map<String, String>> parsedDN = new ArrayDeque<>();
Map<String, String> rdn = new LinkedHashMap<>();
parsedDN.offer(rdn);
var buf = CharBuffer.wrap(name);
final Supplier<Character> next = () -> buf.hasRemaining() ? buf.get() : '\0';
while (buf.hasRemaining()) {
// type = keystring / numericoid
final var startOfAttributeType = buf.position();
var c = next.get();
if (digit(c))
do { // numericoid = number 1*( DOT number )
if (c == '.') {
c = next.get();
if (!digit(c))
throw new IllegalArgumentException("expected DIGIT at " + buf.position());
}
// number = DIGIT / ( LDIGIT 1*DIGIT )
final var startOfNumber = buf.position();
final var ldigit = ldigit(c);
do {
if (buf.position() > startOfNumber && !ldigit)
throw new IllegalArgumentException("unexpected at " + buf.position());
c = next.get();
} while (digit(c));
} while (c == '.');
else if (alpha(c))
do // keystring = ALPHA *keychar
c = next.get();
while (keychar(c));
else
throw new IllegalArgumentException("expected ALPHA or DIGIT at " + buf.position());
// typedValue = type EQUALS value
final var attributeType = name.substring(startOfAttributeType, buf.position() - 1);
if (c != '=')
throw new IllegalArgumentException("expected EQUALS at " + buf.position());
// value = string / hexstring
final String value;
c = next.get();
// string = [ ( leadchar / pair )
// [ *( stringchar / pair ) ( trailchar / pair ) ] ]
if (leadchar(c) //
|| c == '\\') {
final var valueBuilder = new StringBuilder();
final var pendingBytes = new ByteArrayOutputStream();
final Consumer<Object> append = a -> {
if (pendingBytes.size() > 0) {
valueBuilder.append(IuText.utf8(pendingBytes.toByteArray()));
pendingBytes.reset();
}
if (a != null)
valueBuilder.append(a);
};
do {
if (c == '\\') {
// pair = ESC ( ESC / special / hexpair )
c = next.get();
if (c == '\\' || special(c)) {
// replace <ESC><ESC> with <ESC>;
// replace <ESC><special> with <special>;
append.accept(c);
c = next.get();
} else if (hexchar(c)) {
// hexpair = HEX HEX
var hexval = Integer.parseInt(Character.toString(c), 0x10) * 0x10;
c = next.get();
if (!hexchar(c))
throw new IllegalArgumentException("expected HEX at " + buf.position());
else
hexval += Integer.parseInt(Character.toString(c), 0x10);
pendingBytes.write(hexval);
c = next.get();
} else
throw new IllegalArgumentException("unexpected at " + buf.position());
} else {
final var l = c;
c = next.get();
if (!stringchar(c) //
&& c != '\\' //
&& !trailchar(l))
throw new IllegalArgumentException("unexpected SP at " + buf.position());
append.accept(l);
}
} while (stringchar(c) || c == '\\');
append.accept(null);
value = valueBuilder.toString();
} else if (c == '#') {
// hexstring = SHARP 1*hexpair
c = next.get();
if (!hexchar(c))
throw new IllegalArgumentException("expected HEX at " + buf.position());
ByteArrayOutputStream hexString = new ByteArrayOutputStream();
do {
var hexval = Integer.parseInt(Character.toString(c), 0x10) * 0x10;
c = next.get();
if (!hexchar(c))
throw new IllegalArgumentException("expected HEX at " + buf.position());
hexval += Integer.parseInt(Character.toString(c), 0x10);
hexString.write(hexval);
c = next.get();
} while (hexchar(c));
final var ber = hexString.toByteArray();
if (ber.length <= 2 //
|| ber[0] != 0x16 //
|| ber[1] <= 0)
value = "data:;base64," + IuText.base64(ber);
else
// IA5String: BER-encoded type 0x16 ASCII string
// used to represent CANONICAL DC attribute
value = IuText.ascii(Arrays.copyOfRange(ber, 2, ber[1] + 2));
} else
throw new IllegalArgumentException("expected <stringchar> or SHARP at " + buf.position());
rdn.put(attributeType, value);
if (c == ',') {
// dn = [ rdn *( COMMA rdn ) ]
rdn = new LinkedHashMap<>();
parsedDN.offer(rdn);
} else if (c != '+' //
&& (c != '\0' //
|| buf.hasRemaining()))
throw new IllegalArgumentException("expected PLUS or COMMA at " + buf.position());
// rdn = typedValue *( PLUS typedValue )
}
return parsedDN;
}
private static boolean alpha(char c) {
// ALPHA = %x41-5A / %x61-7A ; "A"-"Z" / "a"-"z"
// ALPHA = <any ASCII alphabetic character>
// ; (decimal 65-90 and 97-122)
return (c >= 'A' && c <= 'Z') || (c >= 'a' && c < 'z');
}
private static boolean digit(char c) {
// DIGIT = %x30 / LDIGIT ; "0"-"9"
// DIGIT = <any ASCII decimal digit>
// ; (decimal 48-57)
return c == '0' || ldigit(c);
}
private static boolean ldigit(char c) {
// LDIGIT = %x31-39 ; "1"-"9"
return c >= '1' && c <= '9';
}
private static boolean hexchar(char c) {
// HEX = DIGIT / %x41-46 / %x61-66 ; "0"-"9" / "A"-"F" / "a"-"f"
// hexchar = DIGIT / "A" / "B" / "C" / "D" / "E" / "F"
// / "a" / "b" / "c" / "d" / "e" / "f"
return digit(c) //
|| (c >= 'A' && c <= 'F') //
|| (c >= 'a' && c <= 'f');
}
private static boolean keychar(char c) {
// keychar = ALPHA / DIGIT / HYPHEN
return alpha(c) || digit(c) || c == '-';
}
private static boolean leadchar(char c) {
// leadchar = LUTF1 / UTFMB
// LUTF1 = %x01-1F / %x21 / %x24-2A / %x2D-3A /
// %x3D / %x3F-5B / %x5D-7F
return trailchar(c) && c != '#';
}
private static boolean trailchar(char c) {
// trailchar = TUTF1 / UTFMB
// TUTF1 = %x01-1F / %x21 / %x23-2A / %x2D-3A /
// %x3D / %x3F-5B / %x5D-7F
return stringchar(c) && c != ' ';
}
private static boolean stringchar(char c) {
// stringchar = SUTF1 / UTFMB
// SUTF1 = %x01-21 / %x23-2A / %x2D-3A /
// %x3D / %x3F-5B / %x5D-7F
return !escaped(c) && c != '\0' && c != '\\';
}
private static boolean escaped(char c) {
// escaped = DQUOTE / PLUS / COMMA / SEMI / LANGLE / RANGLE
return "\"+,;<>".indexOf(c) != -1;
}
private static boolean special(char c) {
// special = escaped / SPACE / SHARP / EQUALS
return escaped(c) //
|| " #=".indexOf(c) != -1;
}
private X500Utils() {
}
}