/*
*
* 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.qa;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;
import org.bouncycastle.asn1.ASN1Encodable;
import org.bouncycastle.asn1.ASN1GeneralizedTime;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.ASN1Sequence;
import org.bouncycastle.asn1.DERBMPString;
import org.bouncycastle.asn1.DERIA5String;
import org.bouncycastle.asn1.DERPrintableString;
import org.bouncycastle.asn1.DERT61String;
import org.bouncycastle.asn1.DERUTF8String;
import org.bouncycastle.asn1.x500.AttributeTypeAndValue;
import org.bouncycastle.asn1.x500.RDN;
import org.bouncycastle.asn1.x500.X500Name;
import org.xipki.commons.common.qa.ValidationIssue;
import org.xipki.commons.common.util.CollectionUtil;
import org.xipki.commons.common.util.ParamUtil;
import org.xipki.commons.security.ObjectIdentifiers;
import org.xipki.commons.security.util.X509Util;
import org.xipki.pki.ca.api.BadCertTemplateException;
import org.xipki.pki.ca.api.profile.CertprofileException;
import org.xipki.pki.ca.api.profile.RdnControl;
import org.xipki.pki.ca.api.profile.StringType;
import org.xipki.pki.ca.api.profile.x509.SpecialX509CertprofileBehavior;
import org.xipki.pki.ca.api.profile.x509.SubjectControl;
import org.xipki.pki.ca.api.profile.x509.SubjectDnSpec;
/**
* @author Lijun Liao
* @since 2.0.0
*/
public class SubjectChecker {
private final SpecialX509CertprofileBehavior specialBehavior;
private final SubjectControl subjectControl;
public SubjectChecker(final SpecialX509CertprofileBehavior specialBehavior,
final SubjectControl subjectControl) throws CertprofileException {
this.specialBehavior = specialBehavior;
this.subjectControl = ParamUtil.requireNonNull("subjectControl", subjectControl);
}
public List<ValidationIssue> checkSubject(final X500Name subject,
final X500Name requestedSubject) {
ParamUtil.requireNonNull("subject", subject);
ParamUtil.requireNonNull("requestedSubject", requestedSubject);
// collect subject attribute types to check
Set<ASN1ObjectIdentifier> oids = new HashSet<>();
for (ASN1ObjectIdentifier oid : subjectControl.getTypes()) {
oids.add(oid);
}
for (ASN1ObjectIdentifier oid : subject.getAttributeTypes()) {
oids.add(oid);
}
List<ValidationIssue> result = new LinkedList<>();
ValidationIssue issue = new ValidationIssue("X509.SUBJECT.group", "X509 subject RDN group");
result.add(issue);
if (CollectionUtil.isNonEmpty(subjectControl.getGroups())) {
Set<String> groups = new HashSet<>(subjectControl.getGroups());
for (String g : groups) {
boolean toBreak = false;
RDN rdn = null;
for (ASN1ObjectIdentifier type : subjectControl.getTypesForGroup(g)) {
RDN[] rdns = subject.getRDNs(type);
if (rdns == null || rdns.length == 0) {
continue;
}
if (rdns.length > 1) {
issue.setFailureMessage("AttributeTypeAndValues of group " + g
+ " is not in one RDN");
toBreak = true;
break;
}
if (rdn == null) {
rdn = rdns[0];
} else if (rdn != rdns[0]) {
issue.setFailureMessage("AttributeTypeAndValues of group " + g
+ " is not in one RDN");
toBreak = true;
break;
}
}
if (toBreak) {
break;
}
}
}
for (ASN1ObjectIdentifier type : oids) {
ValidationIssue valIssue;
try {
valIssue = checkSubjectAttribute(type, subject, requestedSubject);
} catch (BadCertTemplateException ex) {
valIssue = new ValidationIssue("X509.SUBJECT.REQUEST", "Subject in request");
valIssue.setFailureMessage(ex.getMessage());
}
result.add(valIssue);
}
return result;
} // method checkSubject
private ValidationIssue checkSubjectAttribute(final ASN1ObjectIdentifier type,
final X500Name subject, final X500Name requestedSubject)
throws BadCertTemplateException {
boolean multiValuedRdn = subjectControl.getGroup(type) != null;
if (multiValuedRdn) {
return checkSubjectAttributeMultiValued(type, subject, requestedSubject);
} else {
return checkSubjectAttributeNotMultiValued(type, subject, requestedSubject);
}
}
private ValidationIssue checkSubjectAttributeNotMultiValued(final ASN1ObjectIdentifier type,
final X500Name subject, final X500Name requestedSubject)
throws BadCertTemplateException {
ValidationIssue issue = createSubjectIssue(type);
// control
RdnControl rdnControl = subjectControl.getControl(type);
int minOccurs = (rdnControl == null) ? 0 : rdnControl.getMinOccurs();
int maxOccurs = (rdnControl == null) ? 0 : rdnControl.getMaxOccurs();
RDN[] rdns = subject.getRDNs(type);
int rdnsSize = (rdns == null) ? 0 : rdns.length;
if (rdnsSize < minOccurs || rdnsSize > maxOccurs) {
issue.setFailureMessage("number of RDNs '" + rdnsSize
+ "' is not within [" + minOccurs + ", " + maxOccurs + "]");
return issue;
}
RDN[] requestedRdns = requestedSubject.getRDNs(type);
if (rdnsSize == 0) {
// check optional attribute but is present in requestedSubject
if (maxOccurs > 0 && requestedRdns != null && requestedRdns.length > 0) {
issue.setFailureMessage("is absent but expected present");
}
return issue;
}
StringBuilder failureMsg = new StringBuilder();
// check the encoding
StringType stringType = null;
if (rdnControl != null) {
stringType = rdnControl.getStringType();
}
List<String> requestedCoreAtvTextValues = new LinkedList<>();
if (requestedRdns != null) {
for (RDN requestedRdn : requestedRdns) {
String textValue = getRdnTextValueOfRequest(requestedRdn);
requestedCoreAtvTextValues.add(textValue);
}
if (rdnControl != null && rdnControl.getPatterns() != null) {
// sort the requestedRDNs
requestedCoreAtvTextValues = sort(requestedCoreAtvTextValues,
rdnControl.getPatterns());
}
}
if (rdns == null) { // return always false, only to make the null checker happy
return issue;
}
for (int i = 0; i < rdns.length; i++) {
RDN rdn = rdns[i];
AttributeTypeAndValue[] atvs = rdn.getTypesAndValues();
if (atvs.length > 1) {
failureMsg.append("size of RDN[" + i + "] is '" + atvs.length
+ "' but expected '1'");
failureMsg.append("; ");
continue;
}
String atvTextValue = getAtvValueString("RDN[" + i + "]", atvs[0], stringType,
failureMsg);
if (atvTextValue == null) {
continue;
}
checkAttributeTypeAndValue("RDN[" + i + "]", type,
atvTextValue, rdnControl, requestedCoreAtvTextValues, i, failureMsg);
}
int len = failureMsg.length();
if (len > 2) {
failureMsg.delete(len - 2, len);
issue.setFailureMessage(failureMsg.toString());
}
return issue;
} // method checkSubjectAttributeNotMultiValued
private ValidationIssue checkSubjectAttributeMultiValued(final ASN1ObjectIdentifier type,
final X500Name subject, final X500Name requestedSubject)
throws BadCertTemplateException {
ValidationIssue issue = createSubjectIssue(type);
RDN[] rdns = subject.getRDNs(type);
int rdnsSize = (rdns == null) ? 0 : rdns.length;
RDN[] requestedRdns = requestedSubject.getRDNs(type);
if (rdnsSize != 1) {
if (rdnsSize == 0) {
// check optional attribute but is present in requestedSubject
if (requestedRdns != null && requestedRdns.length > 0) {
issue.setFailureMessage("is absent but expected present");
}
} else {
issue.setFailureMessage("number of RDNs '" + rdnsSize + "' is not 1");
}
return issue;
}
// control
final RdnControl rdnControl = subjectControl.getControl(type);
// check the encoding
StringType stringType = null;
if (rdnControl != null) {
stringType = rdnControl.getStringType();
}
List<String> requestedCoreAtvTextValues = new LinkedList<>();
if (requestedRdns != null) {
for (RDN requestedRdn : requestedRdns) {
String textValue = getRdnTextValueOfRequest(requestedRdn);
requestedCoreAtvTextValues.add(textValue);
}
if (rdnControl != null && rdnControl.getPatterns() != null) {
// sort the requestedRDNs
requestedCoreAtvTextValues = sort(requestedCoreAtvTextValues,
rdnControl.getPatterns());
}
}
if (rdns == null) { // return always false, only to make the null checker happy
return issue;
}
StringBuilder failureMsg = new StringBuilder();
AttributeTypeAndValue[] li = rdns[0].getTypesAndValues();
List<AttributeTypeAndValue> atvs = new LinkedList<>();
for (AttributeTypeAndValue m : li) {
if (type.equals(m.getType())) {
atvs.add(m);
}
}
final int atvsSize = atvs.size();
int minOccurs = (rdnControl == null) ? 0 : rdnControl.getMinOccurs();
int maxOccurs = (rdnControl == null) ? 0 : rdnControl.getMaxOccurs();
if (atvsSize < minOccurs || atvsSize > maxOccurs) {
issue.setFailureMessage("number of AttributeTypeAndValuess '" + atvsSize
+ "' is not within [" + minOccurs + ", " + maxOccurs + "]");
return issue;
}
for (int i = 0; i < atvsSize; i++) {
AttributeTypeAndValue atv = atvs.get(i);
String atvTextValue = getAtvValueString("AttributeTypeAndValue[" + i + "]", atv,
stringType, failureMsg);
if (atvTextValue == null) {
continue;
}
checkAttributeTypeAndValue("AttributeTypeAndValue[" + i + "]", type, atvTextValue,
rdnControl, requestedCoreAtvTextValues, i, failureMsg);
}
int len = failureMsg.length();
if (len > 2) {
failureMsg.delete(len - 2, len);
issue.setFailureMessage(failureMsg.toString());
}
return issue;
} // method checkSubjectAttributeMultiValued
private void checkAttributeTypeAndValue(final String name, final ASN1ObjectIdentifier type,
final String atvTextValue, final RdnControl rdnControl,
final List<String> requestedCoreAtvTextValues, final int index,
final StringBuilder failureMsg) throws BadCertTemplateException {
String tmpAtvTextValue = atvTextValue;
if (ObjectIdentifiers.DN_DATE_OF_BIRTH.equals(type)) {
if (!SubjectDnSpec.PATTERN_DATE_OF_BIRTH.matcher(tmpAtvTextValue).matches()) {
throw new BadCertTemplateException(
"Value of RDN dateOfBirth does not have format YYYMMDD000000Z");
}
} else if (rdnControl != null) {
String prefix = rdnControl.getPrefix();
if (prefix != null) {
if (!tmpAtvTextValue.startsWith(prefix)) {
failureMsg.append(name).append(" '").append(tmpAtvTextValue)
.append("' does not start with prefix '").append(prefix).append("'; ");
return;
} else {
tmpAtvTextValue = tmpAtvTextValue.substring(prefix.length());
}
}
String suffix = rdnControl.getSuffix();
if (suffix != null) {
if (!tmpAtvTextValue.endsWith(suffix)) {
failureMsg.append(name).append(" '").append(tmpAtvTextValue)
.append("' does not end with suffix '").append(suffix).append("'; ");
return;
} else {
tmpAtvTextValue = tmpAtvTextValue.substring(0,
tmpAtvTextValue.length() - suffix.length());
}
}
List<Pattern> patterns = rdnControl.getPatterns();
if (patterns != null) {
Pattern pattern = patterns.get(index);
boolean matches = pattern.matcher(tmpAtvTextValue).matches();
if (!matches) {
failureMsg.append(name).append(" '").append(tmpAtvTextValue)
.append("' is not valid against regex '")
.append(pattern.pattern()).append("'; ");
return;
}
}
}
if (CollectionUtil.isEmpty(requestedCoreAtvTextValues)) {
if (!type.equals(ObjectIdentifiers.DN_SERIALNUMBER)) {
failureMsg.append("is present but not contained in the request");
failureMsg.append("; ");
}
} else {
String requestedCoreAtvTextValue = requestedCoreAtvTextValues.get(index);
if (ObjectIdentifiers.DN_CN.equals(type) && specialBehavior != null
&& SpecialX509CertprofileBehavior.gematik_gSMC_K.equals(specialBehavior)) {
if (!tmpAtvTextValue.startsWith(requestedCoreAtvTextValue + "-")) {
failureMsg.append("content '").append(tmpAtvTextValue)
.append("' does not start with '")
.append(requestedCoreAtvTextValue).append("-'; ");
}
} else if (!type.equals(ObjectIdentifiers.DN_SERIALNUMBER)) {
if (!tmpAtvTextValue.equals(requestedCoreAtvTextValue)) {
failureMsg.append("content '").append(tmpAtvTextValue)
.append("' but expected '").append(requestedCoreAtvTextValue).append("'; ");
}
}
}
} // mehtod checkAttributeTypeAndValue
private static List<String> sort(final List<String> contentList,
final List<Pattern> patternList) {
List<String> sorted = new ArrayList<>(contentList.size());
for (Pattern p : patternList) {
for (String value : contentList) {
if (!sorted.contains(value) && p.matcher(value).matches()) {
sorted.add(value);
}
}
}
for (String value : contentList) {
if (!sorted.contains(value)) {
sorted.add(value);
}
}
return sorted;
}
private static boolean matchStringType(final ASN1Encodable atvValue,
final StringType stringType) {
boolean correctStringType = true;
switch (stringType) {
case bmpString:
correctStringType = (atvValue instanceof DERBMPString);
break;
case printableString:
correctStringType = (atvValue instanceof DERPrintableString);
break;
case teletexString:
correctStringType = (atvValue instanceof DERT61String);
break;
case utf8String:
correctStringType = (atvValue instanceof DERUTF8String);
break;
case ia5String:
correctStringType = (atvValue instanceof DERIA5String);
break;
default:
throw new RuntimeException("should not reach here, unknown StringType " + stringType);
} // end switch
return correctStringType;
}
private static String getRdnTextValueOfRequest(final RDN requestedRdn)
throws BadCertTemplateException {
ASN1ObjectIdentifier type = requestedRdn.getFirst().getType();
ASN1Encodable vec = requestedRdn.getFirst().getValue();
if (ObjectIdentifiers.DN_DATE_OF_BIRTH.equals(type)) {
if (!(vec instanceof ASN1GeneralizedTime)) {
throw new BadCertTemplateException("requested RDN is not of GeneralizedTime");
}
return ((ASN1GeneralizedTime) vec).getTimeString();
} else if (ObjectIdentifiers.DN_POSTAL_ADDRESS.equals(type)) {
if (!(vec instanceof ASN1Sequence)) {
throw new BadCertTemplateException("requested RDN is not of Sequence");
}
ASN1Sequence seq = (ASN1Sequence) vec;
final int n = seq.size();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < n; i++) {
ASN1Encodable obj = seq.getObjectAt(i);
String textValue = X509Util.rdnValueToString(obj);
sb.append("[").append(i).append("]=").append(textValue).append(",");
}
return sb.toString();
} else {
return X509Util.rdnValueToString(vec);
}
}
private static ValidationIssue createSubjectIssue(final ASN1ObjectIdentifier subjectAttrType) {
ValidationIssue issue;
String attrName = ObjectIdentifiers.getName(subjectAttrType);
if (attrName == null) {
attrName = subjectAttrType.getId().replace('.', '_');
issue = new ValidationIssue("X509.SUBJECT." + attrName, "attribute "
+ subjectAttrType.getId());
} else {
issue = new ValidationIssue("X509.SUBJECT." + attrName, "attribute " + attrName
+ " (" + subjectAttrType.getId() + ")");
}
return issue;
}
private static String getAtvValueString(final String name, final AttributeTypeAndValue atv,
final StringType stringType, final StringBuilder failureMsg) {
ASN1ObjectIdentifier type = atv.getType();
ASN1Encodable atvValue = atv.getValue();
if (ObjectIdentifiers.DN_DATE_OF_BIRTH.equals(type)) {
if (!(atvValue instanceof ASN1GeneralizedTime)) {
failureMsg.append(name).append(" is not of type GeneralizedTime; ");
return null;
}
return ((ASN1GeneralizedTime) atvValue).getTimeString();
} else if (ObjectIdentifiers.DN_POSTAL_ADDRESS.equals(type)) {
if (!(atvValue instanceof ASN1Sequence)) {
failureMsg.append(name).append(" is not of type Sequence; ");
return null;
}
ASN1Sequence seq = (ASN1Sequence) atvValue;
final int n = seq.size();
StringBuilder sb = new StringBuilder();
boolean validEncoding = true;
for (int i = 0; i < n; i++) {
ASN1Encodable obj = seq.getObjectAt(i);
if (!matchStringType(obj, stringType)) {
failureMsg.append(name).append(".[").append(i).append("] is not of type ")
.append(stringType.name()).append("; ");
validEncoding = false;
break;
}
String textValue = X509Util.rdnValueToString(obj);
sb.append("[").append(i).append("]=").append(textValue).append(",");
}
if (!validEncoding) {
return null;
}
return sb.toString();
} else {
if (!matchStringType(atvValue, stringType)) {
failureMsg.append(name).append(" is not of type " + stringType.name()).append("; ");
return null;
}
return X509Util.rdnValueToString(atvValue);
}
} // method getAtvValueString
}