/*
*
* Copyright (c) 2013 - 2017 Lijun Liao
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License version 3
* as published by the Free Software Foundation with the addition of the
* following permission added to Section 15 as permitted in Section 7(a):
*
* FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY
* THE AUTHOR LIJUN LIAO. LIJUN LIAO DISCLAIMS THE WARRANTY OF NON INFRINGEMENT
* OF THIRD PARTY RIGHTS.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License.
*
* You can be released from the requirements of the license by purchasing
* a commercial license. Buying such a license is mandatory as soon as you
* develop commercial activities involving the XiPKI software without
* disclosing the source code of your own applications.
*
* For more information, please contact Lijun Liao at this
* address: lijun.liao@gmail.com
*/
package org.xipki.pki.ca.api.profile.x509;
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.regex.Pattern;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xipki.commons.common.util.ParamUtil;
import org.xipki.commons.common.util.StringUtil;
import org.xipki.commons.security.ObjectIdentifiers;
import org.xipki.pki.ca.api.profile.CertprofileException;
import org.xipki.pki.ca.api.profile.Range;
import org.xipki.pki.ca.api.profile.RdnControl;
import org.xipki.pki.ca.api.profile.StringType;
/**
* @author Lijun Liao
* @since 2.0.0
*/
public class SubjectDnSpec {
private static final Logger LOG = LoggerFactory.getLogger(SubjectDnSpec.class);
public static final Pattern PATTERN_DATE_OF_BIRTH =
Pattern.compile("^(19|20)\\d\\d(0[1-9]|1[012])(0[1-9]|[12][0-9]|3[01])120000Z");
/**
* ranges.
*/
private static final Range RANGE_64 = new Range(1, 64);
private static final Range RANGE_128 = new Range(1, 128);
private static final Range RANGE_POSTAL_CODE = new Range(1, 40);
private static final Range RANGE_COUNTRY_NAME = new Range(2, 2);
private static final Range RANGE_POSTAL_ADDRESS = new Range(0, 30);
private static final Range RANGE_GENDER = new Range(1, 1);
private static final Range RANGE_DATE_OF_BIRTH = new Range(15, 15);
// according to specification should be 32768, 256 is specified by XiPKI.
private static final Range RANGE_NAME = new Range(1, 256);
// patterns
private static final Pattern PATTERN_GENDER = Pattern.compile("M|m|F|f");
private static final Pattern PATTERN_COUNTRY = Pattern.compile("[A-Za-z]{2}");
// stringTypes
private static final Set<StringType> DIRECTORY_STRINGS = new HashSet<>(
Arrays.asList(StringType.bmpString, StringType.printableString,
StringType.teletexString, StringType.utf8String));
private static final Set<StringType> PRINTABLE_STRING_ONLY = new HashSet<>(
Arrays.asList(StringType.printableString));
private static final Set<StringType> IA5_STRING_ONLY = new HashSet<>(
Arrays.asList(StringType.ia5String));
private static final Map<ASN1ObjectIdentifier, StringType> DFLT_STRING_TYPES = new HashMap<>();
private static final Map<ASN1ObjectIdentifier, Range> RANGES = new HashMap<>();
private static final Map<ASN1ObjectIdentifier, Pattern> PATTERNS = new HashMap<>();
private static final Map<ASN1ObjectIdentifier, RdnControl> CONTROLS = new HashMap<>();
private static final Map<ASN1ObjectIdentifier, Set<StringType>> STRING_TYPE_SET =
new HashMap<>();
private static final List<ASN1ObjectIdentifier> forwardDNs;
private static final Set<String> countryAreaCodes = new HashSet<>();
static {
// ----- RDN order -----
BufferedReader reader = getReader("org.xipki.pki.ca.rdnorder.cfg", "/conf/rdnorder.cfg");
List<ASN1ObjectIdentifier> tmpForwardDNs = new ArrayList<>(25);
String line;
try {
while ((line = reader.readLine()) != null) {
line = line.trim();
if (line.isEmpty() || line.startsWith("#")) {
continue;
}
ASN1ObjectIdentifier oid = new ASN1ObjectIdentifier(line);
tmpForwardDNs.add(oid);
}
} catch (Exception ex) {
throw new RuntimeException("could not load RDN order: " + ex.getMessage(), ex);
} finally {
try {
reader.close();
} catch (IOException ex) {
// CHECKSTYLE:SKIP
}
}
forwardDNs = Collections.unmodifiableList(tmpForwardDNs);
if (LOG.isInfoEnabled()) {
StringBuilder sb = new StringBuilder(500);
sb.append("forward RDNs: ");
for (ASN1ObjectIdentifier oid : forwardDNs) {
sb.append(oid.getId()).append(", ");
}
if (!forwardDNs.isEmpty()) {
sb.delete(sb.length() - 2, sb.length());
}
LOG.info(sb.toString());
}
List<ASN1ObjectIdentifier> tmpBackwardDNs = new ArrayList<>(25);
int size = tmpForwardDNs.size();
for (int i = size - 1; i >= 0; i--) {
tmpBackwardDNs.add(tmpForwardDNs.get(i));
}
// ----- country/area code -----
reader = getReader("org.xipki.pki.ca.areacode.cfg", "/conf/areacode.cfg");
try {
while ((line = reader.readLine()) != null) {
line = line.trim();
if (line.isEmpty() || line.startsWith("#")) {
continue;
}
StringTokenizer st = new StringTokenizer(line, " \t");
final int n = st.countTokens();
// 1. country/area name
// 2. ISO ALPHA-2 code
// 3. ISO ALPHA-3 code
// 4. ISO numeric code
if (n < 4) {
LOG.warn("invalid country/area line {}", line);
continue;
}
final int alpha2CodeIndex = n - 3;
for (int i = 0; i < alpha2CodeIndex; i++) {
st.nextToken();
}
String areaCode = st.nextToken();
countryAreaCodes.add(areaCode.toUpperCase());
}
if (LOG.isInfoEnabled()) {
List<String> list = new ArrayList<>(countryAreaCodes);
Collections.sort(list);
LOG.info("area/country codes: {}", list);
}
} catch (Exception ex) {
throw new RuntimeException("could not load area code: " + ex.getMessage(), ex);
} finally {
try {
reader.close();
} catch (IOException ex) {
// CHECKSTYLE:SKIP
}
}
// ----- Type, Length -----
ASN1ObjectIdentifier id;
Set<ASN1ObjectIdentifier> ids = new HashSet<>();
// businessCategory
id = ObjectIdentifiers.DN_BUSINESS_CATEGORY;
ids.add(id);
RANGES.put(id, RANGE_128);
STRING_TYPE_SET.put(id, DIRECTORY_STRINGS);
DFLT_STRING_TYPES.put(id, StringType.utf8String);
// countryName
id = ObjectIdentifiers.DN_C;
ids.add(id);
RANGES.put(id, RANGE_COUNTRY_NAME);
STRING_TYPE_SET.put(id, PRINTABLE_STRING_ONLY);
DFLT_STRING_TYPES.put(id, StringType.printableString);
// commonName
id = ObjectIdentifiers.DN_CN;
ids.add(id);
RANGES.put(id, RANGE_64);
STRING_TYPE_SET.put(id, DIRECTORY_STRINGS);
DFLT_STRING_TYPES.put(id, StringType.utf8String);
// countryOfCitizenship
id = ObjectIdentifiers.DN_COUNTRY_OF_CITIZENSHIP;
ids.add(id);
RANGES.put(id, RANGE_COUNTRY_NAME);
PATTERNS.put(id, PATTERN_COUNTRY);
STRING_TYPE_SET.put(id, PRINTABLE_STRING_ONLY);
DFLT_STRING_TYPES.put(id, StringType.printableString);
// countryOfResidence
id = ObjectIdentifiers.DN_COUNTRY_OF_RESIDENCE;
ids.add(id);
RANGES.put(id, RANGE_COUNTRY_NAME);
PATTERNS.put(id, PATTERN_COUNTRY);
STRING_TYPE_SET.put(id, PRINTABLE_STRING_ONLY);
DFLT_STRING_TYPES.put(id, StringType.printableString);
// DATE_OF_BIRTH
id = ObjectIdentifiers.DN_DATE_OF_BIRTH;
ids.add(id);
RANGES.put(id, RANGE_DATE_OF_BIRTH);
PATTERNS.put(id, PATTERN_DATE_OF_BIRTH);
// domainComponent
id = ObjectIdentifiers.DN_DC;
ids.add(id);
STRING_TYPE_SET.put(id, IA5_STRING_ONLY);
DFLT_STRING_TYPES.put(id, StringType.ia5String);
// RFC 2256 dmdName
id = ObjectIdentifiers.DN_DMD_NAME;
ids.add(id);
STRING_TYPE_SET.put(id, DIRECTORY_STRINGS);
DFLT_STRING_TYPES.put(id, StringType.utf8String);
// gender
id = ObjectIdentifiers.DN_GENDER;
ids.add(id);
PATTERNS.put(id, PATTERN_GENDER);
RANGES.put(id, RANGE_GENDER);
STRING_TYPE_SET.put(id, PRINTABLE_STRING_ONLY);
DFLT_STRING_TYPES.put(id, StringType.printableString);
// generation qualifier
id = ObjectIdentifiers.DN_GENERATION_QUALIFIER;
ids.add(id);
RANGES.put(id, RANGE_64);
STRING_TYPE_SET.put(id, DIRECTORY_STRINGS);
DFLT_STRING_TYPES.put(id, StringType.utf8String);
// givenName
id = ObjectIdentifiers.DN_GIVENNAME;
ids.add(id);
RANGES.put(id, RANGE_64);
STRING_TYPE_SET.put(id, DIRECTORY_STRINGS);
DFLT_STRING_TYPES.put(id, StringType.utf8String);
// initials
id = ObjectIdentifiers.DN_INITIALS;
ids.add(id);
RANGES.put(id, RANGE_64);
STRING_TYPE_SET.put(id, DIRECTORY_STRINGS);
DFLT_STRING_TYPES.put(id, StringType.utf8String);
// LDAP user ID
id = ObjectIdentifiers.DN_LDAP_UID;
ids.add(id);
STRING_TYPE_SET.put(id, DIRECTORY_STRINGS);
DFLT_STRING_TYPES.put(id, StringType.utf8String);
// localityName
id = ObjectIdentifiers.DN_LOCALITYNAME;
ids.add(id);
RANGES.put(id, RANGE_128);
STRING_TYPE_SET.put(id, DIRECTORY_STRINGS);
DFLT_STRING_TYPES.put(id, StringType.utf8String);
// name
id = ObjectIdentifiers.DN_NAME;
ids.add(id);
RANGES.put(id, RANGE_NAME);
STRING_TYPE_SET.put(id, DIRECTORY_STRINGS);
DFLT_STRING_TYPES.put(id, StringType.utf8String);
// nameOfBirth
id = ObjectIdentifiers.DN_NAME_AT_BIRTH;
ids.add(id);
RANGES.put(id, RANGE_64);
STRING_TYPE_SET.put(id, DIRECTORY_STRINGS);
DFLT_STRING_TYPES.put(id, StringType.utf8String);
// organizationName
id = ObjectIdentifiers.DN_O;
ids.add(id);
RANGES.put(id, RANGE_64);
STRING_TYPE_SET.put(id, DIRECTORY_STRINGS);
DFLT_STRING_TYPES.put(id, StringType.utf8String);
// organizationIdentifier
id = ObjectIdentifiers.DN_organizationIdentifier;
ids.add(id);
RANGES.put(id, RANGE_64);
STRING_TYPE_SET.put(id, DIRECTORY_STRINGS);
DFLT_STRING_TYPES.put(id, StringType.utf8String);
// organizationalUnitName
id = ObjectIdentifiers.DN_OU;
ids.add(id);
RANGES.put(id, RANGE_64);
STRING_TYPE_SET.put(id, DIRECTORY_STRINGS);
DFLT_STRING_TYPES.put(id, StringType.utf8String);
// placeOfBirth
id = ObjectIdentifiers.DN_PLACE_OF_BIRTH;
ids.add(id);
RANGES.put(id, RANGE_128);
STRING_TYPE_SET.put(id, DIRECTORY_STRINGS);
DFLT_STRING_TYPES.put(id, StringType.utf8String);
// postalAddress
id = ObjectIdentifiers.DN_POSTAL_ADDRESS;
ids.add(id);
RANGES.put(id, RANGE_POSTAL_ADDRESS);
STRING_TYPE_SET.put(id, DIRECTORY_STRINGS);
DFLT_STRING_TYPES.put(id, StringType.utf8String);
// postalCode
id = ObjectIdentifiers.DN_POSTAL_CODE;
ids.add(id);
RANGES.put(id, RANGE_POSTAL_CODE);
STRING_TYPE_SET.put(id, DIRECTORY_STRINGS);
DFLT_STRING_TYPES.put(id, StringType.utf8String);
// pseudonym
id = ObjectIdentifiers.DN_PSEUDONYM;
ids.add(id);
RANGES.put(id, RANGE_64);
STRING_TYPE_SET.put(id, DIRECTORY_STRINGS);
DFLT_STRING_TYPES.put(id, StringType.utf8String);
// distinguishedNameQualifier
id = ObjectIdentifiers.DN_QUALIFIER;
ids.add(id);
RANGES.put(id, RANGE_64);
STRING_TYPE_SET.put(id, PRINTABLE_STRING_ONLY);
DFLT_STRING_TYPES.put(id, StringType.printableString);
// serialNumber
id = ObjectIdentifiers.DN_SERIALNUMBER;
ids.add(id);
RANGES.put(id, RANGE_64);
STRING_TYPE_SET.put(id, PRINTABLE_STRING_ONLY);
DFLT_STRING_TYPES.put(id, StringType.printableString);
// stateOrProvinceName
id = ObjectIdentifiers.DN_ST;
ids.add(id);
RANGES.put(id, RANGE_128);
STRING_TYPE_SET.put(id, DIRECTORY_STRINGS);
DFLT_STRING_TYPES.put(id, StringType.utf8String);
// streetAddress
id = ObjectIdentifiers.DN_STREET;
ids.add(id);
RANGES.put(id, RANGE_128);
STRING_TYPE_SET.put(id, DIRECTORY_STRINGS);
DFLT_STRING_TYPES.put(id, StringType.utf8String);
// surName
id = ObjectIdentifiers.DN_SURNAME;
ids.add(id);
RANGES.put(id, RANGE_64);
STRING_TYPE_SET.put(id, DIRECTORY_STRINGS);
DFLT_STRING_TYPES.put(id, StringType.utf8String);
// title
id = ObjectIdentifiers.DN_T;
ids.add(id);
RANGES.put(id, RANGE_64);
STRING_TYPE_SET.put(id, DIRECTORY_STRINGS);
DFLT_STRING_TYPES.put(id, StringType.utf8String);
// telefonNumber
id = ObjectIdentifiers.DN_TELEPHONE_NUMBER;
ids.add(id);
STRING_TYPE_SET.put(id, DIRECTORY_STRINGS);
DFLT_STRING_TYPES.put(id, StringType.utf8String);
// unique Identifier
id = ObjectIdentifiers.DN_UNIQUE_IDENTIFIER;
ids.add(id);
STRING_TYPE_SET.put(id, DIRECTORY_STRINGS);
DFLT_STRING_TYPES.put(id, StringType.utf8String);
// unstructedAddress
id = ObjectIdentifiers.DN_UnstructuredAddress;
ids.add(id);
STRING_TYPE_SET.put(id, DIRECTORY_STRINGS);
DFLT_STRING_TYPES.put(id, StringType.utf8String);
// unstructedName
id = ObjectIdentifiers.DN_UnstructuredName;
ids.add(id);
STRING_TYPE_SET.put(id, DIRECTORY_STRINGS);
DFLT_STRING_TYPES.put(id, StringType.utf8String);
for (ASN1ObjectIdentifier type : ids) {
StringType stringType = DFLT_STRING_TYPES.get(type);
if (stringType == null) {
stringType = StringType.utf8String;
}
RdnControl control = new RdnControl(type,
0, // minOccurs
9 //maxOccurs
);
control.setStringType(stringType);
control.setStringLengthRange(RANGES.get(type));
Pattern pattern = PATTERNS.get(type);
if (pattern != null) {
control.setPatterns(Arrays.asList(pattern));
}
CONTROLS.put(type, control);
}
}
private SubjectDnSpec() {
}
public static Range getStringLengthRange(final ASN1ObjectIdentifier rdnType) {
ParamUtil.requireNonNull("rdnType", rdnType);
return RANGES.get(rdnType);
}
public static Pattern getPattern(final ASN1ObjectIdentifier rdnType) {
ParamUtil.requireNonNull("rdnType", rdnType);
return PATTERNS.get(rdnType);
}
public static StringType getStringType(final ASN1ObjectIdentifier rdnType) {
ParamUtil.requireNonNull("rdnType", rdnType);
return DFLT_STRING_TYPES.get(rdnType);
}
public static RdnControl getRdnControl(final ASN1ObjectIdentifier rdnType) {
ParamUtil.requireNonNull("rdnType", rdnType);
RdnControl control = CONTROLS.get(rdnType);
if (control == null) {
// minOccurs = 0, maxOccurs = 9
control = new RdnControl(rdnType, 0, 9);
control.setStringType(StringType.utf8String);
}
return control;
} // static
public static void fixRdnControl(final RdnControl control) throws CertprofileException {
ParamUtil.requireNonNull("control", control);
ASN1ObjectIdentifier type = control.getType();
StringType stringType = control.getStringType();
if (stringType != null) {
if (STRING_TYPE_SET.containsKey(type)
&& !STRING_TYPE_SET.get(type).contains(stringType)) {
throw new CertprofileException(
String.format("%s is not allowed %s", stringType.name(), type.getId()));
}
} else {
StringType specStrType = DFLT_STRING_TYPES.get(type);
if (specStrType != null) {
control.setStringType(specStrType);
}
}
if (control.getPatterns() == null && PATTERNS.containsKey(type)) {
control.setPatterns(Arrays.asList(PATTERNS.get(type)));
}
Range specRange = RANGES.get(type);
if (specRange == null) {
control.setStringLengthRange(null);
return;
}
Range isRange = control.getStringLengthRange();
if (isRange == null) {
control.setStringLengthRange(specRange);
return;
}
boolean changed = false;
Integer specMin = specRange.getMin();
Integer min = isRange.getMin();
if (min == null) {
changed = true;
min = specMin;
} else if (specMin != null && specMin > min) {
changed = true;
min = specMin;
}
Integer specMax = specRange.getMax();
Integer max = isRange.getMax();
if (max == null) {
changed = true;
max = specMax;
} else if (specMax != null && specMax < max) {
changed = true;
max = specMax;
}
if (changed) {
isRange.setRange(min, max);
} // isRange
} // method fixRdnControl
public static List<ASN1ObjectIdentifier> getForwardDNs() {
return forwardDNs;
}
public static boolean isValidCountryAreaCode(String code) {
ParamUtil.requireNonBlank("code", code);
return countryAreaCodes.isEmpty() ? true : countryAreaCodes.contains(code.toUpperCase());
}
private static BufferedReader getReader(final String propKey, final String fallbackResource) {
String confFile = System.getProperty(propKey);
if (StringUtil.isNotBlank(confFile)) {
LOG.info("read from file " + confFile);
try {
return new BufferedReader(new FileReader(confFile));
} catch (FileNotFoundException ex) {
throw new RuntimeException("could not access non-existing file " + confFile);
}
} else {
InputStream confStream = SubjectDnSpec.class.getResourceAsStream(fallbackResource);
if (confStream == null) {
throw new RuntimeException("could not access non-existing resource "
+ fallbackResource);
}
LOG.info("read from resource " + fallbackResource);
return new BufferedReader(new InputStreamReader(confStream));
}
}
}