/*
* Copyright (c) 2012 ICM Uniwersytet Warszawski All rights reserved.
* See LICENCE.txt file for licensing information.
*/
package eu.emi.security.authn.x509.impl;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.bouncycastle.asn1.ASN1Object;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.DERBitString;
import org.bouncycastle.asn1.ASN1String;
import org.bouncycastle.asn1.x500.AttributeTypeAndValue;
import org.bouncycastle.asn1.x500.RDN;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.util.Strings;
import eu.emi.security.authn.x509.helpers.JavaAndBCStyle;
/**
* This class provides support for the <b>legacy</b> Openssl format of DN encoding.
* Please <b>do not use this format unless it is absolutely necessary</b>. It has a number of problems
* see particular methods documentation for details.
*
* @author K. Benedyczak
*/
public class OpensslNameUtils
{
/**
* Holds mappings of labels which occur in the wild but are output differently by OpenSSL.
* Also useful to have a uniform representation when creating a normalized form.
* Note that in some cases OpenSSL doesn't have a label -> then an oid is used.
*/
public static final Map<String, String> NORMALIZED_LABELS = new HashMap<String, String>();
static
{
NORMALIZED_LABELS.put("e", "emailAddress");
NORMALIZED_LABELS.put("email", "emailAddress");
NORMALIZED_LABELS.put("userid", "UID");
NORMALIZED_LABELS.put("sn", "serialnumber");
NORMALIZED_LABELS.put("surname", "sn");
NORMALIZED_LABELS.put("givenname", "gn");
NORMALIZED_LABELS.put("dn", "dnQualifier");
NORMALIZED_LABELS.put("dnq", "dnQualifier");
NORMALIZED_LABELS.put("uniqueidentifier", "x500UniqueIdentifier");
NORMALIZED_LABELS.put("generation", "generationQualifier");
NORMALIZED_LABELS.put("s", "ST");
NORMALIZED_LABELS.put("ip", "1.3.6.1.4.1.42.2.11.2.1");
NORMALIZED_LABELS.put("nameatbirth", "1.3.36.8.3.14");
}
private static String normalizeLabel(String label)
{
String normalized = NORMALIZED_LABELS.get(label.toLowerCase());
return normalized == null ? label : normalized;
}
/**
* Performs cleaning of the provided openssl legacy DN. The following actions are performed:
* <ul>
* <li> all strings of the form '/TOKEN=' are converted to the '/NORMALIZED-TOKEN=',
* where TOKEN and NORMALIZED-TOKEN are taken from the {@link #NORMALIZED_LABELS} map
* <li> the string is converted to lower case
* </ul>
* Please note that this normalization is far from being perfect: non-ascii characters
* encoded in hex are not lower-cased, it may happen that some tokens are not in the map,
* values containing '/TOKEN=' as a substring will be messed up.
* @param legacyDN legacy DN
* @return normalized string (hopefully) suitable for the string comparison
*/
public static String normalize(String legacyDN)
{
Pattern p = Pattern.compile("/[^=]+=");
Matcher m = p.matcher(legacyDN);
StringBuilder output = new StringBuilder();
int i=0;
while (m.find())
{
output.append(legacyDN.substring(i, m.start()));
String group = m.group();
String label = group.substring(1, group.length()-1);
label = normalizeLabel(label);
output.append("/");
output.append(label);
output.append("=");
i=m.end();
}
output.append(legacyDN.substring(i, legacyDN.length()));
return output.toString().toLowerCase();
}
/**
* @see #opensslToRfc2253(String, boolean) with second arg equal to false
* @param inputDN input DN
* @return RFC 2253 representation of the input
* @deprecated This method is not planned for removal but it is marked as deprecated as it is highly unreliable
* and you should update your code not to use openssl style DNs at all
* @since 1.1.0
*/
@Deprecated
public static String opensslToRfc2253(String inputDN)
{
return opensslToRfc2253(inputDN, false);
}
/**
* Tries to convert the OpenSSL string representation
* of a DN into a RFC 2253 form. The conversion is as follows:
* <ol>
* <li> the string is split on '/',
* <li> all resulting parts which have no '=' sign inside are glued with the previous element
* <li> parts are output with ',' as a separator in reversed order.
* </ol>
* @param inputDN input DN
* @param withWildcards whether '*' wildcards need to be recognized
* @return RFC 2253 representation of the input
* @deprecated This method is not planned for removal but it is marked as deprecated as it is highly unreliable
* and you should update your code not to use openssl style DNs at all
* @since 1.1.0
*/
@Deprecated
public static String opensslToRfc2253(String inputDN, boolean withWildcards)
{
if (inputDN.length() < 2 || !inputDN.startsWith("/"))
throw new IllegalArgumentException("The string '" + inputDN +
"' is not a valid OpenSSL-encoded DN");
inputDN = inputDN.replace(",", "\\,");
String[] parts = inputDN.split("/");
if (parts.length < 2)
return inputDN.substring(1);
List<String> avas = new ArrayList<String>();
avas.add(parts[1]);
for (int i=2, j=0; i<parts.length; i++)
{
if (!(parts[i].contains("=") || (withWildcards && parts[i].contains("*"))))
{
String cur = avas.get(j);
avas.set(j, cur+"/"+parts[i]);
} else
{
avas.add(++j, parts[i]);
}
}
StringBuilder buf = new StringBuilder();
for (int i=avas.size()-1; i>0; i--)
buf.append(avas.get(i)).append(",");
buf.append(avas.get(0));
return buf.toString();
}
/**
* Returns an OpenSSL legacy (and as of now the default in OpenSSL) encoding of the provided RFC 2253 DN.
* Please note that this method is:
* <ul>
* <li> written on a best effort basis: OpenSSL format is not documented anywhere.
* <li> it much more problematic to perform an opposite translation as OpenSSL format is highly ambiguous.
* <li> it is <b>STRONGLY</b> suggested not to use this format anywhere, especially in security setups, as
* many different DNs has the same OpenSSL representation, and also not to use this method.
* </ul>
* Additionally there is a possibility to turn on the "Globus" compatible mode. In this mode this method
* behaves more similarly to the one provided by the COG Jglobus. The basic difference is that RDNs containing
* multiple AVAs are are concatenated with '+' not with '/'.
* <p>
* If you want to compare the output of this method (using string comparison) with something
* generated by openssl from a certificate, you can expect problems in case of:
* <ul>
* <li>multivalued RDNs: you should sort them, but in OpenSSL format it is even impossible to find them.
* With globusFlavouring turned on it is bit better, but as there is no escaping of special characters
* you are lost too.
* <li>not-so-common attributes used in DN: there is a plenty of attributes which have (or have not)
* short or long names defined in OpenSSL. This changes over the time in OpenSSL.
* Also every Globus/gLite/... tool can use a different set. Therefore whether a correct short name,
* long name or oid is used by this method is also problematic. It is guaranteed that the basic ones
* (DC, C, OU, O, L, ...) are working. But in case of less common expect troubles (e.g.
* openssl 1.0.0i uses 'id-pda-countryOfResidence', while this method will output 'CountryOfResidence').
* </ul>
* @param srcDn input in RFC 2253 format or similar
* @param globusFlavouring globus flavouring
* @return openssl format encoded input.
* @since 1.1.0
*/
public static String convertFromRfc2253(String srcDn, boolean globusFlavouring)
{
String avasSeparator = globusFlavouring ? "+" : "/";
JavaAndBCStyle style = new JavaAndBCStyle();
X500Name x500Name = new X500Name(style, srcDn);
RDN[] rdns = x500Name.getRDNs();
StringBuilder ret = new StringBuilder();
for (int i=rdns.length-1; i>=0; i--)
{
ret.append("/");
RDN rdn = rdns[i];
AttributeTypeAndValue[] atvs = rdn.getTypesAndValues();
for (int j=atvs.length-1; j>=0; j--)
{
AttributeTypeAndValue atv = atvs[j];
ret.append(getShortName4Openssl(atv.getType()));
ret.append("=");
ret.append(getOpensslValue(atv.getValue().toASN1Primitive()));
if (j>0)
ret.append(avasSeparator);
}
}
return ret.toString();
}
private static String getShortName4Openssl(ASN1ObjectIdentifier id)
{
JavaAndBCStyle style = new JavaAndBCStyle();
String name = style.getLabelForOidFull(id);
if (name == null)
return id.getId();
return normalizeLabel(name);
}
private static String getOpensslValue(ASN1Object val)
{
byte[] bytes;
if (val instanceof DERBitString)
{
bytes = ((DERBitString)val).getBytes();
} else if (val instanceof ASN1String)
{
String valS = ((ASN1String)val).getString();
char[] chars = valS.toCharArray();
bytes = Strings.toUTF8ByteArray(chars);
} else
throw new IllegalArgumentException("Got AVA value of unsupported type: " +
val.getClass().getName());
StringBuilder sb = new StringBuilder();
for (byte b: bytes)
{
if (b <= 0x1f)
{
sb.append("\\x" + Integer.toHexString(b & 0xff).toUpperCase());
} else
sb.append((char)b);
}
return sb.toString();
}
}