org.xipki.pki.ca.qa.SubjectChecker.java Source code

Java tutorial

Introduction

Here is the source code for org.xipki.pki.ca.qa.SubjectChecker.java

Source

/*
 *
 * Copyright (c) 2013 - 2016 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

}