JNI: add String and X500Principal constructors to WolfSSLX509Name
parent
1bea4bec2d
commit
e013b63b20
|
|
@ -269,6 +269,38 @@
|
|||
<Field name="x509NamePtr"/>
|
||||
<Bug pattern="IS2_INCONSISTENT_SYNC"/>
|
||||
</Match>
|
||||
<Match>
|
||||
<Class name="com.wolfssl.WolfSSLX509Name"/>
|
||||
<Field name="active"/>
|
||||
<Bug pattern="IS2_INCONSISTENT_SYNC"/>
|
||||
</Match>
|
||||
<!--
|
||||
IS2_INCONSISTENT_SYNC: WolfSSLX509Name cached mirror fields.
|
||||
Written by updateMirrorField() during DN-based construction
|
||||
(before the object is published) and from synchronized
|
||||
setXxx() methods after construction. Reads via synchronized
|
||||
getXxx() methods. Safe under JMM publication semantics.
|
||||
-->
|
||||
<Match>
|
||||
<Class name="com.wolfssl.WolfSSLX509Name"/>
|
||||
<Or>
|
||||
<Field name="countryName"/>
|
||||
<Field name="stateOrProvinceName"/>
|
||||
<Field name="streetAddress"/>
|
||||
<Field name="localityName"/>
|
||||
<Field name="surname"/>
|
||||
<Field name="commonName"/>
|
||||
<Field name="emailAddress"/>
|
||||
<Field name="organizationName"/>
|
||||
<Field name="organizationalUnitName"/>
|
||||
<Field name="postalCode"/>
|
||||
<Field name="userId"/>
|
||||
<Field name="title"/>
|
||||
<Field name="domainComponent"/>
|
||||
<Field name="serialNumber"/>
|
||||
</Or>
|
||||
<Bug pattern="IS2_INCONSISTENT_SYNC"/>
|
||||
</Match>
|
||||
|
||||
<!--
|
||||
IS2_INCONSISTENT_SYNC: WolfSSLSession callback context
|
||||
|
|
|
|||
|
|
@ -20,7 +20,14 @@
|
|||
*/
|
||||
package com.wolfssl;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import javax.security.auth.x500.X500Principal;
|
||||
|
||||
/**
|
||||
* WolfSSLX509Name class, wraps native WOLFSSL_X509_NAME functionality.
|
||||
|
|
@ -49,6 +56,9 @@ public class WolfSSLX509Name {
|
|||
private String organizationalUnitName = null;
|
||||
private String postalCode = null;
|
||||
private String userId = null;
|
||||
private String title = null;
|
||||
private String domainComponent = null;
|
||||
private String serialNumber = null;
|
||||
|
||||
/* Encoding types, matched to native define values */
|
||||
private static final int MBSTRING_UTF8 = 0x100;
|
||||
|
|
@ -66,6 +76,119 @@ public class WolfSSLX509Name {
|
|||
*/
|
||||
public WolfSSLX509Name() throws WolfSSLException {
|
||||
|
||||
initNative();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new WolfSSLX509Name populated from a distinguished name string.
|
||||
*
|
||||
* Two string formats are accepted:
|
||||
* - RFC 2253 / RFC 4514: "CN=foo,O=bar,C=US"
|
||||
* - OpenSSL-style oneline: "/C=US/O=bar/CN=foo"
|
||||
*
|
||||
* The format is auto detected. A string with first non-whitespace
|
||||
* character '/' is parsed as OpenSSL-style oneline. Everything else is
|
||||
* parsed as RFC 2253 / RFC 4514. Both ',' and ';' are accepted as RDN
|
||||
* separators in RFC 2253 input. Backslash escapes (single character or
|
||||
* two-hex-digit byte form) and double-quoted values are supported in
|
||||
* RFC 2253. Hex-encoded values ("#hexdigits") are not supported and will
|
||||
* cause this constructor to throw a WolfSSLException.
|
||||
*
|
||||
* Callers with a javax.naming.ldap.LdapName instance can pass
|
||||
* myLdapName.toString() since LdapName's string form is RFC 2253.
|
||||
* javax.naming is not available on Android, which is why this class
|
||||
* does not provide a typed LdapName constructor.
|
||||
*
|
||||
* Relative Distinguished Names (RDNs) are added in the order used to build
|
||||
* the X.509 subject, not necessarily in the textual order of the input.
|
||||
* For RFC 2253 / RFC 4514 input, RDNs are added in reverse textual order
|
||||
* (least significant first). For example "CN=foo,O=bar,C=US" is added as
|
||||
* C, O, CN. OpenSSL-style oneline input is already written in that
|
||||
* insertion order.
|
||||
*
|
||||
* Multi value RDNs (ex: "CN=a+OU=b") are not currently supported and will
|
||||
* cause this constructor to throw a WolfSSLException (not supported in
|
||||
* native wolfSSL).
|
||||
*
|
||||
* @param dn distinguished name string in RFC 2253 or OpenSSL oneline format
|
||||
*
|
||||
* @throws WolfSSLException if dn is null, empty, cannot be parsed, contains
|
||||
* a multi value RDN, or contains an attribute type not recognized
|
||||
* by native wolfSSL.
|
||||
*/
|
||||
@SuppressWarnings("this-escape")
|
||||
public WolfSSLX509Name(String dn) throws WolfSSLException {
|
||||
|
||||
String trimmed = null;
|
||||
|
||||
if (dn == null) {
|
||||
throw new WolfSSLException("WolfSSLX509Name dn is null");
|
||||
}
|
||||
|
||||
trimmed = dn.trim();
|
||||
if (trimmed.isEmpty()) {
|
||||
throw new WolfSSLException("WolfSSLX509Name dn is empty");
|
||||
}
|
||||
|
||||
initNative();
|
||||
|
||||
try {
|
||||
if (trimmed.charAt(0) == '/') {
|
||||
populateFromOneline(trimmed);
|
||||
} else {
|
||||
populateFromRfc2253(trimmed);
|
||||
}
|
||||
|
||||
} catch (WolfSSLException | RuntimeException e) {
|
||||
free();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new WolfSSLX509Name populated from an X500Principal.
|
||||
*
|
||||
* Internally calls X500Principal.getName(RFC2253) and parses the string.
|
||||
* This could be used with X509Certificate.getSubjectX500Principal().
|
||||
*
|
||||
* Multi value Relative Distinguished Names (RDNs) are not currently
|
||||
* supported and will cause this constructor to throw a WolfSSLException.
|
||||
*
|
||||
* @param principal X500Principal to populate from
|
||||
*
|
||||
* @throws WolfSSLException if principal is null, contains a multi value
|
||||
* RDN, or contains an attribute type not recognized by wolfSSL.
|
||||
*/
|
||||
@SuppressWarnings("this-escape")
|
||||
public WolfSSLX509Name(X500Principal principal)
|
||||
throws WolfSSLException {
|
||||
|
||||
String dn = null;
|
||||
|
||||
if (principal == null) {
|
||||
throw new WolfSSLException("principal is null");
|
||||
}
|
||||
|
||||
dn = principal.getName(X500Principal.RFC2253);
|
||||
initNative();
|
||||
|
||||
try {
|
||||
populateFromRfc2253(dn);
|
||||
|
||||
} catch (WolfSSLException | RuntimeException e) {
|
||||
free();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Allocate the native WOLFSSL_X509_NAME and mark this object active.
|
||||
* Shared initialization for all constructors.
|
||||
*
|
||||
* @throws WolfSSLException if native API call fails.
|
||||
*/
|
||||
private void initNative() throws WolfSSLException {
|
||||
|
||||
x509NamePtr = X509_NAME_new();
|
||||
if (x509NamePtr == 0) {
|
||||
throw new WolfSSLException("Failed to create WolfSSLX509Name");
|
||||
|
|
@ -80,6 +203,577 @@ public class WolfSSLX509Name {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse an OpenSSL "oneline" DN string into a list of [type, value] pairs,
|
||||
* in encoded order (country first).
|
||||
*
|
||||
* The oneline format is "/Type1=Value1/Type2=Value2/..." with no defined
|
||||
* escaping for '/' or '=' inside values. Inputs containing backslash
|
||||
* escapes are rejected. Leading and trailing whitespace around both the
|
||||
* attribute type and value is trimmed.
|
||||
*
|
||||
* @param dn oneline DN string to parse
|
||||
*
|
||||
* @throws WolfSSLException on malformed input (empty RDN, missing '=',
|
||||
* empty attribute type, or backslash escape sequences)
|
||||
*/
|
||||
private static List<String[]> parseOnelineDn(String dn)
|
||||
throws WolfSSLException {
|
||||
|
||||
int eq;
|
||||
String type = null;
|
||||
String value = null;
|
||||
String[] parts = null;
|
||||
List<String[]> rdns = new ArrayList<String[]>();
|
||||
|
||||
if (dn.length() < 2 || dn.charAt(0) != '/') {
|
||||
throw new WolfSSLException(
|
||||
"Oneline DN must start with '/' followed by RDNs");
|
||||
}
|
||||
|
||||
if (dn.indexOf('\\') >= 0) {
|
||||
throw new WolfSSLException(
|
||||
"Backslash escapes in oneline DN are not supported, " +
|
||||
"use RFC 2253 form (e.g. \"CN=foo,O=bar\") or pass an " +
|
||||
"X500Principal instead");
|
||||
}
|
||||
|
||||
parts = dn.substring(1).split("/", -1);
|
||||
int partOffset = 1;
|
||||
for (String part : parts) {
|
||||
if (part.isEmpty()) {
|
||||
throw new WolfSSLException(
|
||||
"Invalid oneline DN: empty RDN at position " + partOffset);
|
||||
}
|
||||
eq = part.indexOf('=');
|
||||
if (eq < 0) {
|
||||
throw new WolfSSLException(
|
||||
"Invalid oneline DN: missing '=' at position " +
|
||||
partOffset + " (in segment \"" + part + "\")");
|
||||
}
|
||||
type = part.substring(0, eq).trim();
|
||||
value = part.substring(eq + 1).trim();
|
||||
if (type.isEmpty()) {
|
||||
throw new WolfSSLException(
|
||||
"Invalid oneline DN: empty attribute type at " +
|
||||
"position " + partOffset);
|
||||
}
|
||||
rdns.add(new String[] { type, value });
|
||||
partOffset += part.length() + 1;
|
||||
}
|
||||
|
||||
if (rdns.isEmpty()) {
|
||||
throw new WolfSSLException("Invalid oneline DN: no RDNs found");
|
||||
}
|
||||
|
||||
return rdns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Populate this name from a parsed oneline DN. The oneline format stores
|
||||
* RDNs in encoded order (country first), which matches our append-to-end
|
||||
* behavior in addEntryByTxt.
|
||||
*
|
||||
* @param dn oneline DN string to parse and populate from
|
||||
* @throws WolfSSLException if dn is malformed or contains an attribute
|
||||
* type not recognized by native wolfSSL.
|
||||
*/
|
||||
private void populateFromOneline(String dn)
|
||||
throws WolfSSLException {
|
||||
|
||||
for (String[] tv : parseOnelineDn(dn)) {
|
||||
addAttribute(tv[0], tv[1]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Populate this name from an RFC 2253 / RFC 4514 distinguished name
|
||||
* string. Parses the input, then iterates RDNs in reverse textual order
|
||||
* (least significant first), so "CN=foo,O=bar,C=US" is added as C, O, CN.
|
||||
*
|
||||
* @param dn RFC 2253 / RFC 4514 distinguished name string
|
||||
* @throws WolfSSLException if dn is malformed, contains a multi value RDN,
|
||||
* contains a hex-encoded value, or contains an attribute type not
|
||||
* recognized by native wolfSSL.
|
||||
*/
|
||||
private void populateFromRfc2253(String dn) throws WolfSSLException {
|
||||
|
||||
for (String[] tv : parseRfc2253Dn(dn)) {
|
||||
addAttribute(tv[0], tv[1]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse an RFC 2253 / RFC 4514 distinguished name string into a list of
|
||||
* [type, value] pairs, in encoded order (least significant RDN first,
|
||||
* matching conventional X.509 subject encoding order such as C, O, OU,
|
||||
* CN).
|
||||
*
|
||||
* Supported syntax:
|
||||
* - ',' and ';' as RDN separators
|
||||
* - Whitespace tolerated around '=' and RDN separators
|
||||
* - Attribute type as descr ([A-Za-z][A-Za-z0-9-]*) or numericoid
|
||||
* ([0-9]+(\.[0-9]+)*)
|
||||
* - Single-character escapes: \, \; \+ \" \\ \= \< \> \# \space
|
||||
* - Hex-byte escapes: \xx, accumulated and decoded as UTF-8 (so \C3\A9
|
||||
* produces 'e' acute)
|
||||
* - Double-quoted values (defined in RFC 2253, omitted from RFC 4514
|
||||
* but still accepted by major parsers including LdapName and
|
||||
* X500Principal)
|
||||
*
|
||||
* Rejected (throws WolfSSLException):
|
||||
* - '#hexpairs' values (BER-encoded form)
|
||||
* - '+' multi-valued RDNs
|
||||
* - Malformed input (missing '=', unterminated quote, trailing
|
||||
* separator, etc.)
|
||||
*
|
||||
* @param dn RFC 2253 / RFC 4514 distinguished name string
|
||||
* @return list of [type, value] pairs in least-significant-first order
|
||||
* @throws WolfSSLException on malformed or unsupported input
|
||||
*/
|
||||
private static List<String[]> parseRfc2253Dn(String dn)
|
||||
throws WolfSSLException {
|
||||
|
||||
List<String[]> rdns = new ArrayList<String[]>();
|
||||
int len = dn.length();
|
||||
int pos = 0;
|
||||
|
||||
while (true) {
|
||||
pos = skipDnWhitespace(dn, pos);
|
||||
if (pos >= len) {
|
||||
if (rdns.isEmpty()) {
|
||||
throw new WolfSSLException(
|
||||
"Invalid DN: no RDNs found at position " + pos);
|
||||
}
|
||||
throw new WolfSSLException(
|
||||
"Invalid DN: trailing RDN separator at position " + pos);
|
||||
}
|
||||
|
||||
/* Parse attribute type */
|
||||
int typeStart = pos;
|
||||
char c = dn.charAt(pos);
|
||||
if (c >= '0' && c <= '9') {
|
||||
while (pos < len) {
|
||||
char dc = dn.charAt(pos);
|
||||
if ((dc >= '0' && dc <= '9') || dc == '.') {
|
||||
pos++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')) {
|
||||
while (pos < len) {
|
||||
char tc = dn.charAt(pos);
|
||||
if ((tc >= 'A' && tc <= 'Z') || (tc >= 'a' && tc <= 'z') ||
|
||||
(tc >= '0' && tc <= '9') || tc == '-') {
|
||||
pos++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new WolfSSLException(
|
||||
"Invalid DN: unexpected character '" + c +
|
||||
"' at position " + pos);
|
||||
}
|
||||
String type = dn.substring(typeStart, pos);
|
||||
|
||||
/* Skip whitespace, expect '=' */
|
||||
pos = skipDnWhitespace(dn, pos);
|
||||
if (pos >= len || dn.charAt(pos) != '=') {
|
||||
throw new WolfSSLException(
|
||||
"Invalid DN: missing '=' after type '" + type + "'");
|
||||
}
|
||||
pos++;
|
||||
pos = skipDnWhitespace(dn, pos);
|
||||
|
||||
/* Parse value */
|
||||
int[] posRef = new int[] { pos };
|
||||
String value = parseRfc2253Value(dn, posRef);
|
||||
pos = posRef[0];
|
||||
|
||||
rdns.add(new String[] { type, value });
|
||||
|
||||
/* Separator */
|
||||
pos = skipDnWhitespace(dn, pos);
|
||||
if (pos >= len) {
|
||||
break;
|
||||
}
|
||||
char sep = dn.charAt(pos);
|
||||
if (sep == '+') {
|
||||
throw new WolfSSLException(
|
||||
"Multi-valued RDNs are not supported");
|
||||
}
|
||||
if (sep != ',' && sep != ';') {
|
||||
throw new WolfSSLException(
|
||||
"Invalid DN: expected ',' or ';' at position " + pos);
|
||||
}
|
||||
pos++;
|
||||
}
|
||||
|
||||
if (rdns.isEmpty()) {
|
||||
throw new WolfSSLException(
|
||||
"Invalid DN: no RDNs found at position " + pos);
|
||||
}
|
||||
|
||||
/* Reverse to insertion order matching the conventional X.509
|
||||
* subject encoding order (least significant first, e.g. C, O, OU,
|
||||
* CN for input "CN=foo,O=bar,OU=baz,C=US"). */
|
||||
Collections.reverse(rdns);
|
||||
|
||||
return rdns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a single RFC 2253 attribute value starting at posRef[0]. Updates
|
||||
* posRef[0] to the index after the value (before any separator).
|
||||
*
|
||||
* Handles quoted and unquoted forms, single-char and hex-byte escapes.
|
||||
* Builds the value as a byte stream so multi-byte UTF-8 escapes (e.g.
|
||||
* \C3\A9) decode correctly.
|
||||
*/
|
||||
private static String parseRfc2253Value(String dn, int[] posRef)
|
||||
throws WolfSSLException {
|
||||
|
||||
int pos = posRef[0];
|
||||
int len = dn.length();
|
||||
|
||||
/* Empty value at end of input ("CN=") is allowed for parity with the
|
||||
* explicit setX("") path and the mid-DN empty case ("CN=,O=foo").
|
||||
* Native may or may not produce useful output for an empty value, but
|
||||
* the wrapper accepts it consistently across all entry points. */
|
||||
if (pos >= len) {
|
||||
posRef[0] = pos;
|
||||
return "";
|
||||
}
|
||||
|
||||
char first = dn.charAt(pos);
|
||||
if (first == '#') {
|
||||
throw new WolfSSLException(
|
||||
"Hex-encoded RFC 2253 attribute values (#hexpairs form) " +
|
||||
"are not supported");
|
||||
}
|
||||
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
|
||||
/* Number of unescaped trailing whitespace bytes (' ' or '\t')
|
||||
* appended at the current end of `bytes`. Reset to 0 by any
|
||||
* non-whitespace byte and by any escape (so escaped trailing
|
||||
* whitespace per RFC 2253, e.g. "CN=foo\ ", is preserved).
|
||||
* Used to trim only unescaped trailing whitespace at the end. */
|
||||
int trimmableTail = 0;
|
||||
boolean quoted = (first == '"');
|
||||
int quoteStart = pos;
|
||||
if (quoted) {
|
||||
pos++;
|
||||
}
|
||||
|
||||
while (pos < len) {
|
||||
char ch = dn.charAt(pos);
|
||||
|
||||
if (quoted) {
|
||||
if (ch == '"') {
|
||||
pos++;
|
||||
posRef[0] = pos;
|
||||
return new String(bytes.toByteArray(),
|
||||
StandardCharsets.UTF_8);
|
||||
}
|
||||
} else {
|
||||
if (ch == ',' || ch == ';' || ch == '+') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (ch == '\\') {
|
||||
if (pos + 1 >= len) {
|
||||
throw new WolfSSLException(
|
||||
"Invalid DN: trailing '\\' escape at position " + pos);
|
||||
}
|
||||
char next = dn.charAt(pos + 1);
|
||||
if (isHex(next)) {
|
||||
if (pos + 2 >= len || !isHex(dn.charAt(pos + 2))) {
|
||||
throw new WolfSSLException(
|
||||
"Invalid DN: incomplete hex escape at position " +
|
||||
pos);
|
||||
}
|
||||
bytes.write((hexValue(next) << 4) |
|
||||
hexValue(dn.charAt(pos + 2)));
|
||||
pos += 3;
|
||||
} else {
|
||||
/* Single-character escape, encode as UTF-8 */
|
||||
appendCodePointUtf8(bytes, next);
|
||||
pos += 2;
|
||||
}
|
||||
/* Any escape (single-char or hex byte) is intentional content,
|
||||
* even if the resulting byte is a whitespace one. Reset so we
|
||||
* don't trim. */
|
||||
trimmableTail = 0;
|
||||
|
||||
} else {
|
||||
/* Plain code point, encode as UTF-8. Handle surrogate pairs by
|
||||
* reading the full code point. */
|
||||
int cp = dn.codePointAt(pos);
|
||||
appendCodePointUtf8(bytes, cp);
|
||||
pos += Character.charCount(cp);
|
||||
if (!quoted && (ch == ' ' || ch == '\t')) {
|
||||
trimmableTail++;
|
||||
} else {
|
||||
trimmableTail = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (quoted) {
|
||||
throw new WolfSSLException(
|
||||
"Invalid DN: unterminated quoted value starting at " +
|
||||
"position " + quoteStart);
|
||||
}
|
||||
|
||||
posRef[0] = pos;
|
||||
|
||||
/* Trim unescaped trailing whitespace. Escaped trailing whitespace
|
||||
* ("CN=foo\ " or "CN=foo\20") is preserved because trimmableTail was
|
||||
* reset to 0 at each escape. */
|
||||
byte[] all = bytes.toByteArray();
|
||||
return new String(all, 0, all.length - trimmableTail,
|
||||
StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private static int skipDnWhitespace(String dn, int pos) {
|
||||
|
||||
while (pos < dn.length() && (dn.charAt(pos) == ' ' ||
|
||||
dn.charAt(pos) == '\t')) {
|
||||
pos++;
|
||||
}
|
||||
return pos;
|
||||
}
|
||||
|
||||
private static boolean isHex(char c) {
|
||||
|
||||
return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') ||
|
||||
(c >= 'A' && c <= 'F');
|
||||
}
|
||||
|
||||
private static int hexValue(char c) {
|
||||
|
||||
if (c >= '0' && c <= '9') {
|
||||
return c - '0';
|
||||
}
|
||||
|
||||
if (c >= 'a' && c <= 'f') {
|
||||
return c - 'a' + 10;
|
||||
}
|
||||
|
||||
return c - 'A' + 10;
|
||||
}
|
||||
|
||||
private static void appendCodePointUtf8(ByteArrayOutputStream out, int cp) {
|
||||
|
||||
byte[] b = new String(Character.toChars(cp))
|
||||
.getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
out.write(b, 0, b.length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a single attribute to this name, normalizing the type before the
|
||||
* native call and updating the cached Java side mirror.
|
||||
*
|
||||
* Normalization is necessary because wolfSSL's OBJ_txt2nid() lookup
|
||||
* is case sensitive, while X500Principal.getName(RFC2253) commonly
|
||||
* returns uppercase forms (ex: "STREET", "EMAILADDRESS") that don't
|
||||
* match wolfSSL's canonical form.
|
||||
*
|
||||
* @param type attribute type string, for example "CN" or "commonName"
|
||||
* @param value attribute value string
|
||||
*
|
||||
* @throws WolfSSLException if native JNI error has occurred, or input
|
||||
* attribute type is not recognized by native wolfSSL.
|
||||
*/
|
||||
private void addAttribute(String type, String value)
|
||||
throws WolfSSLException {
|
||||
|
||||
confirmObjectIsActive();
|
||||
|
||||
String nativeType = canonicalAttributeName(type);
|
||||
addEntryByTxt(nativeType, value);
|
||||
updateMirrorField(nativeType, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Map a DN attribute type to the canonical form recognized by native
|
||||
* wolfSSL_OBJ_txt2nid(). Handles the common short/long aliases case
|
||||
* insensitively. Dotted OIDs are mapped to their canonical keyword for
|
||||
* well known X.500 attributes. Unrecognized OIDs and unrecognized keyword
|
||||
* types are passed through. For pass through types the downstream native
|
||||
* call will succeed if the casing matches, or fail cleanly via
|
||||
* addEntryByTxt() if not. Null and empty inputs are returned unchanged.
|
||||
* Callers downstream reject them via addEntryByTxt().
|
||||
*
|
||||
* @param attrType attribute type string, for example "CN", "commonName",
|
||||
* or "2.5.4.3"
|
||||
*
|
||||
* @return canonical attribute type string recognized by wolfSSL, for
|
||||
* example "commonName" for "CN" / "commonName" / "2.5.4.3".
|
||||
* Unrecognized inputs are passed through.
|
||||
*/
|
||||
private static String canonicalAttributeName(String attrType) {
|
||||
|
||||
String key = null;
|
||||
|
||||
if (attrType == null || attrType.isEmpty()) {
|
||||
return attrType;
|
||||
}
|
||||
|
||||
/* Dotted OIDs start with an ASCII digit. Translate well known X.500
|
||||
* attribute OIDs to their canonical keyword so equivalent DN forms
|
||||
* (ex: "CN=foo" vs "2.5.4.3=foo") produce identical state. Unknown
|
||||
* OIDs are passed through. wolfSSL_OBJ_txt2nid() handles dotted OID
|
||||
* lookup natively. */
|
||||
char first = attrType.charAt(0);
|
||||
if (first >= '0' && first <= '9') {
|
||||
switch (attrType) {
|
||||
case "2.5.4.3": return "commonName";
|
||||
case "2.5.4.4": return "surname";
|
||||
case "2.5.4.5": return "serialNumber";
|
||||
case "2.5.4.6": return "countryName";
|
||||
case "2.5.4.7": return "localityName";
|
||||
case "2.5.4.8": return "stateOrProvinceName";
|
||||
case "2.5.4.9": return "streetAddress";
|
||||
case "2.5.4.10": return "organizationName";
|
||||
case "2.5.4.11": return "organizationalUnitName";
|
||||
case "2.5.4.12": return "title";
|
||||
case "2.5.4.17": return "postalCode";
|
||||
case "0.9.2342.19200300.100.1.1": return "userId";
|
||||
case "0.9.2342.19200300.100.1.25": return "domainComponent";
|
||||
case "1.2.840.113549.1.9.1": return "emailAddress";
|
||||
default: return attrType;
|
||||
}
|
||||
}
|
||||
|
||||
key = attrType.toUpperCase(Locale.ROOT);
|
||||
switch (key) {
|
||||
case "C":
|
||||
case "COUNTRYNAME":
|
||||
return "countryName";
|
||||
case "ST":
|
||||
case "STATEORPROVINCENAME":
|
||||
return "stateOrProvinceName";
|
||||
case "STREET":
|
||||
case "STREETADDRESS":
|
||||
return "streetAddress";
|
||||
case "L":
|
||||
case "LOCALITYNAME":
|
||||
return "localityName";
|
||||
case "SN":
|
||||
case "SURNAME":
|
||||
return "surname";
|
||||
case "CN":
|
||||
case "COMMONNAME":
|
||||
return "commonName";
|
||||
case "EMAILADDRESS":
|
||||
return "emailAddress";
|
||||
case "O":
|
||||
case "ORGANIZATIONNAME":
|
||||
return "organizationName";
|
||||
case "OU":
|
||||
case "ORGANIZATIONALUNITNAME":
|
||||
return "organizationalUnitName";
|
||||
case "POSTALCODE":
|
||||
return "postalCode";
|
||||
case "UID":
|
||||
case "USERID":
|
||||
return "userId";
|
||||
case "T":
|
||||
case "TITLE":
|
||||
return "title";
|
||||
case "DC":
|
||||
case "DOMAINCOMPONENT":
|
||||
return "domainComponent";
|
||||
case "SERIALNUMBER":
|
||||
return "serialNumber";
|
||||
default:
|
||||
return attrType;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the cached Java-side mirror field if attrType matches one of the
|
||||
* known short/long names. Unknown attribute types are silently skipped
|
||||
* since they were already pushed through the native call. Comparison is
|
||||
* case-insensitive.
|
||||
*
|
||||
* @param attrType attribute type string
|
||||
* @param value attribute value string
|
||||
*/
|
||||
private void updateMirrorField(String attrType, String value) {
|
||||
|
||||
String key = null;
|
||||
|
||||
if (attrType == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
key = attrType.toUpperCase(Locale.ROOT);
|
||||
switch (key) {
|
||||
case "C":
|
||||
case "COUNTRYNAME":
|
||||
this.countryName = value;
|
||||
break;
|
||||
case "ST":
|
||||
case "STATEORPROVINCENAME":
|
||||
this.stateOrProvinceName = value;
|
||||
break;
|
||||
case "STREET":
|
||||
case "STREETADDRESS":
|
||||
this.streetAddress = value;
|
||||
break;
|
||||
case "L":
|
||||
case "LOCALITYNAME":
|
||||
this.localityName = value;
|
||||
break;
|
||||
case "SN":
|
||||
case "SURNAME":
|
||||
this.surname = value;
|
||||
break;
|
||||
case "CN":
|
||||
case "COMMONNAME":
|
||||
this.commonName = value;
|
||||
break;
|
||||
case "EMAILADDRESS":
|
||||
this.emailAddress = value;
|
||||
break;
|
||||
case "O":
|
||||
case "ORGANIZATIONNAME":
|
||||
this.organizationName = value;
|
||||
break;
|
||||
case "OU":
|
||||
case "ORGANIZATIONALUNITNAME":
|
||||
this.organizationalUnitName = value;
|
||||
break;
|
||||
case "POSTALCODE":
|
||||
this.postalCode = value;
|
||||
break;
|
||||
case "UID":
|
||||
case "USERID":
|
||||
this.userId = value;
|
||||
break;
|
||||
case "T":
|
||||
case "TITLE":
|
||||
this.title = value;
|
||||
break;
|
||||
case "DC":
|
||||
case "DOMAINCOMPONENT":
|
||||
this.domainComponent = value;
|
||||
break;
|
||||
case "SERIALNUMBER":
|
||||
this.serialNumber = value;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies that the current WolfSSLX509Name object is active.
|
||||
*
|
||||
|
|
@ -133,6 +827,10 @@ public class WolfSSLX509Name {
|
|||
throw new WolfSSLException(
|
||||
"field or entry is null in addEntryByTxt()");
|
||||
}
|
||||
if (field.isEmpty()) {
|
||||
throw new WolfSSLException(
|
||||
"field is empty in addEntryByTxt()");
|
||||
}
|
||||
|
||||
synchronized (x509NameLock) {
|
||||
entryBytes = entry.getBytes(StandardCharsets.UTF_8);
|
||||
|
|
@ -389,6 +1087,72 @@ public class WolfSSLX509Name {
|
|||
this.userId = id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set title for this name object.
|
||||
*
|
||||
* @param name String containing title to be set
|
||||
*
|
||||
* @throws IllegalStateException if WolfSSLX509Name has been freed.
|
||||
* @throws WolfSSLException if native JNI error has occurred, or input
|
||||
* argument is invalid.
|
||||
*/
|
||||
public synchronized void setTitle(String name)
|
||||
throws IllegalStateException, WolfSSLException {
|
||||
|
||||
confirmObjectIsActive();
|
||||
|
||||
WolfSSLDebug.log(getClass(), WolfSSLDebug.Component.JNI,
|
||||
WolfSSLDebug.INFO, this.x509NamePtr,
|
||||
() -> "entered setTitle(" + name + ")");
|
||||
|
||||
addEntryByTxt("title", name);
|
||||
this.title = name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set domain component for this name object.
|
||||
*
|
||||
* @param dc String containing domain component to be set
|
||||
*
|
||||
* @throws IllegalStateException if WolfSSLX509Name has been freed.
|
||||
* @throws WolfSSLException if native JNI error has occurred, or input
|
||||
* argument is invalid.
|
||||
*/
|
||||
public synchronized void setDomainComponent(String dc)
|
||||
throws IllegalStateException, WolfSSLException {
|
||||
|
||||
confirmObjectIsActive();
|
||||
|
||||
WolfSSLDebug.log(getClass(), WolfSSLDebug.Component.JNI,
|
||||
WolfSSLDebug.INFO, this.x509NamePtr,
|
||||
() -> "entered setDomainComponent(" + dc + ")");
|
||||
|
||||
addEntryByTxt("domainComponent", dc);
|
||||
this.domainComponent = dc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set serial number for this name object.
|
||||
*
|
||||
* @param sn String containing serial number to be set
|
||||
*
|
||||
* @throws IllegalStateException if WolfSSLX509Name has been freed.
|
||||
* @throws WolfSSLException if native JNI error has occurred, or input
|
||||
* argument is invalid.
|
||||
*/
|
||||
public synchronized void setSerialNumber(String sn)
|
||||
throws IllegalStateException, WolfSSLException {
|
||||
|
||||
confirmObjectIsActive();
|
||||
|
||||
WolfSSLDebug.log(getClass(), WolfSSLDebug.Component.JNI,
|
||||
WolfSSLDebug.INFO, this.x509NamePtr,
|
||||
() -> "entered setSerialNumber(" + sn + ")");
|
||||
|
||||
addEntryByTxt("serialNumber", sn);
|
||||
this.serialNumber = sn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get country name set in this object.
|
||||
*
|
||||
|
|
@ -587,6 +1351,59 @@ public class WolfSSLX509Name {
|
|||
return this.userId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get title set in this object.
|
||||
*
|
||||
* @return title string, or null if not yet set
|
||||
*
|
||||
* @throws IllegalStateException if WolfSSLX509Name has been freed.
|
||||
*/
|
||||
public synchronized String getTitle() {
|
||||
|
||||
confirmObjectIsActive();
|
||||
|
||||
WolfSSLDebug.log(getClass(), WolfSSLDebug.Component.JNI,
|
||||
WolfSSLDebug.INFO, this.x509NamePtr, () -> "entered getTitle()");
|
||||
|
||||
return this.title;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get domain component set in this object.
|
||||
*
|
||||
* @return domain component string, or null if not yet set
|
||||
*
|
||||
* @throws IllegalStateException if WolfSSLX509Name has been freed.
|
||||
*/
|
||||
public synchronized String getDomainComponent() {
|
||||
|
||||
confirmObjectIsActive();
|
||||
|
||||
WolfSSLDebug.log(getClass(), WolfSSLDebug.Component.JNI,
|
||||
WolfSSLDebug.INFO, this.x509NamePtr,
|
||||
() -> "entered getDomainComponent()");
|
||||
|
||||
return this.domainComponent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get serial number set in this object.
|
||||
*
|
||||
* @return serial number string, or null if not yet set
|
||||
*
|
||||
* @throws IllegalStateException if WolfSSLX509Name has been freed.
|
||||
*/
|
||||
public synchronized String getSerialNumber() {
|
||||
|
||||
confirmObjectIsActive();
|
||||
|
||||
WolfSSLDebug.log(getClass(), WolfSSLDebug.Component.JNI,
|
||||
WolfSSLDebug.INFO, this.x509NamePtr,
|
||||
() -> "entered getSerialNumber()");
|
||||
|
||||
return this.serialNumber;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ import com.wolfssl.WolfSSLException;
|
|||
WolfCryptECCTest.class,
|
||||
WolfSSLCertificateTest.class,
|
||||
WolfSSLCertRequestTest.class,
|
||||
WolfSSLX509NameTest.class,
|
||||
WolfSSLCertManagerTest.class,
|
||||
WolfSSLNameConstraintsTest.class,
|
||||
WolfSSLCRLTest.class
|
||||
|
|
|
|||
|
|
@ -0,0 +1,848 @@
|
|||
/* WolfSSLX509NameTest.java
|
||||
*
|
||||
* Copyright (C) 2006-2026 wolfSSL Inc.
|
||||
*
|
||||
* This file is part of wolfSSL.
|
||||
*
|
||||
* wolfSSL is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* wolfSSL is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA
|
||||
*/
|
||||
|
||||
package com.wolfssl.test;
|
||||
|
||||
import org.junit.Assume;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TestRule;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
import javax.security.auth.x500.X500Principal;
|
||||
|
||||
import com.wolfssl.WolfSSL;
|
||||
import com.wolfssl.WolfSSLException;
|
||||
import com.wolfssl.WolfSSLX509Name;
|
||||
|
||||
public class WolfSSLX509NameTest {
|
||||
|
||||
/* True if native wolfSSL recognizes the "title" attribute (added in
|
||||
* v5.8.2). When false, tests that exercise title / domainComponent /
|
||||
* serialNumber are skipped via Assume. */
|
||||
private static boolean extendedAttrsSupported = false;
|
||||
|
||||
@Rule
|
||||
public TestRule testWatcher = TimedTestWatcher.create();
|
||||
|
||||
@BeforeClass
|
||||
public static void loadLibrary() {
|
||||
|
||||
System.out.println("WolfSSLX509Name Class");
|
||||
|
||||
try {
|
||||
WolfSSL.loadLibrary();
|
||||
} catch (UnsatisfiedLinkError ule) {
|
||||
fail("failed to load native JNI library");
|
||||
}
|
||||
|
||||
/* Check if wolfSSL knows the extended attributes added in v5.8.2
|
||||
* (title, domainComponent, serialNumber). The check uses setTitle
|
||||
* since title was the first added in that set. */
|
||||
try {
|
||||
WolfSSLX509Name probe = new WolfSSLX509Name();
|
||||
try {
|
||||
probe.setTitle("probe");
|
||||
extendedAttrsSupported = true;
|
||||
} finally {
|
||||
probe.free();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
extendedAttrsSupported = false;
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_String_RFC2253_AllKnownAttributes()
|
||||
throws WolfSSLException {
|
||||
|
||||
/* Exercises every cached mirror field that wolfSSL has
|
||||
* supported since before v5.8.2. The extended attributes (title,
|
||||
* domainComponent, serialNumber) are covered separately by
|
||||
* test_String_RFC2253_ExtendedAttributes since they require
|
||||
* wolfSSL >= 5.8.2. */
|
||||
String dn = "UID=tester,CN=wolfssl.com," +
|
||||
"EMAILADDRESS=support@wolfssl.com,SN=Smith," +
|
||||
"OU=Engineering,O=wolfSSL Inc.,POSTALCODE=59715," +
|
||||
"STREET=12345 Test St,L=Bozeman,ST=Montana,C=US";
|
||||
|
||||
WolfSSLX509Name name = new WolfSSLX509Name(dn);
|
||||
try {
|
||||
assertEquals("US", name.getCountryName());
|
||||
assertEquals("Montana", name.getStateOrProvinceName());
|
||||
assertEquals("Bozeman", name.getLocalityName());
|
||||
assertEquals("12345 Test St", name.getStreetAddress());
|
||||
assertEquals("59715", name.getPostalCode());
|
||||
assertEquals("wolfSSL Inc.", name.getOrganizationName());
|
||||
assertEquals("Engineering", name.getOrganizationalUnitName());
|
||||
assertEquals("Smith", name.getSurname());
|
||||
assertEquals("support@wolfssl.com", name.getEmailAddress());
|
||||
assertEquals("wolfssl.com", name.getCommonName());
|
||||
assertEquals("tester", name.getUserId());
|
||||
} finally {
|
||||
name.free();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_String_RFC2253_ExtendedAttributes()
|
||||
throws WolfSSLException {
|
||||
|
||||
/* title, domainComponent, and serialNumber were added to wolfSSL
|
||||
* OBJ table in v5.8.2. Skip on older builds. The input also tests
|
||||
* case-sensitive aliases that wolfSSL natively doesn't recognize
|
||||
* without canonical mapping (lowercase "dc", short forms "T" /
|
||||
* "SERIALNUMBER". */
|
||||
Assume.assumeTrue(
|
||||
"wolfSSL does not recognize 'title' (require >= 5.8.2)",
|
||||
extendedAttrsSupported);
|
||||
|
||||
WolfSSLX509Name name = new WolfSSLX509Name(
|
||||
"T=CTO,dc=example,SERIALNUMBER=42,CN=foo");
|
||||
try {
|
||||
assertEquals("CTO", name.getTitle());
|
||||
assertEquals("example", name.getDomainComponent());
|
||||
assertEquals("42", name.getSerialNumber());
|
||||
assertEquals("foo", name.getCommonName());
|
||||
} finally {
|
||||
name.free();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_String_RFC2253_SerialNumberOid()
|
||||
throws WolfSSLException {
|
||||
|
||||
/* 2.5.4.5 is the standard X.500 OID for serialNumber. The
|
||||
* canonicalAttributeName() OID switch must map it to "serialNumber"
|
||||
* so the mirror populates the same as if the user had passed
|
||||
* "SERIALNUMBER=foo". Gated like the other extended-attribute tests
|
||||
* since wolfSSL added serialNumber to its OBJ table in v5.8.2. */
|
||||
Assume.assumeTrue(
|
||||
"wolfSSL does not recognize 'serialNumber' (require >= 5.8.2)",
|
||||
extendedAttrsSupported);
|
||||
|
||||
WolfSSLX509Name name =
|
||||
new WolfSSLX509Name("2.5.4.5=ABC123,CN=foo");
|
||||
try {
|
||||
assertEquals("ABC123", name.getSerialNumber());
|
||||
assertEquals("foo", name.getCommonName());
|
||||
} finally {
|
||||
name.free();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_String_RFC2253_LongAttributeNames()
|
||||
throws WolfSSLException {
|
||||
|
||||
String dn = "commonName=wolfssl.com," +
|
||||
"organizationName=wolfSSL,countryName=US";
|
||||
|
||||
WolfSSLX509Name name = new WolfSSLX509Name(dn);
|
||||
try {
|
||||
assertEquals("US", name.getCountryName());
|
||||
assertEquals("wolfSSL", name.getOrganizationName());
|
||||
assertEquals("wolfssl.com", name.getCommonName());
|
||||
} finally {
|
||||
name.free();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_String_RFC2253_SemicolonSeparator()
|
||||
throws WolfSSLException {
|
||||
|
||||
/* RFC 2253 also accepts ';' as an RDN separator. */
|
||||
String dn = "CN=wolfssl.com;O=wolfSSL Inc.;C=US";
|
||||
|
||||
WolfSSLX509Name name = new WolfSSLX509Name(dn);
|
||||
try {
|
||||
assertEquals("US", name.getCountryName());
|
||||
assertEquals("wolfSSL Inc.", name.getOrganizationName());
|
||||
assertEquals("wolfssl.com", name.getCommonName());
|
||||
} finally {
|
||||
name.free();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_String_RFC2253_OidAttributeTypes()
|
||||
throws WolfSSLException {
|
||||
|
||||
/* 2.5.4.3 = commonName,
|
||||
* 2.5.4.10 = organizationName,
|
||||
* 2.5.4.6 = countryName. */
|
||||
String dn = "2.5.4.3=wolfssl.com,2.5.4.10=wolfSSL,2.5.4.6=US";
|
||||
|
||||
WolfSSLX509Name name = new WolfSSLX509Name(dn);
|
||||
try {
|
||||
/* Well known X.500 OIDs are translated to their canonical
|
||||
* keyword, so mirror fields populate the same as if the user
|
||||
* had passed "CN=...,O=...,C=...". */
|
||||
assertEquals("US", name.getCountryName());
|
||||
assertEquals("wolfSSL", name.getOrganizationName());
|
||||
assertEquals("wolfssl.com", name.getCommonName());
|
||||
} finally {
|
||||
name.free();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_String_RFC2253_UnknownOidPassesThrough() {
|
||||
|
||||
/* An OID not in our well known table. wolfSSL doesn't recognize
|
||||
* "1.2.3.4.5.6.7" so the native call rejects it. This documents
|
||||
* that unknown OIDs reach native and aren't silently swallowed. */
|
||||
try {
|
||||
new WolfSSLX509Name("1.2.3.4.5.6.7=foo,CN=bar");
|
||||
fail("expected WolfSSLException for unknown OID");
|
||||
} catch (WolfSSLException e) {
|
||||
/* expected */
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_String_RFC2253_QuotedValueWithComma()
|
||||
throws WolfSSLException {
|
||||
|
||||
/* RFC 2253 allows quoting a value that contains a comma. */
|
||||
String dn = "CN=\"Foo, Inc.\",O=Bar,C=US";
|
||||
|
||||
WolfSSLX509Name name = new WolfSSLX509Name(dn);
|
||||
try {
|
||||
assertEquals("Foo, Inc.", name.getCommonName());
|
||||
assertEquals("Bar", name.getOrganizationName());
|
||||
assertEquals("US", name.getCountryName());
|
||||
} finally {
|
||||
name.free();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_String_RFC2253_EscapedComma()
|
||||
throws WolfSSLException {
|
||||
|
||||
/* Backslash-escaped comma in value. */
|
||||
String dn = "CN=Foo\\, Inc.,O=Bar,C=US";
|
||||
|
||||
WolfSSLX509Name name = new WolfSSLX509Name(dn);
|
||||
try {
|
||||
assertEquals("Foo, Inc.", name.getCommonName());
|
||||
} finally {
|
||||
name.free();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_String_RFC2253_LeadingTrailingWhitespace()
|
||||
throws WolfSSLException {
|
||||
|
||||
WolfSSLX509Name name = new WolfSSLX509Name(" CN=wolfssl.com,C=US ");
|
||||
try {
|
||||
assertEquals("US", name.getCountryName());
|
||||
assertEquals("wolfssl.com", name.getCommonName());
|
||||
} finally {
|
||||
name.free();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_String_RFC2253_SingleComponent()
|
||||
throws WolfSSLException {
|
||||
|
||||
WolfSSLX509Name name = new WolfSSLX509Name("CN=foo");
|
||||
try {
|
||||
assertEquals("foo", name.getCommonName());
|
||||
assertNull(name.getCountryName());
|
||||
} finally {
|
||||
name.free();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_String_Oneline_Basic() throws WolfSSLException {
|
||||
|
||||
WolfSSLX509Name name = new WolfSSLX509Name(
|
||||
"/C=US/ST=Montana/L=Bozeman/O=wolfSSL Inc." +
|
||||
"/OU=Engineering/CN=wolfssl.com");
|
||||
try {
|
||||
assertEquals("US", name.getCountryName());
|
||||
assertEquals("Montana", name.getStateOrProvinceName());
|
||||
assertEquals("Bozeman", name.getLocalityName());
|
||||
assertEquals("wolfSSL Inc.", name.getOrganizationName());
|
||||
assertEquals("Engineering", name.getOrganizationalUnitName());
|
||||
assertEquals("wolfssl.com", name.getCommonName());
|
||||
} finally {
|
||||
name.free();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_String_Oneline_SingleComponent()
|
||||
throws WolfSSLException {
|
||||
|
||||
WolfSSLX509Name name = new WolfSSLX509Name("/CN=foo");
|
||||
try {
|
||||
assertEquals("foo", name.getCommonName());
|
||||
} finally {
|
||||
name.free();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_String_Oneline_LongAttributeNames()
|
||||
throws WolfSSLException {
|
||||
|
||||
WolfSSLX509Name name =
|
||||
new WolfSSLX509Name("/countryName=US/commonName=foo");
|
||||
try {
|
||||
assertEquals("US", name.getCountryName());
|
||||
assertEquals("foo", name.getCommonName());
|
||||
} finally {
|
||||
name.free();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_String_Oneline_LeadingTrailingWhitespace()
|
||||
throws WolfSSLException {
|
||||
|
||||
WolfSSLX509Name name = new WolfSSLX509Name(" /C=US/CN=foo ");
|
||||
try {
|
||||
assertEquals("US", name.getCountryName());
|
||||
assertEquals("foo", name.getCommonName());
|
||||
} finally {
|
||||
name.free();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_String_Oneline_BareSlashThrows() {
|
||||
|
||||
try {
|
||||
new WolfSSLX509Name("/");
|
||||
fail("expected WolfSSLException for bare '/'");
|
||||
} catch (WolfSSLException e) {
|
||||
/* expected */
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_String_Oneline_MissingEqualsThrows() {
|
||||
|
||||
try {
|
||||
new WolfSSLX509Name("/CN=foo/bar/O=baz");
|
||||
fail("expected WolfSSLException for RDN missing '='");
|
||||
} catch (WolfSSLException e) {
|
||||
/* expected */
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_String_Oneline_EmptyTypeThrows() {
|
||||
|
||||
try {
|
||||
new WolfSSLX509Name("/=foo/CN=bar");
|
||||
fail("expected WolfSSLException for empty attribute type");
|
||||
} catch (WolfSSLException e) {
|
||||
/* expected */
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_String_Oneline_DoubleSlashThrows() {
|
||||
|
||||
/* "//CN=foo" (empty first RDN) */
|
||||
try {
|
||||
new WolfSSLX509Name("//CN=foo");
|
||||
fail("expected WolfSSLException for empty RDN");
|
||||
} catch (WolfSSLException e) {
|
||||
/* expected */
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_String_Oneline_TrailingSlashThrows() {
|
||||
|
||||
/* split with limit -1 keeps trailing empty (empty RDN) */
|
||||
try {
|
||||
new WolfSSLX509Name("/CN=foo/");
|
||||
fail("expected WolfSSLException for trailing '/'");
|
||||
} catch (WolfSSLException e) {
|
||||
/* expected */
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_String_Oneline_BackslashEscapeThrows() {
|
||||
|
||||
/* OpenSSL's oneline can emit "\/" for slashes in values. We
|
||||
* reject rather than mis-parse, and the message points users
|
||||
* to RFC 2253 / X500Principal alternatives. */
|
||||
try {
|
||||
new WolfSSLX509Name("/O=ACME\\/West/CN=foo");
|
||||
fail("expected WolfSSLException for backslash escape");
|
||||
} catch (WolfSSLException e) {
|
||||
/* expected */
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_String_Oneline_UnknownAttributeThrows() {
|
||||
|
||||
try {
|
||||
new WolfSSLX509Name("/FOO=bar/CN=baz");
|
||||
fail("expected WolfSSLException for unknown attribute");
|
||||
} catch (WolfSSLException e) {
|
||||
/* expected */
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_String_NullThrows() {
|
||||
|
||||
try {
|
||||
new WolfSSLX509Name((String) null);
|
||||
fail("expected WolfSSLException for null String DN");
|
||||
} catch (WolfSSLException e) {
|
||||
/* expected */
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_String_EmptyThrows() {
|
||||
|
||||
try {
|
||||
new WolfSSLX509Name("");
|
||||
fail("expected WolfSSLException for empty String DN");
|
||||
} catch (WolfSSLException e) {
|
||||
/* expected */
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_String_WhitespaceOnlyThrows() {
|
||||
|
||||
try {
|
||||
new WolfSSLX509Name(" ");
|
||||
fail("expected WolfSSLException for whitespace-only DN");
|
||||
} catch (WolfSSLException e) {
|
||||
/* expected */
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_String_InvalidSyntaxThrows() {
|
||||
|
||||
try {
|
||||
new WolfSSLX509Name("not a valid dn");
|
||||
fail("expected WolfSSLException for invalid DN syntax");
|
||||
} catch (WolfSSLException e) {
|
||||
/* expected */
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_String_MultiValuedRdnThrows() {
|
||||
|
||||
try {
|
||||
new WolfSSLX509Name("CN=foo+OU=bar,O=baz,C=US");
|
||||
fail("expected WolfSSLException for multi-valued RDN");
|
||||
} catch (WolfSSLException e) {
|
||||
/* expected */
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_String_UnknownAttributeThrows() {
|
||||
|
||||
/* "FOO" is not a recognized short/long name and is not a dotted
|
||||
* OID, so wolfSSL_OBJ_txt2nid() returns WC_NID_undef and the native
|
||||
* add_entry_by_txt() call fails. */
|
||||
try {
|
||||
new WolfSSLX509Name("FOO=bar,CN=baz");
|
||||
fail("expected WolfSSLException for unknown attribute");
|
||||
} catch (WolfSSLException e) {
|
||||
/* expected */
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_String_RFC2253_HexEncodedValueThrows() {
|
||||
|
||||
/* RFC 2253 #hexpairs form are rejected: bytes are BER-encoded
|
||||
* (tag + length + value), not UTF-8 text. "#1303616263" =
|
||||
* PrintableString "abc" in BER. */
|
||||
try {
|
||||
new WolfSSLX509Name("CN=#1303616263,O=foo,C=US");
|
||||
fail("expected WolfSSLException for hex-encoded BER value");
|
||||
} catch (WolfSSLException e) {
|
||||
/* expected */
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_String_RFC2253_HexByteEscape()
|
||||
throws WolfSSLException {
|
||||
|
||||
/* "\C3\A9" = UTF-8 encoding of 'e' acute (U+00E9). The parser
|
||||
* accumulates hex-byte escapes as a byte sequence and decodes the
|
||||
* whole value as UTF-8, so multi-byte sequences round-trip.
|
||||
*
|
||||
* Use a unicode escape for the literal rather than the raw 'e'
|
||||
* acute character, so this test compiles correctly on platforms
|
||||
* where javac defaults to a non-UTF-8 source encoding (e.g. on
|
||||
* Windows where javac defaults to the platform charset). */
|
||||
WolfSSLX509Name name = new WolfSSLX509Name("CN=Caf\\C3\\A9,C=US");
|
||||
try {
|
||||
assertEquals("Caf\u00e9", name.getCommonName());
|
||||
assertEquals("US", name.getCountryName());
|
||||
} finally {
|
||||
name.free();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_String_RFC2253_BackslashLiteral()
|
||||
throws WolfSSLException {
|
||||
|
||||
WolfSSLX509Name name = new WolfSSLX509Name("CN=foo\\\\bar,C=US");
|
||||
try {
|
||||
assertEquals("foo\\bar", name.getCommonName());
|
||||
} finally {
|
||||
name.free();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_String_RFC2253_EscapedTrailingSpacePreserved()
|
||||
throws WolfSSLException {
|
||||
|
||||
/* RFC 2253 requires trailing spaces in unquoted values to be escaped
|
||||
* (single-char "\ " or hex "\20"). The parser must preserve the
|
||||
* escaped space and only trim unescaped trailing whitespace. */
|
||||
WolfSSLX509Name viaCharEscape = new WolfSSLX509Name("CN=foo\\ ,O=bar");
|
||||
try {
|
||||
assertEquals("foo ", viaCharEscape.getCommonName());
|
||||
} finally {
|
||||
viaCharEscape.free();
|
||||
}
|
||||
|
||||
WolfSSLX509Name viaHexEscape = new WolfSSLX509Name("CN=foo\\20,O=bar");
|
||||
try {
|
||||
assertEquals("foo ", viaHexEscape.getCommonName());
|
||||
} finally {
|
||||
viaHexEscape.free();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_String_RFC2253_MixedEscapedAndUnescapedTrailingSpace()
|
||||
throws WolfSSLException {
|
||||
|
||||
/* "foo\ " = escaped space then unescaped space. The escaped space
|
||||
* is preserved, the unescaped one is trimmed. */
|
||||
WolfSSLX509Name name = new WolfSSLX509Name("CN=foo\\ ,O=bar");
|
||||
try {
|
||||
assertEquals("foo ", name.getCommonName());
|
||||
} finally {
|
||||
name.free();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_String_RFC2253_UnescapedTrailingWhitespaceTrimmed()
|
||||
throws WolfSSLException {
|
||||
|
||||
/* Plain trailing whitespace in an unquoted value is trimmed. */
|
||||
WolfSSLX509Name name = new WolfSSLX509Name("CN=foo ,O=bar");
|
||||
try {
|
||||
assertEquals("foo", name.getCommonName());
|
||||
assertEquals("bar", name.getOrganizationName());
|
||||
} finally {
|
||||
name.free();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_String_RFC2253_QuotedWithEmbeddedQuote()
|
||||
throws WolfSSLException {
|
||||
|
||||
WolfSSLX509Name name =
|
||||
new WolfSSLX509Name("CN=\"he said \\\"hi\\\"\",C=US");
|
||||
try {
|
||||
assertEquals("he said \"hi\"", name.getCommonName());
|
||||
} finally {
|
||||
name.free();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_String_RFC2253_WhitespaceAroundEquals()
|
||||
throws WolfSSLException {
|
||||
|
||||
WolfSSLX509Name name = new WolfSSLX509Name("CN = foo, C = US");
|
||||
try {
|
||||
assertEquals("foo", name.getCommonName());
|
||||
assertEquals("US", name.getCountryName());
|
||||
} finally {
|
||||
name.free();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_String_RFC2253_TrailingSeparatorThrows() {
|
||||
|
||||
try {
|
||||
new WolfSSLX509Name("CN=foo,C=US,");
|
||||
fail("expected WolfSSLException for trailing separator");
|
||||
} catch (WolfSSLException e) {
|
||||
/* expected */
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_String_RFC2253_MissingEqualsThrows() {
|
||||
|
||||
try {
|
||||
new WolfSSLX509Name("CN foo,C=US");
|
||||
fail("expected WolfSSLException for missing '='");
|
||||
} catch (WolfSSLException e) {
|
||||
/* expected */
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_String_RFC2253_UnterminatedQuoteThrows() {
|
||||
|
||||
try {
|
||||
new WolfSSLX509Name("CN=\"unterminated,C=US");
|
||||
fail("expected WolfSSLException for unterminated quote");
|
||||
} catch (WolfSSLException e) {
|
||||
/* expected */
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_String_RFC2253_TrailingBackslashThrows() {
|
||||
|
||||
/* Single '\' at end of value with nothing to escape. */
|
||||
try {
|
||||
new WolfSSLX509Name("CN=foo\\");
|
||||
fail("expected WolfSSLException for trailing backslash");
|
||||
} catch (WolfSSLException e) {
|
||||
/* expected */
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_String_RFC2253_IncompleteHexEscapeAtEndThrows() {
|
||||
|
||||
/* "\C" at end of input: only one hex digit, no second to make a
|
||||
* complete \xx byte escape. */
|
||||
try {
|
||||
new WolfSSLX509Name("CN=foo\\C");
|
||||
fail("expected WolfSSLException for incomplete hex escape");
|
||||
} catch (WolfSSLException e) {
|
||||
/* expected */
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_String_RFC2253_IncompleteHexEscapeBadDigitThrows() {
|
||||
|
||||
/* "\Cz": first nibble is hex, second isn't. Parser must reject
|
||||
* rather than treat as single-char escape. */
|
||||
try {
|
||||
new WolfSSLX509Name("CN=foo\\Cz");
|
||||
fail("expected WolfSSLException for non-hex second digit");
|
||||
} catch (WolfSSLException e) {
|
||||
/* expected */
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_String_RFC2253_UnexpectedCharAtTypeStartThrows() {
|
||||
|
||||
/* '=' is not a valid first char for an attribute type. */
|
||||
try {
|
||||
new WolfSSLX509Name("=foo,CN=bar");
|
||||
fail("expected WolfSSLException for unexpected character");
|
||||
} catch (WolfSSLException e) {
|
||||
/* expected */
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_String_RFC2253_BadRdnSeparatorThrows() {
|
||||
|
||||
/* After a quoted value the next non-whitespace char must be ','
|
||||
* or ';'. Anything else (here '|') hits the parser's separator
|
||||
* check rather than being silently absorbed into the value. */
|
||||
try {
|
||||
new WolfSSLX509Name("CN=\"foo\"|O=bar");
|
||||
fail("expected WolfSSLException for bad RDN separator");
|
||||
} catch (WolfSSLException e) {
|
||||
/* expected */
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_String_RFC2253_RdnInsertionOrderIsReversed()
|
||||
throws WolfSSLException {
|
||||
|
||||
/* Parser walks textual L-to-R then Collections.reverse()s before
|
||||
* insertion (least-significant-first / X.509 encoding order).
|
||||
* Mirrors are "last write wins": after reversal the calls happen
|
||||
* as CN=last then CN=first, leaving "first" in the mirror. If
|
||||
* reversal weren't happening, the mirror would hold "last". */
|
||||
WolfSSLX509Name name = new WolfSSLX509Name("CN=first,CN=last");
|
||||
try {
|
||||
assertEquals("first", name.getCommonName());
|
||||
} finally {
|
||||
name.free();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_String_RFC2253_EmptyValueAtEofMatchesSetX()
|
||||
throws WolfSSLException {
|
||||
|
||||
/* "CN=" at end of input and "CN=,O=foo" both produce an RDN with an
|
||||
* empty value, matching the explicit setX("") path. Native may or may
|
||||
* not produce a useful cert from an empty value, but the wrapper
|
||||
* accepts it consistently across all three entry points (no-arg +
|
||||
* setX, parser at EOF, parser mid-DN). */
|
||||
WolfSSLX509Name viaSet = new WolfSSLX509Name();
|
||||
try {
|
||||
viaSet.setCommonName("");
|
||||
} catch (WolfSSLException e) {
|
||||
/* If setX("") doesn't go through cleanly, the parser path for
|
||||
* "CN=" must fail the same way. Verify and exit. */
|
||||
viaSet.free();
|
||||
try {
|
||||
new WolfSSLX509Name("CN=");
|
||||
fail("setX(\"\") threw but parser path didn't");
|
||||
} catch (WolfSSLException e2) {
|
||||
/* expected: paths conform */
|
||||
}
|
||||
return;
|
||||
}
|
||||
viaSet.free();
|
||||
|
||||
/* setX("") succeeded, so the parser must too. */
|
||||
WolfSSLX509Name viaParser = new WolfSSLX509Name("CN=");
|
||||
try {
|
||||
assertEquals("", viaParser.getCommonName());
|
||||
} finally {
|
||||
viaParser.free();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_String_RFC2253_SurrogatePairValue()
|
||||
throws WolfSSLException {
|
||||
|
||||
/* U+1F600 (grinning face emoji) encodes as a UTF-16 surrogate
|
||||
* pair in a Java String and four UTF-8 bytes. The parser uses
|
||||
* codePointAt + charCount so the pair flows through to the byte
|
||||
* stream correctly. charAt-only logic would split or mangle it. */
|
||||
String emoji = "\uD83D\uDE00";
|
||||
WolfSSLX509Name name =
|
||||
new WolfSSLX509Name("CN=" + emoji + ",C=US");
|
||||
try {
|
||||
assertEquals(emoji, name.getCommonName());
|
||||
assertEquals("US", name.getCountryName());
|
||||
} finally {
|
||||
name.free();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_X500Principal_PopulatesMirrorFields()
|
||||
throws WolfSSLException {
|
||||
|
||||
X500Principal principal = new X500Principal(
|
||||
"CN=wolfssl.com,O=wolfSSL Inc.,C=US");
|
||||
|
||||
WolfSSLX509Name name = new WolfSSLX509Name(principal);
|
||||
try {
|
||||
assertEquals("US", name.getCountryName());
|
||||
assertEquals("wolfSSL Inc.", name.getOrganizationName());
|
||||
assertEquals("wolfssl.com", name.getCommonName());
|
||||
} finally {
|
||||
name.free();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_X500Principal_RoundTrip() throws WolfSSLException {
|
||||
|
||||
/* Build a principal from RFC 2253 string, feed it to our
|
||||
* constructor, verify mirrors match. This is the canonical use case
|
||||
* for round tripping cert subjects. */
|
||||
String original = "CN=wolfssl.com,OU=Engineering," +
|
||||
"O=wolfSSL Inc.,L=Bozeman,ST=Montana,C=US";
|
||||
|
||||
X500Principal principal = new X500Principal(original);
|
||||
WolfSSLX509Name name = new WolfSSLX509Name(principal);
|
||||
try {
|
||||
assertEquals("US", name.getCountryName());
|
||||
assertEquals("Montana", name.getStateOrProvinceName());
|
||||
assertEquals("Bozeman", name.getLocalityName());
|
||||
assertEquals("wolfSSL Inc.", name.getOrganizationName());
|
||||
assertEquals("Engineering", name.getOrganizationalUnitName());
|
||||
assertEquals("wolfssl.com", name.getCommonName());
|
||||
} finally {
|
||||
name.free();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_X500Principal_NullThrows() {
|
||||
|
||||
try {
|
||||
new WolfSSLX509Name((X500Principal) null);
|
||||
fail("expected WolfSSLException for null X500Principal");
|
||||
} catch (WolfSSLException e) {
|
||||
/* expected */
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test_X500Principal_UnknownAttributeThrows() {
|
||||
|
||||
/* X500Principal accepts any attribute as a string. Our constructor
|
||||
* should fail when wolfSSL doesn't recognize it. */
|
||||
X500Principal principal = new X500Principal("1.2.3.4.5.6.7=foo,CN=bar");
|
||||
try {
|
||||
new WolfSSLX509Name(principal);
|
||||
fail("expected WolfSSLException for unknown OID");
|
||||
} catch (WolfSSLException e) {
|
||||
/* expected */
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue