org.xipki.ca.qa.impl.X509CertprofileQAImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.xipki.ca.qa.impl.X509CertprofileQAImpl.java

Source

/*
 *
 * This file is part of the XiPKI project.
 * Copyright (c) 2014 - 2015 Lijun Liao
 * Author: 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.ca.qa.impl;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.security.spec.InvalidKeySpecException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TimeZone;
import java.util.regex.Pattern;

import org.bouncycastle.asn1.ASN1Encodable;
import org.bouncycastle.asn1.ASN1EncodableVector;
import org.bouncycastle.asn1.ASN1Integer;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.ASN1Sequence;
import org.bouncycastle.asn1.ASN1StreamParser;
import org.bouncycastle.asn1.ASN1String;
import org.bouncycastle.asn1.ASN1TaggedObject;
import org.bouncycastle.asn1.DERBMPString;
import org.bouncycastle.asn1.DERIA5String;
import org.bouncycastle.asn1.DERPrintableString;
import org.bouncycastle.asn1.DERSequence;
import org.bouncycastle.asn1.DERT61String;
import org.bouncycastle.asn1.DERTaggedObject;
import org.bouncycastle.asn1.DERUTF8String;
import org.bouncycastle.asn1.isismtt.x509.AdmissionSyntax;
import org.bouncycastle.asn1.isismtt.x509.Admissions;
import org.bouncycastle.asn1.isismtt.x509.ProfessionInfo;
import org.bouncycastle.asn1.x500.AttributeTypeAndValue;
import org.bouncycastle.asn1.x500.DirectoryString;
import org.bouncycastle.asn1.x500.RDN;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x509.AccessDescription;
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
import org.bouncycastle.asn1.x509.AuthorityInformationAccess;
import org.bouncycastle.asn1.x509.AuthorityKeyIdentifier;
import org.bouncycastle.asn1.x509.BasicConstraints;
import org.bouncycastle.asn1.x509.CRLDistPoint;
import org.bouncycastle.asn1.x509.CertPolicyId;
import org.bouncycastle.asn1.x509.Certificate;
import org.bouncycastle.asn1.x509.DistributionPoint;
import org.bouncycastle.asn1.x509.DistributionPointName;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.asn1.x509.Extensions;
import org.bouncycastle.asn1.x509.GeneralName;
import org.bouncycastle.asn1.x509.GeneralNames;
import org.bouncycastle.asn1.x509.GeneralSubtree;
import org.bouncycastle.asn1.x509.KeyPurposeId;
import org.bouncycastle.asn1.x509.PolicyInformation;
import org.bouncycastle.asn1.x509.PolicyQualifierId;
import org.bouncycastle.asn1.x509.PolicyQualifierInfo;
import org.bouncycastle.asn1.x509.SubjectKeyIdentifier;
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
import org.bouncycastle.asn1.x509.UserNotice;
import org.bouncycastle.asn1.x509.X509ObjectIdentifiers;
import org.bouncycastle.asn1.x9.X9ECParameters;
import org.bouncycastle.jcajce.provider.asymmetric.util.ECUtil;
import org.bouncycastle.math.ec.ECCurve;
import org.bouncycastle.util.encoders.Hex;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xipki.ca.api.BadCertTemplateException;
import org.xipki.ca.api.CertprofileException;
import org.xipki.ca.api.profile.CertValidity;
import org.xipki.ca.api.profile.DirectoryStringType;
import org.xipki.ca.api.profile.ExtensionControl;
import org.xipki.ca.api.profile.GeneralNameMode;
import org.xipki.ca.api.profile.KeyParametersOption;
import org.xipki.ca.api.profile.KeyParametersOption.AllowAllParametersOption;
import org.xipki.ca.api.profile.KeyParametersOption.DSAParametersOption;
import org.xipki.ca.api.profile.KeyParametersOption.ECParamatersOption;
import org.xipki.ca.api.profile.KeyParametersOption.RSAParametersOption;
import org.xipki.ca.api.profile.KeyParametersOption.Range;
import org.xipki.ca.api.profile.RDNControl;
import org.xipki.ca.api.profile.x509.AuthorityInfoAccessControl;
import org.xipki.ca.api.profile.x509.ExtKeyUsageControl;
import org.xipki.ca.api.profile.x509.KeyUsageControl;
import org.xipki.ca.api.profile.x509.X509CertVersion;
import org.xipki.ca.api.profile.x509.X509Certprofile;
import org.xipki.ca.certprofile.SubjectDNOption;
import org.xipki.ca.certprofile.XmlX509CertprofileUtil;
import org.xipki.ca.certprofile.x509.jaxb.Admission;
import org.xipki.ca.certprofile.x509.jaxb.ConstantExtValue;
import org.xipki.ca.certprofile.x509.jaxb.ExtendedKeyUsage;
import org.xipki.ca.certprofile.x509.jaxb.ExtensionType;
import org.xipki.ca.certprofile.x509.jaxb.ExtensionsType;
import org.xipki.ca.certprofile.x509.jaxb.InhibitAnyPolicy;
import org.xipki.ca.certprofile.x509.jaxb.PolicyConstraints;
import org.xipki.ca.certprofile.x509.jaxb.PolicyMappings;
import org.xipki.ca.certprofile.x509.jaxb.RangeType;
import org.xipki.ca.certprofile.x509.jaxb.RangesType;
import org.xipki.ca.certprofile.x509.jaxb.RdnType;
import org.xipki.ca.certprofile.x509.jaxb.SubjectAltName;
import org.xipki.ca.certprofile.x509.jaxb.SubjectInfoAccess;
import org.xipki.ca.certprofile.x509.jaxb.SubjectInfoAccess.Access;
import org.xipki.ca.certprofile.x509.jaxb.X509ProfileType;
import org.xipki.ca.certprofile.x509.jaxb.X509ProfileType.Subject;
import org.xipki.ca.qa.api.X509CertprofileQA;
import org.xipki.ca.qa.api.X509IssuerInfo;
import org.xipki.ca.qa.impl.internal.QaAdmission;
import org.xipki.ca.qa.impl.internal.QaCertificatePolicies;
import org.xipki.ca.qa.impl.internal.QaCertificatePolicies.QaCertificatePolicyInformation;
import org.xipki.ca.qa.impl.internal.QaExtensionValue;
import org.xipki.ca.qa.impl.internal.QaGeneralSubtree;
import org.xipki.ca.qa.impl.internal.QaInhibitAnyPolicy;
import org.xipki.ca.qa.impl.internal.QaNameConstraints;
import org.xipki.ca.qa.impl.internal.QaPolicyConstraints;
import org.xipki.ca.qa.impl.internal.QaPolicyMappingsOption;
import org.xipki.ca.qa.impl.internal.QaPolicyQualifierInfo;
import org.xipki.ca.qa.impl.internal.QaPolicyQualifierInfo.QaCPSUriPolicyQualifier;
import org.xipki.ca.qa.impl.internal.QaPolicyQualifierInfo.QaUserNoticePolicyQualifierInfo;
import org.xipki.ca.qa.impl.internal.QaPolicyQualifiers;
import org.xipki.common.HashAlgoType;
import org.xipki.common.HashCalculator;
import org.xipki.common.KeyUsage;
import org.xipki.common.LruCache;
import org.xipki.common.ObjectIdentifiers;
import org.xipki.common.ParamChecker;
import org.xipki.common.qa.ValidationIssue;
import org.xipki.common.qa.ValidationResult;
import org.xipki.common.util.AlgorithmUtil;
import org.xipki.common.util.CollectionUtil;
import org.xipki.common.util.LogUtil;
import org.xipki.common.util.SecurityUtil;
import org.xipki.common.util.X509Util;
import org.xipki.security.api.ExtensionExistence;

/**
 * @author Lijun Liao
 */

public class X509CertprofileQAImpl implements X509CertprofileQA {
    private static final byte[] DERNull = new byte[] { 5, 0 };
    private static final Logger LOG = LoggerFactory.getLogger(X509CertprofileQAImpl.class);
    private static final TimeZone UTC = TimeZone.getTimeZone("UTC");
    private static final long SECOND = 1000L;

    private static final List<String> allUsages = Arrays.asList(KeyUsage.digitalSignature.getName(), // 0
            KeyUsage.contentCommitment.getName(), // 1
            KeyUsage.keyEncipherment.getName(), // 2
            KeyUsage.dataEncipherment.getName(), // 3
            KeyUsage.keyAgreement.getName(), // 4
            KeyUsage.keyCertSign.getName(), // 5
            KeyUsage.cRLSign.getName(), // 6
            KeyUsage.encipherOnly.getName(), // 7
            KeyUsage.decipherOnly.getName() // 8
    );

    private String specialBehavior;

    private Map<ASN1ObjectIdentifier, KeyParametersOption> keyAlgorithms;

    private Map<ASN1ObjectIdentifier, SubjectDNOption> subjectDNOptions;
    private Set<RDNControl> subjectDNControls;
    private Map<ASN1ObjectIdentifier, ExtensionControl> extensionControls;

    private CertValidity validity;
    private X509CertVersion version;
    private Set<String> signatureAlgorithms;
    private boolean ca;
    private boolean notBeforeMidnight;
    private Integer pathLen;
    private AuthorityInfoAccessControl aiaControl;
    private Set<KeyUsageControl> keyusages;
    private Set<ExtKeyUsageControl> extendedKeyusages;
    private Set<GeneralNameMode> allowedSubjectAltNameModes;
    private Map<ASN1ObjectIdentifier, Set<GeneralNameMode>> allowedSubjectInfoAccessModes;

    private boolean includeIssuerAndSerialInAKI;
    private QaCertificatePolicies certificatePolicies;
    private QaPolicyMappingsOption policyMappings;
    private QaNameConstraints nameConstraints;
    private QaPolicyConstraints policyConstraints;
    private QaInhibitAnyPolicy inhibitAnyPolicy;
    private QaAdmission admission;

    private Map<ASN1ObjectIdentifier, QaExtensionValue> constantExtensions;

    private static LruCache<ASN1ObjectIdentifier, Integer> ecCurveFieldSizes = new LruCache<>(100);

    public X509CertprofileQAImpl(final String data) throws CertprofileException {
        this(data.getBytes());
    }

    public X509CertprofileQAImpl(final byte[] dataBytes) throws CertprofileException {
        try {
            X509ProfileType conf = XmlX509CertprofileUtil.parse(new ByteArrayInputStream(dataBytes));

            this.version = X509CertVersion.getInstance(conf.getVersion());
            if (this.version == null) {
                throw new CertprofileException("invalid version " + conf.getVersion());
            }

            if (conf.getSignatureAlgorithms() != null) {
                this.signatureAlgorithms = new HashSet<>();
                for (String algo : conf.getSignatureAlgorithms().getAlgorithm()) {
                    String c14nAlgo;
                    try {
                        c14nAlgo = AlgorithmUtil.canonicalizeSignatureAlgo(algo);
                    } catch (NoSuchAlgorithmException e) {
                        throw new CertprofileException(e.getMessage(), e);
                    }
                    this.signatureAlgorithms.add(c14nAlgo);
                }
            }

            this.validity = CertValidity.getInstance(conf.getValidity());
            this.ca = conf.isCa();
            this.notBeforeMidnight = "midnight".equalsIgnoreCase(conf.getNotBeforeTime());
            this.specialBehavior = conf.getSpecialBehavior();
            if (this.specialBehavior != null && "gematik_gSMC_K".equalsIgnoreCase(this.specialBehavior) == false) {
                throw new CertprofileException("unknown special bahavior " + this.specialBehavior);
            }

            // KeyAlgorithms
            if (conf.getKeyAlgorithms() != null) {
                this.keyAlgorithms = XmlX509CertprofileUtil.buildKeyAlgorithms(conf.getKeyAlgorithms());
            }

            // Subject
            if (conf.getSubject() != null) {
                Subject subject = conf.getSubject();

                this.subjectDNControls = new HashSet<RDNControl>();
                this.subjectDNOptions = new HashMap<>();

                for (RdnType t : subject.getRdn()) {
                    DirectoryStringType directoryStringEnum = XmlX509CertprofileUtil
                            .convertDirectoryStringType(t.getDirectoryStringType());
                    ASN1ObjectIdentifier type = new ASN1ObjectIdentifier(t.getType().getValue());
                    RDNControl occ = new RDNControl(type, getInt(t.getMinOccurs(), 1), getInt(t.getMaxOccurs(), 1),
                            directoryStringEnum);
                    this.subjectDNControls.add(occ);

                    List<Pattern> patterns = null;
                    if (CollectionUtil.isNotEmpty(t.getRegex())) {
                        patterns = new LinkedList<>();
                        for (String regex : t.getRegex()) {
                            Pattern pattern = Pattern.compile(regex);
                            patterns.add(pattern);
                        }
                    }

                    SubjectDNOption option = new SubjectDNOption(t.getPrefix(), t.getSuffix(), patterns,
                            t.getMinLen(), t.getMaxLen());
                    this.subjectDNOptions.put(type, option);
                }
            }

            // Extensions
            ExtensionsType extensionsType = conf.getExtensions();

            // Extension controls
            this.extensionControls = XmlX509CertprofileUtil.buildExtensionControls(extensionsType);

            // BasicConstrains
            ASN1ObjectIdentifier type = Extension.basicConstraints;
            if (extensionControls.containsKey(type)) {
                org.xipki.ca.certprofile.x509.jaxb.BasicConstraints extConf = (org.xipki.ca.certprofile.x509.jaxb.BasicConstraints) getExtensionValue(
                        type, extensionsType);
                if (extConf != null) {
                    this.pathLen = extConf.getPathLen();
                }
            }

            // Extension KeyUsage
            type = Extension.keyUsage;
            if (extensionControls.containsKey(type)) {
                org.xipki.ca.certprofile.x509.jaxb.KeyUsage extConf = (org.xipki.ca.certprofile.x509.jaxb.KeyUsage) getExtensionValue(
                        type, extensionsType);
                if (extConf != null) {
                    this.keyusages = XmlX509CertprofileUtil.buildKeyUsageOptions(extConf);
                }
            }

            // ExtendedKeyUsage
            type = Extension.extendedKeyUsage;
            if (extensionControls.containsKey(type)) {
                ExtendedKeyUsage extConf = (ExtendedKeyUsage) getExtensionValue(type, extensionsType);
                if (extConf != null) {
                    this.extendedKeyusages = XmlX509CertprofileUtil.buildExtKeyUsageOptions(extConf);
                }
            }

            // AuthorityKeyIdentifier
            type = Extension.authorityKeyIdentifier;
            if (extensionControls.containsKey(type)) {
                org.xipki.ca.certprofile.x509.jaxb.AuthorityKeyIdentifier extConf = (org.xipki.ca.certprofile.x509.jaxb.AuthorityKeyIdentifier) getExtensionValue(
                        type, extensionsType);
                if (extConf != null) {
                    this.includeIssuerAndSerialInAKI = extConf.isIncludeIssuerAndSerial();
                }
            }

            // Certificate Policies
            type = Extension.certificatePolicies;
            if (extensionControls.containsKey(type)) {
                org.xipki.ca.certprofile.x509.jaxb.CertificatePolicies extConf = (org.xipki.ca.certprofile.x509.jaxb.CertificatePolicies) getExtensionValue(
                        type, extensionsType);
                if (extConf != null) {
                    this.certificatePolicies = new QaCertificatePolicies(extConf);
                }
            }

            // Policy Mappings
            type = Extension.policyMappings;
            if (extensionControls.containsKey(type)) {
                PolicyMappings extConf = (PolicyMappings) getExtensionValue(type, extensionsType);
                if (extConf != null) {
                    this.policyMappings = new QaPolicyMappingsOption(extConf);
                }
            }

            // Name Constrains
            // Name Constrains
            type = Extension.nameConstraints;
            if (extensionControls.containsKey(type)) {
                org.xipki.ca.certprofile.x509.jaxb.NameConstraints extConf = (org.xipki.ca.certprofile.x509.jaxb.NameConstraints) getExtensionValue(
                        type, extensionsType);
                if (extConf != null) {
                    this.nameConstraints = new QaNameConstraints(extConf);
                }
            }

            // Policy Constraints
            type = Extension.policyConstraints;
            if (extensionControls.containsKey(type)) {
                PolicyConstraints extConf = (PolicyConstraints) getExtensionValue(type, extensionsType);
                if (extConf != null) {
                    this.policyConstraints = new QaPolicyConstraints(extConf);
                }
            }

            // Inhibit anyPolicy
            type = Extension.inhibitAnyPolicy;
            if (extensionControls.containsKey(type)) {
                InhibitAnyPolicy extConf = (InhibitAnyPolicy) getExtensionValue(type, extensionsType);
                if (extConf != null) {
                    this.inhibitAnyPolicy = new QaInhibitAnyPolicy(extConf);
                }
            }

            // admission
            type = ObjectIdentifiers.id_extension_admission;
            if (extensionControls.containsKey(type)) {
                Admission extConf = (Admission) getExtensionValue(type, extensionsType);
                if (extConf != null) {
                    this.admission = new QaAdmission(extConf);
                }
            }

            // SubjectAltNameMode
            type = Extension.subjectAlternativeName;
            if (extensionControls.containsKey(type)) {
                SubjectAltName extConf = (SubjectAltName) getExtensionValue(type, extensionsType);
                if (extConf != null) {
                    this.allowedSubjectAltNameModes = XmlX509CertprofileUtil.buildGeneralNameMode(extConf);
                }
            }

            // SubjectInfoAccess
            type = Extension.subjectInfoAccess;
            if (extensionControls.containsKey(type)) {
                SubjectInfoAccess extConf = (SubjectInfoAccess) getExtensionValue(type, extensionsType);
                if (extConf != null) {
                    List<Access> list = extConf.getAccess();
                    this.allowedSubjectInfoAccessModes = new HashMap<>();
                    for (Access entry : list) {
                        this.allowedSubjectInfoAccessModes.put(
                                new ASN1ObjectIdentifier(entry.getAccessMethod().getValue()),
                                XmlX509CertprofileUtil.buildGeneralNameMode(entry.getAccessLocation()));
                    }
                }
            }

            // constant extensions
            this.constantExtensions = buildConstantExtesions(extensionsType);
        } catch (RuntimeException e) {
            final String message = "RuntimeException";
            if (LOG.isErrorEnabled()) {
                LOG.error(LogUtil.buildExceptionLogFormat(message), e.getClass().getName(), e.getMessage());
            }
            LOG.debug(message, e);
            throw new CertprofileException(
                    "RuntimeException thrown while initializing certprofile: " + e.getMessage());
        }
    }

    @Override
    public ValidationResult checkCert(final byte[] certBytes, final X509IssuerInfo issuerInfo,
            final X500Name requestedSubject, final SubjectPublicKeyInfo requestedPublicKey,
            final Extensions requestedExtensions) {
        ParamChecker.assertNotNull("certBytes", certBytes);
        ParamChecker.assertNotNull("issuerInfo", issuerInfo);
        ParamChecker.assertNotNull("requestedSubject", requestedSubject);
        ParamChecker.assertNotNull("requestedPublicKey", requestedPublicKey);

        List<ValidationIssue> resultIssues = new LinkedList<ValidationIssue>();

        Certificate bcCert;
        X509Certificate cert;

        // certificate encoding
        {
            ValidationIssue issue = new ValidationIssue("X509.ENCODING", "certificate encoding");
            resultIssues.add(issue);
            try {
                bcCert = Certificate.getInstance(certBytes);
                cert = X509Util.parseCert(certBytes);
            } catch (CertificateException | IOException e) {
                issue.setFailureMessage("certificate is not corrected encoded");
                return new ValidationResult(resultIssues);
            }
        }

        // syntax version
        {
            ValidationIssue issue = new ValidationIssue("X509.VERSION", "certificate version");
            resultIssues.add(issue);
            int versionNumber = cert.getVersion();
            if (versionNumber != version.getVersion()) {
                issue.setFailureMessage("is '" + versionNumber + "' but expected '" + version.getVersion() + "'");
            }
        }

        // signatureAlgorithm
        if (CollectionUtil.isNotEmpty(signatureAlgorithms)) {
            ValidationIssue issue = new ValidationIssue("X509.SIGALG", "signature algorithm");
            resultIssues.add(issue);

            AlgorithmIdentifier sigAlgId = bcCert.getSignatureAlgorithm();
            AlgorithmIdentifier tbsSigAlgId = bcCert.getTBSCertificate().getSignature();
            if (tbsSigAlgId.equals(sigAlgId) == false) {
                issue.setFailureMessage("Certificate.tbsCertificate.signature != Certificate.signatureAlgorithm");
            } else {
                try {
                    String sigAlgo = AlgorithmUtil.getSignatureAlgoName(sigAlgId);
                    if (signatureAlgorithms.contains(sigAlgo) == false) {
                        issue.setFailureMessage("signatureAlgorithm '" + sigAlgo + "' is not allowed");
                    }
                } catch (NoSuchAlgorithmException e) {
                    issue.setFailureMessage("unsupported signature algorithm " + sigAlgId.getAlgorithm().getId());
                }
            }
        }

        // notBefore
        if (notBeforeMidnight) {
            ValidationIssue issue = new ValidationIssue("X509.NOTBEFORE", "not before midnight");
            resultIssues.add(issue);
            Calendar c = Calendar.getInstance(UTC);
            c.setTime(cert.getNotBefore());
            int hourOfDay = c.get(Calendar.HOUR_OF_DAY);
            int minute = c.get(Calendar.MINUTE);
            int second = c.get(Calendar.SECOND);

            if (hourOfDay != 0 || minute != 0 || second != 0) {
                issue.setFailureMessage(" '" + cert.getNotBefore() + "' is not midnight time (UTC)");
            }
        }

        // validity
        {
            ValidationIssue issue = new ValidationIssue("X509.VALIDITY", "cert validity");
            resultIssues.add(issue);

            Date expectedNotAfter = validity.add(cert.getNotBefore());
            if (Math.abs(expectedNotAfter.getTime() - cert.getNotAfter().getTime()) > 60 * SECOND) {
                issue.setFailureMessage("cert validity is not within " + validity.toString());
            }
        }

        // public key
        {
            SubjectPublicKeyInfo publicKey = bcCert.getSubjectPublicKeyInfo();
            if (keyAlgorithms != null) {
                ValidationIssue issue = new ValidationIssue("X509.PUBKEY.SYN", "whether public key is permitted");
                resultIssues.add(issue);
                try {
                    checkPublicKey(publicKey);
                } catch (BadCertTemplateException e) {
                    issue.setFailureMessage(e.getMessage());
                }
            }

            ValidationIssue issue = new ValidationIssue("X509.PUBKEY.REQ",
                    "whether public key matches the request one");
            resultIssues.add(issue);
            SubjectPublicKeyInfo c14nRequestedPublicKey;
            try {
                c14nRequestedPublicKey = X509Util.toRfc3279Style(requestedPublicKey);
                if (c14nRequestedPublicKey.equals(publicKey) == false) {
                    issue.setFailureMessage("public key in the certificate does not equal the requested one");
                }
            } catch (InvalidKeySpecException e) {
                issue.setFailureMessage("public key in request is invalid");
            }
        }

        // Signature
        {
            ValidationIssue issue = new ValidationIssue("X509.SIG", "whether certificate is signed by CA");
            resultIssues.add(issue);
            try {
                cert.verify(issuerInfo.getCert().getPublicKey(), "BC");
            } catch (Exception e) {
                issue.setFailureMessage("invalid signature");
            }
        }

        // issuer
        {
            ValidationIssue issue = new ValidationIssue("X509.ISSUER", "certificate issuer");
            resultIssues.add(issue);
            if (cert.getIssuerX500Principal().equals(issuerInfo.getCert().getSubjectX500Principal()) == false) {
                issue.setFailureMessage("issue in certificate does not equal the subject of CA certificate");
            }
        }

        // subject
        X500Name subject = bcCert.getTBSCertificate().getSubject();
        resultIssues.addAll(checkSubject(subject, requestedSubject));

        // extensions
        resultIssues.addAll(checkExtensions(bcCert, cert, issuerInfo, requestedExtensions));

        return new ValidationResult(resultIssues);
    }

    private List<ValidationIssue> checkExtensions(final Certificate bcCert, final X509Certificate cert,
            final X509IssuerInfo issuerInfo, final Extensions requestExtensions) {
        List<ValidationIssue> result = new LinkedList<>();

        // detect the list of extension types in certificate
        Set<ASN1ObjectIdentifier> presentExtenionTypes = getExensionTypes(bcCert, issuerInfo, requestExtensions);

        Extensions extensions = bcCert.getTBSCertificate().getExtensions();
        ASN1ObjectIdentifier[] oids = extensions.getExtensionOIDs();

        if (oids == null) {
            ValidationIssue issue = new ValidationIssue("X509.EXT.GEN", "extension general");
            result.add(issue);
            issue.setFailureMessage("no extension is present");
            return result;
        }

        List<ASN1ObjectIdentifier> certExtTypes = Arrays.asList(oids);

        for (ASN1ObjectIdentifier extType : presentExtenionTypes) {
            if (certExtTypes.contains(extType) == false) {
                ValidationIssue issue = createExtensionIssue(extType);
                result.add(issue);
                issue.setFailureMessage("extension is absent but is required");
            }
        }

        for (ASN1ObjectIdentifier oid : certExtTypes) {
            ValidationIssue issue = createExtensionIssue(oid);
            result.add(issue);
            if (presentExtenionTypes.contains(oid) == false) {
                issue.setFailureMessage("extension is present but is not permitted");
                continue;
            }

            Extension ext = extensions.getExtension(oid);
            StringBuilder failureMsg = new StringBuilder();
            ExtensionControl extControl = extensionControls.get(oid);

            if (extControl.isCritical() != ext.isCritical()) {
                failureMsg.append(
                        "critical is '" + ext.isCritical() + "' but expected '" + extControl.isCritical() + "'");
                failureMsg.append("; ");
            }

            byte[] extensionValue = ext.getExtnValue().getOctets();

            try {
                if (Extension.authorityKeyIdentifier.equals(oid)) {
                    // AuthorityKeyIdentifier
                    checkExtensionIssuerKeyIdentifier(failureMsg, extensionValue, issuerInfo);
                } else if (Extension.subjectKeyIdentifier.equals(oid)) {
                    // SubjectKeyIdentifier
                    checkExtensionSubjectKeyIdentifier(failureMsg, extensionValue,
                            bcCert.getSubjectPublicKeyInfo());
                } else if (Extension.keyUsage.equals(oid)) {
                    // KeyUsage
                    checkExtensionKeyUsage(failureMsg, extensionValue, cert.getKeyUsage(), requestExtensions,
                            extControl);
                } else if (Extension.certificatePolicies.equals(oid)) {
                    // CertificatePolicies
                    checkExtensionCertificatePolicies(failureMsg, extensionValue, requestExtensions, extControl);
                } else if (Extension.policyMappings.equals(oid)) {
                    // Policy Mappings
                    checkExtensionPolicyMappings(failureMsg, extensionValue, requestExtensions, extControl);
                } else if (Extension.subjectAlternativeName.equals(oid)) {
                    // SubjectAltName
                    checkExtensionSubjectAltName(failureMsg, extensionValue, requestExtensions, extControl);
                } else if (Extension.issuerAlternativeName.equals(oid)) {
                    // IssuerAltName
                    checkExtensionIssuerAltNames(failureMsg, extensionValue, issuerInfo);
                } else if (Extension.basicConstraints.equals(oid)) {
                    // Basic Constraints
                    checkExtensionBasicConstraints(failureMsg, extensionValue);
                } else if (Extension.nameConstraints.equals(oid)) {
                    // Name Constraints
                    checkExtensionNameConstraints(failureMsg, extensionValue, extensions, extControl);
                } else if (Extension.policyConstraints.equals(oid)) {
                    // PolicyConstrains
                    checkExtensionPolicyConstraints(failureMsg, extensionValue, requestExtensions, extControl);
                } else if (Extension.extendedKeyUsage.equals(oid)) {
                    // ExtendedKeyUsage
                    checkExtensionExtendedKeyUsage(failureMsg, extensionValue, requestExtensions, extControl);
                } else if (Extension.cRLDistributionPoints.equals(oid)) {
                    // CRL Distribution Points
                    checkExtensionCrlDistributionPoints(failureMsg, extensionValue, issuerInfo);
                    continue;
                } else if (Extension.inhibitAnyPolicy.equals(oid)) {
                    // Inhibit anyPolicy
                    checkExtensionInhibitAnyPolicy(failureMsg, extensionValue, extensions, extControl);
                } else if (Extension.freshestCRL.equals(oid)) {
                    // Freshest CRL
                    checkExtensionDeltaCrlDistributionPoints(failureMsg, extensionValue, issuerInfo);
                } else if (Extension.authorityInfoAccess.equals(oid)) {
                    // Authority Information Access
                    checkExtensionAuthorityInfoAccess(failureMsg, extensionValue, issuerInfo);
                } else if (Extension.subjectInfoAccess.equals(oid)) {
                    // SubjectInfoAccess
                    checkExtensionSubjectInfoAccess(failureMsg, extensionValue, requestExtensions, extControl);
                } else if (ObjectIdentifiers.id_extension_admission.equals(oid)) {
                    // Admission
                    checkExtensionAdmission(failureMsg, extensionValue, requestExtensions, extControl);
                } else if (ObjectIdentifiers.id_extension_pkix_ocsp_nocheck.equals(oid)) {
                    // ocsp-nocheck
                    checkExtensionOcspNocheck(failureMsg, extensionValue);
                } else {
                    byte[] expected = getExpectedExtValue(oid, requestExtensions, extControl);
                    if (Arrays.equals(expected, extensionValue) == false) {
                        failureMsg.append("extension valus is '" + hex(extensionValue) + "' but expected '"
                                + (expected == null ? "not present" : hex(expected)) + "'");
                        failureMsg.append("; ");
                    }
                }

                if (failureMsg.length() > 0) {
                    issue.setFailureMessage(failureMsg.toString());
                }

            } catch (IllegalArgumentException | ClassCastException | ArrayIndexOutOfBoundsException e) {
                LOG.debug("extension value does not have correct syntax", e);
                issue.setFailureMessage("extension value does not have correct syntax");
            }
        }

        return result;
    }

    private byte[] getExpectedExtValue(final ASN1ObjectIdentifier type, final Extensions requestExtensions,
            final ExtensionControl extControl) {
        if (extControl.isRequest() && requestExtensions != null) {
            Extension reqExt = requestExtensions.getExtension(type);
            if (reqExt != null) {
                return reqExt.getExtnValue().getOctets();
            }
        } else if (constantExtensions != null && constantExtensions.containsKey(type)) {
            QaExtensionValue conf = constantExtensions.get(type);
            return conf.getValue();
        }

        return null;
    }

    private Set<ASN1ObjectIdentifier> getExensionTypes(final Certificate cert, final X509IssuerInfo issuerInfo,
            final Extensions requestedExtensions) {
        Set<ASN1ObjectIdentifier> types = new HashSet<>();
        // profile required extension types
        for (ASN1ObjectIdentifier oid : extensionControls.keySet()) {
            if (extensionControls.get(oid).isRequired()) {
                types.add(oid);
            }
        }

        Set<ASN1ObjectIdentifier> wantedExtensionTypes = new HashSet<>();

        if (requestedExtensions != null) {
            Extension reqExtension = requestedExtensions
                    .getExtension(ObjectIdentifiers.id_xipki_ext_cmpRequestExtensions);
            if (reqExtension != null) {
                ExtensionExistence ee = ExtensionExistence.getInstance(reqExtension.getParsedValue());
                types.addAll(ee.getNeedExtensions());
                wantedExtensionTypes.addAll(ee.getWantExtensions());
            }
        }

        if (CollectionUtil.isEmpty(wantedExtensionTypes)) {
            return types;
        }

        // wanted extension types
        // Authority key identifier
        ASN1ObjectIdentifier type = Extension.authorityKeyIdentifier;
        if (wantedExtensionTypes.contains(type)) {
            types.add(type);
        }

        // Subject key identifier
        type = Extension.subjectKeyIdentifier;
        if (wantedExtensionTypes.contains(type)) {
            types.add(type);
        }

        // KeyUsage
        type = Extension.keyUsage;
        if (wantedExtensionTypes.contains(type)) {
            boolean required = false;
            if (requestedExtensions.getExtension(type) != null) {
                required = true;
            }

            if (required == false) {
                Set<KeyUsageControl> requiredKeyusage = getKeyusage(true);
                if (CollectionUtil.isNotEmpty(requiredKeyusage)) {
                    required = true;
                }
            }

            if (required) {
                types.add(type);
            }
        }

        // CertificatePolicies
        type = Extension.certificatePolicies;
        if (wantedExtensionTypes.contains(type)) {
            if (certificatePolicies != null) {
                types.add(type);
            }
        }

        // Policy Mappings
        type = Extension.policyMappings;
        if (wantedExtensionTypes.contains(type)) {
            if (policyMappings != null) {
                types.add(type);
            }
        }

        // SubjectAltNames
        type = Extension.subjectAlternativeName;
        if (wantedExtensionTypes.contains(type)) {
            if (requestedExtensions.getExtension(type) != null) {
                types.add(type);
            }
        }

        // IssuerAltName
        type = Extension.issuerAlternativeName;
        if (wantedExtensionTypes.contains(type)) {
            if (cert.getTBSCertificate().getExtensions().getExtension(Extension.subjectAlternativeName) != null) {
                types.add(type);
            }
        }

        // BasicConstraints
        type = Extension.basicConstraints;
        if (wantedExtensionTypes.contains(type)) {
            types.add(type);
        }

        // Name Constraints
        type = Extension.nameConstraints;
        if (wantedExtensionTypes.contains(type)) {
            if (nameConstraints != null) {
                types.add(type);
            }
        }

        // PolicyConstrains
        type = Extension.policyConstraints;
        if (wantedExtensionTypes.contains(type)) {
            if (policyConstraints != null) {
                types.add(type);
            }
        }

        // ExtendedKeyUsage
        type = Extension.extendedKeyUsage;
        if (wantedExtensionTypes.contains(type)) {
            boolean required = false;
            if (requestedExtensions.getExtension(type) != null) {
                required = true;
            }

            if (required == false) {
                Set<ExtKeyUsageControl> requiredExtKeyusage = getExtKeyusage(true);
                if (CollectionUtil.isNotEmpty(requiredExtKeyusage)) {
                    required = true;
                }
            }

            if (required) {
                types.add(type);
            }
        }

        // CRLDistributionPoints
        type = Extension.cRLDistributionPoints;
        if (wantedExtensionTypes.contains(type)) {
            if (issuerInfo.getCrlURLs() != null) {
                types.add(type);
            }
        }

        // Inhibit anyPolicy
        type = Extension.inhibitAnyPolicy;
        if (wantedExtensionTypes.contains(type)) {
            if (inhibitAnyPolicy != null) {
                types.add(type);
            }
        }

        // FreshestCRL
        type = Extension.freshestCRL;
        if (wantedExtensionTypes.contains(type)) {
            if (issuerInfo.getDeltaCrlURLs() != null) {
                types.add(type);
            }
        }

        // AuthorityInfoAccess
        type = Extension.authorityInfoAccess;
        if (wantedExtensionTypes.contains(type)) {
            if (issuerInfo.getOcspURLs() != null) {
                types.add(type);
            }
        }

        // SubjectInfoAccess
        type = Extension.subjectInfoAccess;
        if (wantedExtensionTypes.contains(type)) {
            if (requestedExtensions.getExtension(type) != null) {
                types.add(type);
            }
        }

        // Admission
        type = ObjectIdentifiers.id_extension_admission;
        if (wantedExtensionTypes.contains(type)) {
            if (admission != null) {
                types.add(type);
            }
        }

        // ocsp-nocheck
        type = ObjectIdentifiers.id_extension_pkix_ocsp_nocheck;
        if (wantedExtensionTypes.contains(type)) {
            types.add(type);
        }

        wantedExtensionTypes.removeAll(types);

        for (ASN1ObjectIdentifier oid : wantedExtensionTypes) {
            if (requestedExtensions.getExtension(oid) != null) {
                if (constantExtensions.containsKey(oid)) {
                    types.add(oid);
                }
            }
        }

        return types;
    }

    private void checkPublicKey(final SubjectPublicKeyInfo publicKey) throws BadCertTemplateException {
        if (CollectionUtil.isEmpty(keyAlgorithms)) {
            return;
        }

        ASN1ObjectIdentifier keyType = publicKey.getAlgorithm().getAlgorithm();
        if (keyAlgorithms.containsKey(keyType) == false) {
            throw new BadCertTemplateException("key type " + keyType.getId() + " is not permitted");
        }

        KeyParametersOption keyParamsOption = keyAlgorithms.get(keyType);
        if (keyParamsOption instanceof AllowAllParametersOption) {
            return;
        } else if (keyParamsOption instanceof ECParamatersOption) {
            ECParamatersOption ecOption = (ECParamatersOption) keyParamsOption;
            // parameters
            ASN1Encodable algParam = publicKey.getAlgorithm().getParameters();
            ASN1ObjectIdentifier curveOid;

            if (algParam instanceof ASN1ObjectIdentifier) {
                curveOid = (ASN1ObjectIdentifier) algParam;
                if (ecOption.allowsCurve(curveOid) == false) {
                    throw new BadCertTemplateException("EC curve " + SecurityUtil.getCurveName(curveOid) + " (OID: "
                            + curveOid.getId() + ") is not allowed");
                }
            } else {
                throw new BadCertTemplateException("only namedCurve or implictCA EC public key is supported");
            }

            // point encoding
            if (ecOption.getPointEncodings() != null) {
                byte[] keyData = publicKey.getPublicKeyData().getBytes();
                if (keyData.length < 1) {
                    throw new BadCertTemplateException("invalid publicKeyData");
                }
                byte pointEncoding = keyData[0];
                if (ecOption.getPointEncodings().contains(pointEncoding) == false) {
                    throw new BadCertTemplateException("unaccepted EC point encoding " + pointEncoding);
                }
            }

            try {
                checkECSubjectPublicKeyInfo(curveOid, publicKey.getPublicKeyData().getBytes());
            } catch (BadCertTemplateException e) {
                throw e;
            } catch (Exception e) {
                LOG.debug("populateFromPubKeyInfo", e);
                throw new BadCertTemplateException("invalid public key: " + e.getMessage());
            }

            return;
        } else if (keyParamsOption instanceof RSAParametersOption) {
            RSAParametersOption rsaOption = (RSAParametersOption) keyParamsOption;

            ASN1Integer modulus;
            try {
                ASN1Sequence seq = ASN1Sequence.getInstance(publicKey.getPublicKeyData().getBytes());
                modulus = ASN1Integer.getInstance(seq.getObjectAt(0));
            } catch (IllegalArgumentException e) {
                throw new BadCertTemplateException("invalid publicKeyData");
            }

            int modulusLength = modulus.getPositiveValue().bitLength();
            if ((rsaOption.allowsModulusLength(modulusLength))) {
                return;
            }
        } else if (keyParamsOption instanceof DSAParametersOption) {
            DSAParametersOption dsaOption = (DSAParametersOption) keyParamsOption;
            ASN1Encodable params = publicKey.getAlgorithm().getParameters();
            if (params == null) {
                throw new BadCertTemplateException("null Dss-Parms is not permitted");
            }

            int pLength;
            int qLength;

            try {
                ASN1Sequence seq = ASN1Sequence.getInstance(params);
                ASN1Integer p = ASN1Integer.getInstance(seq.getObjectAt(0));
                ASN1Integer q = ASN1Integer.getInstance(seq.getObjectAt(1));
                pLength = p.getPositiveValue().bitLength();
                qLength = q.getPositiveValue().bitLength();
            } catch (IllegalArgumentException | ArrayIndexOutOfBoundsException e) {
                throw new BadCertTemplateException("illegal Dss-Parms");
            }

            boolean match = dsaOption.allowsPLength(pLength);
            if (match) {
                match = dsaOption.allowsQLength(qLength);
            }

            if (match) {
                return;
            }
        } else {
            throw new RuntimeException("should not reach here, unknown keyParamsOption "
                    + (keyParamsOption == null ? "null" : keyParamsOption.getClass().getName()));
        }

        throw new BadCertTemplateException("the given publicKey is not permitted");
    }

    private static void checkECSubjectPublicKeyInfo(final ASN1ObjectIdentifier curveOid, final byte[] encoded)
            throws BadCertTemplateException {
        Integer expectedLength = ecCurveFieldSizes.get(curveOid);
        if (expectedLength == null) {
            X9ECParameters ecP = ECUtil.getNamedCurveByOid(curveOid);
            ECCurve curve = ecP.getCurve();
            expectedLength = (curve.getFieldSize() + 7) / 8;
            ecCurveFieldSizes.put(curveOid, expectedLength);
        }

        switch (encoded[0]) {
        case 0x02: // compressed
        case 0x03: // compressed
        {
            if (encoded.length != (expectedLength + 1)) {
                throw new BadCertTemplateException("incorrect length for compressed encoding");
            }
            break;
        }
        case 0x04: // uncompressed
        case 0x06: // hybrid
        case 0x07: // hybrid
        {
            if (encoded.length != (2 * expectedLength + 1)) {
                throw new BadCertTemplateException("incorrect length for uncompressed/hybrid encoding");
            }
            break;
        }
        default:
            throw new BadCertTemplateException("invalid point encoding 0x" + Integer.toString(encoded[0], 16));
        }// end switch
    }

    private List<ValidationIssue> checkSubject(final X500Name subject, final X500Name requestedSubject) {
        // collect subject attribute types to check
        Set<ASN1ObjectIdentifier> oids = new HashSet<>();

        for (ASN1ObjectIdentifier oid : subjectDNOptions.keySet()) {
            oids.add(oid);
        }

        for (ASN1ObjectIdentifier oid : subject.getAttributeTypes()) {
            oids.add(oid);
        }

        List<ValidationIssue> result = new LinkedList<>();
        for (ASN1ObjectIdentifier type : oids) {
            ValidationIssue issue = checkSubjectAttribute(type, subject, requestedSubject);
            result.add(issue);
        }

        return result;
    }

    private ValidationIssue checkSubjectAttribute(final ASN1ObjectIdentifier type, final X500Name subject,
            final X500Name requestedSubject) {
        ValidationIssue issue = createSubjectIssue(type);

        // control
        int minOccurs;
        int maxOccurs;
        RDNControl rdnControl = getSubjectDNControl(type);
        if (rdnControl == null) {
            minOccurs = 0;
            maxOccurs = 0;
        } else {
            minOccurs = rdnControl.getMinOccurs();
            maxOccurs = 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;
        }

        SubjectDNOption rdnOption = subjectDNOptions.get(type);

        // check the encoding
        DirectoryStringType stringType = rdnControl.getDirectoryStringEnum();
        if (stringType == null) {
            if (ObjectIdentifiers.DN_C.equals(type) || ObjectIdentifiers.DN_SERIALNUMBER.equals(type)) {
                stringType = DirectoryStringType.printableString;
            } else {
                stringType = DirectoryStringType.utf8String;
            }
        }

        List<String> requestedCoreAtvTextValues = new LinkedList<>();
        if (requestedRdns != null) {
            for (RDN requestedRdn : requestedRdns) {
                String textValue = X509Util.rdnValueToString(requestedRdn.getFirst().getValue());
                requestedCoreAtvTextValues.add(textValue);
            }

            if (rdnOption != null && rdnOption.getPatterns() != null) {
                // sort the requestedRDNs
                requestedCoreAtvTextValues = sort(requestedCoreAtvTextValues, rdnOption.getPatterns());
            }
        }

        StringBuilder failureMsg = new StringBuilder();
        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;
            }

            ASN1Encodable atvValue = atvs[0].getValue();
            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;
            default:
                throw new RuntimeException("should not reach here, unknown DirectoryStringType " + stringType);
            } // end switch

            if (correctStringType == false) {
                failureMsg.append("RDN + [" + i + "] is not of type DirectoryString." + stringType.name());
                failureMsg.append("; ");
                continue;
            }

            String atvTextValue = X509Util.rdnValueToString(atvValue);
            String coreAtvTextValue = atvTextValue;

            if (rdnOption != null) {
                String prefix = rdnOption.getPrefix();
                if (prefix != null) {
                    if (coreAtvTextValue.startsWith(prefix) == false) {
                        failureMsg.append("RDN + [" + i + "] '" + atvTextValue + "' does not start with prefix '"
                                + prefix + "'");
                        failureMsg.append("; ");
                        continue;
                    } else {
                        coreAtvTextValue = coreAtvTextValue.substring(prefix.length());
                    }
                }

                String suffix = rdnOption.getSufix();
                if (suffix != null) {
                    if (coreAtvTextValue.endsWith(suffix) == false) {
                        failureMsg.append("RDN + [" + i + "] '" + atvTextValue + "' does not end with suffx '"
                                + suffix + "'");
                        failureMsg.append("; ");
                        continue;
                    } else {
                        coreAtvTextValue = coreAtvTextValue.substring(0,
                                coreAtvTextValue.length() - suffix.length());
                    }
                }

                List<Pattern> patterns = rdnOption.getPatterns();
                if (patterns != null) {
                    Pattern pattern = patterns.get(i);
                    boolean matches = pattern.matcher(coreAtvTextValue).matches();
                    if (matches == false) {
                        failureMsg.append("RDN + [" + i + "] '" + coreAtvTextValue
                                + "' is not valid against regex '" + pattern.pattern() + "'");
                        failureMsg.append("; ");
                        continue;
                    }
                }
            }

            if (CollectionUtil.isEmpty(requestedCoreAtvTextValues)) {
                if (type.equals(ObjectIdentifiers.DN_SERIALNUMBER) == false) {
                    failureMsg.append("is present but not contained in the request");
                    failureMsg.append("; ");
                }
            } else {
                String requestedCoreAtvTextValue = requestedCoreAtvTextValues.get(i);
                if (ObjectIdentifiers.DN_CN.equals(type) && specialBehavior != null
                        && "gematik_gSMC_K".equals(specialBehavior)) {
                    if (coreAtvTextValue.startsWith(requestedCoreAtvTextValue + "-") == false) {
                        failureMsg.append("content '" + coreAtvTextValue + "' does not start with '"
                                + requestedCoreAtvTextValue + "-'");
                        failureMsg.append("; ");
                    }
                } else if (type.equals(ObjectIdentifiers.DN_SERIALNUMBER)) {
                } else {
                    if (coreAtvTextValue.equals(requestedCoreAtvTextValue) == false) {
                        failureMsg.append("content '" + coreAtvTextValue + "' but expected '"
                                + requestedCoreAtvTextValue + "'");
                        failureMsg.append("; ");
                    }
                }
            }
        }

        int n = failureMsg.length();
        if (n > 2) {
            failureMsg.delete(n - 2, n);
            issue.setFailureMessage(failureMsg.toString());
        }

        return issue;
    }

    private static int getInt(final Integer i, final int dfltValue) {
        return i == null ? dfltValue : i.intValue();
    }

    private 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,
                    "extension " + attrName + " (" + subjectAttrType.getId() + ")");
        }
        return issue;
    }

    private ValidationIssue createExtensionIssue(ASN1ObjectIdentifier extId) {
        ValidationIssue issue;
        String extName = ObjectIdentifiers.getName(extId);
        if (extName == null) {
            extName = extId.getId().replace('.', '_');
            issue = new ValidationIssue("X509.EXT." + extName, "extension " + extId.getId());
        } else {
            issue = new ValidationIssue("X509.EXT." + extName, "extension " + extName + " (" + extId.getId() + ")");
        }
        return issue;
    }

    private void checkExtensionBasicConstraints(final StringBuilder failureMsg, final byte[] extensionValue) {
        BasicConstraints bc = BasicConstraints.getInstance(extensionValue);
        if (ca != bc.isCA()) {
            failureMsg.append("ca is '" + bc.isCA() + "' but expected '" + ca + "'");
            failureMsg.append("; ");
        }

        if (bc.isCA()) {
            BigInteger _pathLen = bc.getPathLenConstraint();
            if (pathLen == null) {
                if (_pathLen != null) {
                    failureMsg.append("pathLen is '" + _pathLen + "' but expected 'absent'");
                    failureMsg.append("; ");
                }
            } else {
                if (_pathLen == null) {
                    failureMsg.append("pathLen is 'null' but expected '" + pathLen + "'");
                    failureMsg.append("; ");
                } else if (BigInteger.valueOf(pathLen).equals(_pathLen) == false) {
                    failureMsg.append("pathLen is '" + _pathLen + "' but expected '" + pathLen + "'");
                    failureMsg.append("; ");
                }
            }
        }
    }

    private void checkExtensionSubjectKeyIdentifier(final StringBuilder failureMsg, final byte[] extensionValue,
            final SubjectPublicKeyInfo subjectPublicKeyInfo) {
        // subjectKeyIdentifier
        SubjectKeyIdentifier asn1 = SubjectKeyIdentifier.getInstance(extensionValue);
        byte[] ski = asn1.getKeyIdentifier();
        byte[] pkData = subjectPublicKeyInfo.getPublicKeyData().getBytes();
        byte[] expectedSki = HashCalculator.hash(HashAlgoType.SHA1, pkData);
        if (Arrays.equals(expectedSki, ski) == false) {
            failureMsg.append("SKI is '" + hex(ski) + "' but expected is '" + hex(expectedSki) + "'");
            failureMsg.append("; ");
        }
    }

    private void checkExtensionIssuerKeyIdentifier(final StringBuilder failureMsg, final byte[] extensionValue,
            final X509IssuerInfo issuerInfo) {
        AuthorityKeyIdentifier asn1 = AuthorityKeyIdentifier.getInstance(extensionValue);
        byte[] keyIdentifier = asn1.getKeyIdentifier();
        if (keyIdentifier == null) {
            failureMsg.append("keyIdentifier is 'absent' but expected 'present'");
            failureMsg.append("; ");
        } else if (Arrays.equals(issuerInfo.getSubjectKeyIdentifier(), keyIdentifier) == false) {
            failureMsg.append("keyIdentifier is '" + hex(keyIdentifier) + "' but expected '"
                    + hex(issuerInfo.getSubjectKeyIdentifier()) + "'");
            failureMsg.append("; ");
        }

        BigInteger serialNumber = asn1.getAuthorityCertSerialNumber();
        GeneralNames names = asn1.getAuthorityCertIssuer();

        if (includeIssuerAndSerialInAKI) {
            if (serialNumber == null) {
                failureMsg.append("authorityCertSerialNumber is 'absent' but expected 'present'");
                failureMsg.append("; ");
            } else {
                if (issuerInfo.getCert().getSerialNumber().equals(serialNumber) == false) {
                    failureMsg.append("authorityCertSerialNumber is '" + serialNumber + "' but expected '"
                            + issuerInfo.getCert().getSerialNumber() + "'");
                    failureMsg.append("; ");
                }
            }

            if (names == null) {
                failureMsg.append("authorityCertIssuer is 'absent' but expected 'present'");
                failureMsg.append("; ");
            } else {
                GeneralName[] genNames = names.getNames();
                X500Name x500GenName = null;
                for (GeneralName genName : genNames) {
                    if (genName.getTagNo() != GeneralName.directoryName) {
                        continue;
                    }

                    if (x500GenName != null) {
                        failureMsg.append(
                                "authorityCertIssuer contains at least two directoryName " + "but expected one");
                        failureMsg.append("; ");
                        break;
                    } else {
                        x500GenName = (X500Name) genName.getName();
                    }
                }

                if (x500GenName == null) {
                    failureMsg.append("authorityCertIssuer does not contain directoryName but expected one");
                    failureMsg.append("; ");
                } else {
                    X500Name caSubject = issuerInfo.getBcCert().getTBSCertificate().getSubject();
                    if (caSubject.equals(x500GenName) == false) {
                        failureMsg.append("authorityCertIssuer is '" + x500GenName.toString() + "' but expected '"
                                + caSubject.toString() + "'");
                        failureMsg.append("; ");
                    }
                }
            }
        } else {
            if (serialNumber != null) {
                failureMsg.append("authorityCertSerialNumber is 'absent' but expected 'present'");
                failureMsg.append("; ");
            }

            if (names != null) {
                failureMsg.append("authorityCertIssuer is 'absent' but expected 'present'");
                failureMsg.append("; ");
            }
        }
    }

    private void checkExtensionNameConstraints(final StringBuilder failureMsg, final byte[] extensionValue,
            final Extensions requestExtensions, final ExtensionControl extControl) {
        QaNameConstraints conf = nameConstraints;

        if (conf == null) {
            byte[] expected = getExpectedExtValue(Extension.nameConstraints, requestExtensions, extControl);
            if (Arrays.equals(expected, extensionValue) == false) {
                failureMsg.append("extension valus is '" + hex(extensionValue) + "' but expected '"
                        + (expected == null ? "not present" : hex(expected)) + "'");
                failureMsg.append("; ");
            }
            return;
        }

        org.bouncycastle.asn1.x509.NameConstraints iNameConstraints = org.bouncycastle.asn1.x509.NameConstraints
                .getInstance(extensionValue);

        checkExtensionNameConstraintsSubtrees(failureMsg, "PermittedSubtrees",
                iNameConstraints.getPermittedSubtrees(), conf.getPermittedSubtrees());
        checkExtensionNameConstraintsSubtrees(failureMsg, "ExcludedSubtrees",
                iNameConstraints.getExcludedSubtrees(), conf.getExcludedSubtrees());
    }

    private void checkExtensionNameConstraintsSubtrees(final StringBuilder failureMsg, final String description,
            final GeneralSubtree[] subtrees, final List<QaGeneralSubtree> expectedSubtrees) {
        int iSize = subtrees == null ? 0 : subtrees.length;
        int eSize = expectedSubtrees == null ? 0 : expectedSubtrees.size();
        if (iSize != eSize) {
            failureMsg.append("size of " + description + " is '" + iSize + "' but expected '" + eSize + "'");
            failureMsg.append("; ");
            return;
        }

        for (int i = 0; i < iSize; i++) {
            GeneralSubtree iSubtree = subtrees[i];
            QaGeneralSubtree eSubtree = expectedSubtrees.get(i);
            BigInteger bigInt = iSubtree.getMinimum();
            int iMinimum = bigInt == null ? 0 : bigInt.intValue();
            Integer _int = eSubtree.getMinimum();
            int eMinimum = _int == null ? 0 : _int.intValue();
            String desc = description + " [" + i + "]";
            if (iMinimum != eMinimum) {
                failureMsg.append("minimum of " + desc + " is '" + iMinimum + "' but expected '" + eMinimum + "'");
                failureMsg.append("; ");
            }

            bigInt = iSubtree.getMaximum();
            Integer iMaximum = bigInt == null ? null : bigInt.intValue();
            Integer eMaximum = eSubtree.getMaximum();
            if (iMaximum != eMaximum) {
                failureMsg.append("maxmum of " + desc + " is '" + iMaximum + "' but expected '" + eMaximum + "'");
                failureMsg.append("; ");
            }

            GeneralName iBase = iSubtree.getBase();

            GeneralName eBase;
            if (eSubtree.getDirectoryName() != null) {
                eBase = new GeneralName(X509Util.reverse(new X500Name(eSubtree.getDirectoryName())));
            } else if (eSubtree.getDNSName() != null) {
                eBase = new GeneralName(GeneralName.dNSName, eSubtree.getDNSName());
            } else if (eSubtree.getIpAddress() != null) {
                eBase = new GeneralName(GeneralName.iPAddress, eSubtree.getIpAddress());
            } else if (eSubtree.getRfc822Name() != null) {
                eBase = new GeneralName(GeneralName.rfc822Name, eSubtree.getRfc822Name());
            } else if (eSubtree.getUri() != null) {
                eBase = new GeneralName(GeneralName.uniformResourceIdentifier, eSubtree.getUri());
            } else {
                throw new RuntimeException("should not reach here, unknown child of GeneralName");
            }

            if (iBase.equals(eBase) == false) {
                failureMsg.append("base of " + desc + " is '" + iBase + "' but expected '" + eBase + "'");
                failureMsg.append("; ");
            }
        }
    }

    private void checkExtensionPolicyConstraints(final StringBuilder failureMsg, final byte[] extensionValue,
            final Extensions requestExtensions, final ExtensionControl extControl) {
        QaPolicyConstraints conf = policyConstraints;
        if (conf == null) {
            byte[] expected = getExpectedExtValue(Extension.policyConstraints, requestExtensions, extControl);
            if (Arrays.equals(expected, extensionValue) == false) {
                failureMsg.append("extension valus is '" + hex(extensionValue) + "' but expected '"
                        + (expected == null ? "not present" : hex(expected)) + "'");
                failureMsg.append("; ");
            }
            return;
        }

        org.bouncycastle.asn1.x509.PolicyConstraints iPolicyConstraints = org.bouncycastle.asn1.x509.PolicyConstraints
                .getInstance(extensionValue);
        Integer eRequireExplicitPolicy = conf.getRequireExplicitPolicy();
        BigInteger bigInt = iPolicyConstraints.getRequireExplicitPolicyMapping();
        Integer iRequreExplicitPolicy = bigInt == null ? null : bigInt.intValue();

        boolean match = true;
        if (eRequireExplicitPolicy == null) {
            if (iRequreExplicitPolicy != null) {
                match = false;
            }
        } else if (eRequireExplicitPolicy.equals(iRequreExplicitPolicy) == false) {
            match = false;
        }

        if (match == false) {
            failureMsg.append("requreExplicitPolicy is '" + iRequreExplicitPolicy + "' but expected '"
                    + eRequireExplicitPolicy + "'");
            failureMsg.append("; ");
        }

        Integer eInhibitPolicyMapping = conf.getInhibitPolicyMapping();
        bigInt = iPolicyConstraints.getInhibitPolicyMapping();
        Integer iInhibitPolicyMapping = bigInt == null ? null : bigInt.intValue();

        match = true;
        if (eInhibitPolicyMapping == null) {
            if (iInhibitPolicyMapping != null) {
                match = false;
            }
        } else if (eInhibitPolicyMapping.equals(iInhibitPolicyMapping) == false) {
            match = false;
        }

        if (match == false) {
            failureMsg.append("inhibitPolicyMapping is '" + iInhibitPolicyMapping + "' but expected '"
                    + eInhibitPolicyMapping + "'");
            failureMsg.append("; ");
        }
    }

    private void checkExtensionKeyUsage(final StringBuilder failureMsg, final byte[] extensionValue,
            final boolean[] usages, final Extensions requestExtensions, final ExtensionControl extControl) {
        int n = usages.length;

        if (n > 9) {
            failureMsg.append("invalid syntax: size of valid bits is larger than 9: " + n);
            failureMsg.append("; ");
        }

        Set<String> isUsages = new HashSet<>();
        for (int i = 0; i < n; i++) {
            if (usages[i]) {
                isUsages.add(allUsages.get(i));
            }
        }

        Set<String> expectedUsages = new HashSet<>();
        Set<KeyUsageControl> requiredKeyusage = getKeyusage(true);
        for (KeyUsageControl usage : requiredKeyusage) {
            expectedUsages.add(usage.getKeyUsage().getName());
        }

        Set<KeyUsageControl> optionalKeyusage = getKeyusage(false);
        if (extControl.isRequest() && requestExtensions != null && CollectionUtil.isNotEmpty(optionalKeyusage)) {
            Extension extension = requestExtensions.getExtension(Extension.keyUsage);
            if (extension != null) {
                org.bouncycastle.asn1.x509.KeyUsage reqKeyUsage = org.bouncycastle.asn1.x509.KeyUsage
                        .getInstance(extension.getParsedValue());
                for (KeyUsageControl k : optionalKeyusage) {
                    if (reqKeyUsage.hasUsages(k.getKeyUsage().getBcUsage())) {
                        expectedUsages.add(k.getKeyUsage().getName());
                    }
                }
            }
        }

        if (CollectionUtil.isEmpty(expectedUsages)) {
            byte[] constantExtValue = getConstantExtensionValue(Extension.keyUsage);
            if (constantExtValue != null) {
                expectedUsages = getKeyUsage(constantExtValue);
            }
        }

        Set<String> diffs = str_in_b_not_in_a(expectedUsages, isUsages);
        if (CollectionUtil.isNotEmpty(diffs)) {
            failureMsg.append("usages " + diffs.toString() + " are present but not expected");
            failureMsg.append("; ");
        }

        diffs = str_in_b_not_in_a(isUsages, expectedUsages);
        if (CollectionUtil.isNotEmpty(diffs)) {
            failureMsg.append("usages " + diffs.toString() + " are absent but are required");
            failureMsg.append("; ");
        }
    }

    private void checkExtensionExtendedKeyUsage(final StringBuilder failureMsg, final byte[] extensionValue,
            final Extensions requestExtensions, final ExtensionControl extControl) {
        Set<String> isUsages = new HashSet<>();
        {
            org.bouncycastle.asn1.x509.ExtendedKeyUsage keyusage = org.bouncycastle.asn1.x509.ExtendedKeyUsage
                    .getInstance(extensionValue);
            KeyPurposeId[] usages = keyusage.getUsages();
            if (usages != null) {
                for (KeyPurposeId usage : usages) {
                    isUsages.add(usage.getId());
                }
            }
        }

        Set<String> expectedUsages = new HashSet<>();
        Set<ExtKeyUsageControl> requiredExtKeyusage = getExtKeyusage(true);
        if (requiredExtKeyusage != null) {
            for (ExtKeyUsageControl usage : requiredExtKeyusage) {
                expectedUsages.add(usage.getExtKeyUsage().getId());
            }
        }

        Set<ExtKeyUsageControl> optionalExtKeyusage = getExtKeyusage(false);
        if (extControl.isRequest() && requestExtensions != null && CollectionUtil.isNotEmpty(optionalExtKeyusage)) {
            Extension extension = requestExtensions.getExtension(Extension.extendedKeyUsage);
            if (extension != null) {
                org.bouncycastle.asn1.x509.ExtendedKeyUsage reqKeyUsage = org.bouncycastle.asn1.x509.ExtendedKeyUsage
                        .getInstance(extension.getParsedValue());
                for (ExtKeyUsageControl k : optionalExtKeyusage) {
                    if (reqKeyUsage.hasKeyPurposeId(KeyPurposeId.getInstance(k.getExtKeyUsage()))) {
                        expectedUsages.add(k.getExtKeyUsage().getId());
                    }
                }
            }
        }

        if (CollectionUtil.isEmpty(expectedUsages)) {
            byte[] constantExtValue = getConstantExtensionValue(Extension.keyUsage);
            if (constantExtValue != null) {
                expectedUsages = getExtKeyUsage(constantExtValue);
            }
        }

        Set<String> diffs = str_in_b_not_in_a(expectedUsages, isUsages);
        if (CollectionUtil.isNotEmpty(diffs)) {
            failureMsg.append("usages " + diffs.toString() + " are present but not expected");
            failureMsg.append("; ");
        }

        diffs = str_in_b_not_in_a(isUsages, expectedUsages);
        if (CollectionUtil.isNotEmpty(diffs)) {
            failureMsg.append("usages " + diffs.toString() + " are absent but are required");
            failureMsg.append("; ");
        }
    }

    private void checkExtensionCertificatePolicies(final StringBuilder failureMsg, final byte[] extensionValue,
            final Extensions requestExtensions, final ExtensionControl extControl) {
        QaCertificatePolicies conf = certificatePolicies;
        if (conf == null) {
            byte[] expected = getExpectedExtValue(Extension.certificatePolicies, requestExtensions, extControl);
            if (Arrays.equals(expected, extensionValue) == false) {
                failureMsg.append("extension valus is '" + hex(extensionValue) + "' but expected '"
                        + (expected == null ? "not present" : hex(expected)) + "'");
                failureMsg.append("; ");
            }
            return;
        }

        org.bouncycastle.asn1.x509.CertificatePolicies asn1 = org.bouncycastle.asn1.x509.CertificatePolicies
                .getInstance(extensionValue);
        PolicyInformation[] iPolicyInformations = asn1.getPolicyInformation();

        for (PolicyInformation iPolicyInformation : iPolicyInformations) {
            ASN1ObjectIdentifier iPolicyId = iPolicyInformation.getPolicyIdentifier();
            QaCertificatePolicyInformation eCp = conf.getPolicyInformation(iPolicyId.getId());
            if (eCp == null) {
                failureMsg.append("certificate policy '" + iPolicyId + "' is not expected");
                failureMsg.append("; ");
                continue;
            }

            QaPolicyQualifiers eCpPq = eCp.getPolicyQualifiers();
            if (eCpPq == null) {
                continue;
            }

            ASN1Sequence iPolicyQualifiers = iPolicyInformation.getPolicyQualifiers();
            List<String> iCpsUris = new LinkedList<>();
            List<String> iUserNotices = new LinkedList<>();

            int n = iPolicyQualifiers.size();
            for (int i = 0; i < n; i++) {
                PolicyQualifierInfo iPolicyQualifierInfo = (PolicyQualifierInfo) iPolicyQualifiers.getObjectAt(i);
                ASN1ObjectIdentifier iPolicyQualifierId = iPolicyQualifierInfo.getPolicyQualifierId();
                ASN1Encodable iQualifier = iPolicyQualifierInfo.getQualifier();
                if (PolicyQualifierId.id_qt_cps.equals(iPolicyQualifierId)) {
                    String iCpsUri = ((DERIA5String) iQualifier).getString();
                    iCpsUris.add(iCpsUri);
                } else if (PolicyQualifierId.id_qt_unotice.equals(iPolicyQualifierId)) {
                    UserNotice iUserNotice = UserNotice.getInstance(iQualifier);
                    if (iUserNotice.getExplicitText() != null) {
                        iUserNotices.add(iUserNotice.getExplicitText().getString());
                    }
                }
            }

            List<QaPolicyQualifierInfo> qualifierInfos = eCpPq.getPolicyQualifiers();
            for (QaPolicyQualifierInfo qualifierInfo : qualifierInfos) {
                if (qualifierInfo instanceof QaCPSUriPolicyQualifier) {
                    String value = ((QaCPSUriPolicyQualifier) qualifierInfo).getCPSUri();
                    if (iCpsUris.contains(value) == false) {
                        failureMsg.append("CPSUri '" + value + "' is absent but is required");
                        failureMsg.append("; ");
                    }
                } else if (qualifierInfo instanceof QaUserNoticePolicyQualifierInfo) {
                    String value = ((QaUserNoticePolicyQualifierInfo) qualifierInfo).getUserNotice();
                    if (iUserNotices.contains(value) == false) {
                        failureMsg.append("userNotice '" + value + "' is absent but is required");
                        failureMsg.append("; ");
                    }
                } else {
                    throw new RuntimeException("should not reach here");
                }
            }
        }

        for (QaCertificatePolicyInformation cp : conf.getPolicyInformations()) {
            boolean present = false;
            for (PolicyInformation iPolicyInformation : iPolicyInformations) {
                if (iPolicyInformation.getPolicyIdentifier().getId().equals(cp.getPolicyId())) {
                    present = true;
                    break;
                }
            }

            if (present) {
                continue;
            }

            failureMsg.append("certificate policy '").append(cp.getPolicyId())
                    .append("' is absent but is required");
            failureMsg.append("; ");
        }
    }

    private void checkExtensionPolicyMappings(final StringBuilder failureMsg, final byte[] extensionValue,
            final Extensions requestExtensions, final ExtensionControl extControl) {
        QaPolicyMappingsOption conf = policyMappings;
        if (conf == null) {
            byte[] expected = getExpectedExtValue(Extension.policyMappings, requestExtensions, extControl);
            if (Arrays.equals(expected, extensionValue) == false) {
                failureMsg.append("extension valus is '" + hex(extensionValue) + "' but expected '"
                        + (expected == null ? "not present" : hex(expected)) + "'");
                failureMsg.append("; ");
            }
            return;
        }

        ASN1Sequence iPolicyMappings = DERSequence.getInstance(extensionValue);
        Map<String, String> iMap = new HashMap<>();
        int size = iPolicyMappings.size();
        for (int i = 0; i < size; i++) {
            ASN1Sequence seq = (ASN1Sequence) iPolicyMappings.getObjectAt(i);

            CertPolicyId issuerDomainPolicy = CertPolicyId.getInstance(seq.getObjectAt(0));
            CertPolicyId subjectDomainPolicy = CertPolicyId.getInstance(seq.getObjectAt(1));
            iMap.put(issuerDomainPolicy.getId(), subjectDomainPolicy.getId());
        }

        Set<String> eIssuerDomainPolicies = conf.getIssuerDomainPolicies();
        for (String eIssuerDomainPolicy : eIssuerDomainPolicies) {
            String eSubjectDomainPolicy = conf.getSubjectDomainPolicy(eIssuerDomainPolicy);

            String iSubjectDomainPolicy = iMap.remove(eIssuerDomainPolicy);
            if (iSubjectDomainPolicy == null) {
                failureMsg.append("issuerDomainPolicy '").append(eIssuerDomainPolicy)
                        .append("' is absent but is required");
                failureMsg.append("; ");
            } else if (iSubjectDomainPolicy.equals(eSubjectDomainPolicy) == false) {
                failureMsg.append("subjectDomainPolicy for issuerDomainPolicy is '" + iSubjectDomainPolicy
                        + "' but expected '" + eSubjectDomainPolicy + "'");
                failureMsg.append("; ");
            }
        }

        if (CollectionUtil.isNotEmpty(iMap)) {
            failureMsg.append("issuerDomainPolicies '" + iMap.keySet() + "' are present but not expected");
            failureMsg.append("; ");
        }
    }

    private void checkExtensionInhibitAnyPolicy(final StringBuilder failureMsg, final byte[] extensionValue,
            final Extensions requestExtensions, final ExtensionControl extControl) {
        QaInhibitAnyPolicy conf = inhibitAnyPolicy;
        if (conf == null) {
            byte[] expected = getExpectedExtValue(Extension.inhibitAnyPolicy, requestExtensions, extControl);
            if (Arrays.equals(expected, extensionValue) == false) {
                failureMsg.append("extension valus is '").append(hex(extensionValue));
                failureMsg.append("' but expected '").append(expected == null ? "not present" : hex(expected))
                        .append("'");
                failureMsg.append("; ");
            }
            return;
        }

        ASN1Integer asn1Int = ASN1Integer.getInstance(extensionValue);
        int iSkipCerts = asn1Int.getPositiveValue().intValue();
        if (iSkipCerts != conf.getSkipCerts()) {
            failureMsg.append("skipCerts is '" + iSkipCerts + "' but expected '" + conf.getSkipCerts() + "'");
            failureMsg.append("; ");
        }
    }

    private void checkExtensionSubjectAltName(final StringBuilder failureMsg, final byte[] extensionValue,
            final Extensions requestExtensions, final ExtensionControl extControl) {
        if (allowedSubjectAltNameModes == null) {
            byte[] expected = getExpectedExtValue(Extension.subjectAlternativeName, requestExtensions, extControl);
            if (Arrays.equals(expected, extensionValue) == false) {
                failureMsg.append("extension valus is '").append(hex(extensionValue));
                failureMsg.append("' but expected '").append(expected == null ? "not present" : hex(expected))
                        .append("'");
                failureMsg.append("; ");
            }
            return;
        }

        ASN1Encodable extInRequest = null;
        if (requestExtensions != null) {
            extInRequest = requestExtensions.getExtensionParsedValue(Extension.subjectAlternativeName);
        }

        if (extInRequest == null) {
            failureMsg.append("extension is present but not expected");
            failureMsg.append("; ");
            return;
        }

        GeneralName[] requested = GeneralNames.getInstance(extInRequest).getNames();

        GeneralName[] is = GeneralNames.getInstance(extensionValue).getNames();

        GeneralName[] expected = new GeneralName[requested.length];
        for (int i = 0; i < is.length; i++) {
            try {
                expected[i] = createGeneralName(is[i], allowedSubjectAltNameModes);
            } catch (BadCertTemplateException e) {
                failureMsg.append("error while processing ").append(i + 1).append("-th name: ")
                        .append(e.getMessage());
                failureMsg.append("; ");
                return;
            }
        }

        if (is.length != expected.length) {
            failureMsg.append("size of GeneralNames is '").append(is.length);
            failureMsg.append("' but expected '").append(expected.length).append("'");
            failureMsg.append("; ");
            return;
        }

        for (int i = 0; i < is.length; i++) {
            if (is[i].equals(expected[i]) == false) {
                failureMsg.append(i + 1).append("-th name does not match the requested one");
                failureMsg.append("; ");
            }
        }
    }

    private void checkExtensionSubjectInfoAccess(final StringBuilder failureMsg, final byte[] extensionValue,
            final Extensions requestExtensions, final ExtensionControl extControl) {
        if (allowedSubjectInfoAccessModes == null) {
            byte[] expected = getExpectedExtValue(Extension.subjectAlternativeName, requestExtensions, extControl);
            if (Arrays.equals(expected, extensionValue) == false) {
                failureMsg.append("extension valus is '").append(hex(extensionValue));
                failureMsg.append("' but expected '").append(expected == null ? "not present" : hex(expected))
                        .append("'");
                failureMsg.append("; ");
            }
            return;
        }

        ASN1Encodable requestExtValue = null;
        if (requestExtensions != null) {
            requestExtValue = requestExtensions.getExtensionParsedValue(Extension.subjectInfoAccess);
        }
        if (requestExtValue == null) {
            failureMsg.append("extension is present but not expected");
            failureMsg.append("; ");
            return;
        }

        ASN1Sequence requestSeq = ASN1Sequence.getInstance(requestExtValue);
        ASN1Sequence certSeq = ASN1Sequence.getInstance(extensionValue);

        int n = requestSeq.size();

        if (certSeq.size() != n) {
            failureMsg.append("size of GeneralNames is '").append(certSeq.size());
            failureMsg.append("' but expected '").append(n).append("'");
            failureMsg.append("; ");
            return;
        }

        for (int i = 0; i < n; i++) {
            AccessDescription ad = AccessDescription.getInstance(requestSeq.getObjectAt(i));
            ASN1ObjectIdentifier accessMethod = ad.getAccessMethod();

            Set<GeneralNameMode> generalNameModes;
            if (accessMethod == null) {
                generalNameModes = allowedSubjectInfoAccessModes.get(X509Certprofile.OID_ZERO);
            } else {
                generalNameModes = allowedSubjectInfoAccessModes.get(accessMethod);
            }

            if (generalNameModes == null) {
                failureMsg.append("accessMethod in requestExtension ");
                failureMsg.append(accessMethod == null ? "NULL" : accessMethod.getId());
                failureMsg.append(" is not allowed");
                failureMsg.append("; ");
                continue;
            }

            AccessDescription certAccessDesc = AccessDescription.getInstance(certSeq.getObjectAt(i));
            ASN1ObjectIdentifier certAccessMethod = certAccessDesc.getAccessMethod();

            boolean b;
            if (accessMethod == null) {
                b = certAccessDesc == null;
            } else {
                b = accessMethod.equals(certAccessMethod);
            }

            if (b == false) {
                failureMsg.append("accessMethod is '")
                        .append(certAccessMethod == null ? "null" : certAccessMethod.getId());
                failureMsg.append("' but expected '").append(accessMethod == null ? "null" : accessMethod.getId());
                failureMsg.append("; ");
                continue;
            }

            GeneralName accessLocation;
            try {
                accessLocation = createGeneralName(ad.getAccessLocation(), generalNameModes);
            } catch (BadCertTemplateException e) {
                failureMsg.append("invalid requestExtension: " + e.getMessage());
                failureMsg.append("; ");
                continue;
            }

            GeneralName certAccessLocation = certAccessDesc.getAccessLocation();
            if (certAccessLocation.equals(accessLocation) == false) {
                failureMsg.append("accessLocation does not match the requested one");
                failureMsg.append("; ");
            }
        }
    }

    private void checkExtensionIssuerAltNames(final StringBuilder failureMsg, final byte[] extensionValue,
            final X509IssuerInfo issuerInfo) {
        Extension caSubjectAltExtension = issuerInfo.getBcCert().getTBSCertificate().getExtensions()
                .getExtension(Extension.subjectAlternativeName);
        if (caSubjectAltExtension == null) {
            failureMsg.append("issuerAlternativeName is present but expected 'none'");
            failureMsg.append("; ");
            return;
        }

        byte[] caSubjectAltExtensionValue = caSubjectAltExtension.getExtnValue().getOctets();
        if (Arrays.equals(caSubjectAltExtensionValue, extensionValue) == false) {
            failureMsg.append(
                    "is '" + hex(extensionValue) + "' but expected '" + hex(caSubjectAltExtensionValue) + "'");
            failureMsg.append("; ");
        }
    }

    private void checkExtensionAuthorityInfoAccess(final StringBuilder failureMsg, final byte[] extensionValue,
            final X509IssuerInfo issuerInfo) {
        Set<String> eCaIssuerUris;
        if (aiaControl == null || aiaControl.includesCaIssuers()) {
            eCaIssuerUris = issuerInfo.getCaIssuerURLs();
        } else {
            eCaIssuerUris = Collections.emptySet();
        }

        Set<String> eOCSPUris;
        if (aiaControl == null || aiaControl.includesOcsp()) {
            eOCSPUris = issuerInfo.getOcspURLs();
        } else {
            eOCSPUris = Collections.emptySet();
        }

        if (CollectionUtil.isEmpty(eCaIssuerUris) && CollectionUtil.isEmpty(eOCSPUris)) {
            failureMsg.append("AIA is present but expected is 'none'");
            failureMsg.append("; ");
            return;
        }

        AuthorityInformationAccess iAIA = AuthorityInformationAccess.getInstance(extensionValue);
        checkAIA(failureMsg, iAIA, X509ObjectIdentifiers.id_ad_caIssuers, eCaIssuerUris);
        checkAIA(failureMsg, iAIA, X509ObjectIdentifiers.id_ad_ocsp, eOCSPUris);
    }

    private static void checkAIA(final StringBuilder failureMsg, final AuthorityInformationAccess aia,
            final ASN1ObjectIdentifier accessMethod, final Set<String> expectedUris) {
        String typeDesc;
        if (X509ObjectIdentifiers.id_ad_ocsp.equals(accessMethod)) {
            typeDesc = "OCSP";
        } else if (X509ObjectIdentifiers.id_ad_caIssuers.equals(accessMethod)) {
            typeDesc = "caIssuer";
        } else {
            typeDesc = accessMethod.getId();
        }

        List<AccessDescription> iAccessDescriptions = new LinkedList<>();
        for (AccessDescription accessDescription : aia.getAccessDescriptions()) {
            if (accessMethod.equals(accessDescription.getAccessMethod())) {
                iAccessDescriptions.add(accessDescription);
            }
        }

        int n = iAccessDescriptions.size();
        if (n != expectedUris.size()) {
            failureMsg.append("number of AIA " + typeDesc + " URIs is '").append(n);
            failureMsg.append("' but expected is '").append(expectedUris.size()).append("'");
            failureMsg.append("; ");
            return;
        }

        Set<String> iUris = new HashSet<>();
        for (int i = 0; i < n; i++) {
            GeneralName iAccessLocation = iAccessDescriptions.get(i).getAccessLocation();
            if (iAccessLocation.getTagNo() != GeneralName.uniformResourceIdentifier) {
                failureMsg.append("tag of accessLocation of AIA " + typeDesc + " is '")
                        .append(iAccessLocation.getTagNo());
                failureMsg.append("' but expected is '").append(GeneralName.uniformResourceIdentifier).append("'");
                failureMsg.append("; ");
            } else {
                String iOCSPUri = ((ASN1String) iAccessLocation.getName()).getString();
                iUris.add(iOCSPUri);
            }
        }

        Set<String> diffs = str_in_b_not_in_a(expectedUris, iUris);
        if (CollectionUtil.isNotEmpty(diffs)) {
            failureMsg.append(typeDesc + " URIs ").append(diffs.toString()).append(" are present but not expected");
            failureMsg.append("; ");
        }

        diffs = str_in_b_not_in_a(iUris, expectedUris);
        if (CollectionUtil.isNotEmpty(diffs)) {
            failureMsg.append(typeDesc + " URIs ").append(diffs.toString()).append(" are absent but are required");
            failureMsg.append("; ");
        }
    }

    private void checkExtensionCrlDistributionPoints(final StringBuilder failureMsg, final byte[] extensionValue,
            final X509IssuerInfo issuerInfo) {
        CRLDistPoint iCRLDistPoints = CRLDistPoint.getInstance(extensionValue);
        DistributionPoint[] iDistributionPoints = iCRLDistPoints.getDistributionPoints();
        int n = iDistributionPoints == null ? 0 : iDistributionPoints.length;
        if (n != 1) {
            failureMsg.append("size of CRLDistributionPoints is '").append(n).append("' but expected is '1'");
            failureMsg.append("; ");
            return;
        }

        Set<String> iCrlURLs = new HashSet<>();
        for (DistributionPoint entry : iDistributionPoints) {
            int asn1Type = entry.getDistributionPoint().getType();
            if (asn1Type != DistributionPointName.FULL_NAME) {
                failureMsg.append("tag of DistributionPointName of CRLDistibutionPoints is '").append(asn1Type);
                failureMsg.append("' but expected is '").append(DistributionPointName.FULL_NAME).append("'");
                failureMsg.append("; ");
                continue;
            }

            GeneralNames iDistributionPointNames = (GeneralNames) entry.getDistributionPoint().getName();
            GeneralName[] names = iDistributionPointNames.getNames();

            for (int i = 0; i < names.length; i++) {
                GeneralName name = names[i];
                if (name.getTagNo() != GeneralName.uniformResourceIdentifier) {
                    failureMsg.append("tag of CRL URL is '").append(name.getTagNo());
                    failureMsg.append("' but expected is '").append(GeneralName.uniformResourceIdentifier)
                            .append("'");
                    failureMsg.append("; ");
                } else {
                    String uri = ((ASN1String) name.getName()).getString();
                    iCrlURLs.add(uri);
                }
            }

            Set<String> eCRLUrls = issuerInfo.getCrlURLs();
            Set<String> diffs = str_in_b_not_in_a(eCRLUrls, iCrlURLs);
            if (CollectionUtil.isNotEmpty(diffs)) {
                failureMsg.append("CRL URLs ").append(diffs.toString()).append(" are present but not expected");
                failureMsg.append("; ");
            }

            diffs = str_in_b_not_in_a(iCrlURLs, eCRLUrls);
            if (CollectionUtil.isNotEmpty(diffs)) {
                failureMsg.append("CRL URLs ").append(diffs.toString()).append(" are absent but are required");
                failureMsg.append("; ");
            }
        }
    }

    private void checkExtensionDeltaCrlDistributionPoints(final StringBuilder failureMsg,
            final byte[] extensionValue, final X509IssuerInfo issuerInfo) {
        CRLDistPoint iCRLDistPoints = CRLDistPoint.getInstance(extensionValue);
        DistributionPoint[] iDistributionPoints = iCRLDistPoints.getDistributionPoints();
        int n = iDistributionPoints == null ? 0 : iDistributionPoints.length;
        if (n != 1) {
            failureMsg.append("size of CRLDistributionPoints (deltaCRL) is '").append(n)
                    .append("' but expected is '1'");
            failureMsg.append("; ");
            return;
        }

        Set<String> iCrlURLs = new HashSet<>();
        for (DistributionPoint entry : iDistributionPoints) {
            int asn1Type = entry.getDistributionPoint().getType();
            if (asn1Type != DistributionPointName.FULL_NAME) {
                failureMsg.append("tag of DistributionPointName of CRLDistibutionPoints (deltaCRL) is '")
                        .append(asn1Type);
                failureMsg.append("' but expected is '").append(DistributionPointName.FULL_NAME).append("'");
                failureMsg.append("; ");
                continue;
            }

            GeneralNames iDistributionPointNames = (GeneralNames) entry.getDistributionPoint().getName();
            GeneralName[] names = iDistributionPointNames.getNames();

            for (int i = 0; i < names.length; i++) {
                GeneralName name = names[i];
                if (name.getTagNo() != GeneralName.uniformResourceIdentifier) {
                    failureMsg.append("tag of deltaCRL URL is '").append(name.getTagNo());
                    failureMsg.append("' but expected is '").append(GeneralName.uniformResourceIdentifier)
                            .append("'");
                    failureMsg.append("; ");
                } else {
                    String uri = ((ASN1String) name.getName()).getString();
                    iCrlURLs.add(uri);
                }
            }

            Set<String> eCRLUrls = issuerInfo.getCrlURLs();
            Set<String> diffs = str_in_b_not_in_a(eCRLUrls, iCrlURLs);
            if (CollectionUtil.isNotEmpty(diffs)) {
                failureMsg.append("deltaCRL URLs ").append(diffs.toString())
                        .append(" are present but not expected");
                failureMsg.append("; ");
            }

            diffs = str_in_b_not_in_a(iCrlURLs, eCRLUrls);
            if (CollectionUtil.isNotEmpty(diffs)) {
                failureMsg.append("deltaCRL URLs ").append(diffs.toString()).append(" are absent but are required");
                failureMsg.append("; ");
            }
        }
    }

    private void checkExtensionAdmission(final StringBuilder failureMsg, final byte[] extensionValue,
            final Extensions requestExtensions, final ExtensionControl extControl) {
        QaAdmission conf = admission;
        if (conf == null) {
            byte[] expected = getExpectedExtValue(ObjectIdentifiers.id_extension_admission, requestExtensions,
                    extControl);
            if (Arrays.equals(expected, extensionValue) == false) {
                failureMsg.append("extension valus is '").append(hex(extensionValue));
                failureMsg.append("' but expected '").append(expected == null ? "not present" : hex(expected))
                        .append("'");
                failureMsg.append("; ");
            }
            return;
        }

        ASN1Sequence seq = ASN1Sequence.getInstance(extensionValue);
        AdmissionSyntax iAdmissionSyntax = AdmissionSyntax.getInstance(seq);
        Admissions[] iAdmissions = iAdmissionSyntax.getContentsOfAdmissions();
        int n = iAdmissions == null ? 0 : iAdmissions.length;
        if (n != 1) {
            failureMsg.append("size of Admissions is '").append(n).append("' but expected is '1'");
            failureMsg.append("; ");
            return;
        }

        Admissions iAdmission = iAdmissions[0];
        ProfessionInfo[] iProfessionInfos = iAdmission.getProfessionInfos();
        n = iProfessionInfos == null ? 0 : iProfessionInfos.length;
        if (n != 1) {
            failureMsg.append("size of ProfessionInfo is '").append(n).append("' but expected is '1'");
            failureMsg.append("; ");
            return;
        }

        ProfessionInfo iProfessionInfo = iProfessionInfos[0];
        String iRegistrationNumber = iProfessionInfo.getRegistrationNumber();
        String eRegistrationNumber = conf.getRegistrationNumber();
        if (eRegistrationNumber == null) {
            if (iRegistrationNumber != null) {
                failureMsg.append("RegistrationNumber is '").append(iRegistrationNumber);
                failureMsg.append("' but expected is 'null'");
                failureMsg.append("; ");
            }
        } else if (eRegistrationNumber.equals(iRegistrationNumber) == false) {
            failureMsg.append("RegistrationNumber is '").append(iRegistrationNumber);
            failureMsg.append("' but expected is '").append(eRegistrationNumber).append("'");
            failureMsg.append("; ");
        }

        byte[] iAddProfessionInfo = null;
        if (iProfessionInfo.getAddProfessionInfo() != null) {
            iAddProfessionInfo = iProfessionInfo.getAddProfessionInfo().getOctets();
        }
        byte[] eAddProfessionInfo = conf.getAddProfessionInfo();
        if (eAddProfessionInfo == null) {
            if (iAddProfessionInfo != null) {
                failureMsg.append("AddProfessionInfo is '").append(hex(iAddProfessionInfo));
                failureMsg.append("' but expected is 'null'");
                failureMsg.append("; ");
            }
        } else {
            if (iAddProfessionInfo == null) {
                failureMsg.append("AddProfessionInfo is 'null' but expected is '").append(hex(eAddProfessionInfo));
                failureMsg.append("'");
                failureMsg.append("; ");
            } else if (Arrays.equals(eAddProfessionInfo, iAddProfessionInfo) == false) {
                failureMsg.append("AddProfessionInfo is '").append(hex(iAddProfessionInfo));
                failureMsg.append("' but expected is '").append(hex(eAddProfessionInfo)).append("'");
                failureMsg.append("; ");
            }
        }

        List<String> eProfessionOids = conf.getProfessionOIDs();
        ASN1ObjectIdentifier[] _iProfessionOids = iProfessionInfo.getProfessionOIDs();
        List<String> iProfessionOids = new LinkedList<>();
        if (_iProfessionOids != null) {
            for (ASN1ObjectIdentifier entry : _iProfessionOids) {
                iProfessionOids.add(entry.getId());
            }
        }

        Set<String> diffs = str_in_b_not_in_a(eProfessionOids, iProfessionOids);
        if (CollectionUtil.isNotEmpty(diffs)) {
            failureMsg.append("ProfessionOIDs ").append(diffs.toString()).append(" are present but not expected");
            failureMsg.append("; ");
        }

        diffs = str_in_b_not_in_a(iProfessionOids, eProfessionOids);
        if (CollectionUtil.isNotEmpty(diffs)) {
            failureMsg.append("ProfessionOIDs ").append(diffs.toString()).append(" are absent but are required");
            failureMsg.append("; ");
        }

        List<String> eProfessionItems = conf.getProfessionItems();
        DirectoryString[] items = iProfessionInfo.getProfessionItems();
        List<String> iProfessionItems = new LinkedList<>();
        if (items != null) {
            for (DirectoryString item : items) {
                iProfessionItems.add(item.getString());
            }
        }

        diffs = str_in_b_not_in_a(eProfessionItems, iProfessionItems);
        if (CollectionUtil.isNotEmpty(diffs)) {
            failureMsg.append("ProfessionItems ").append(diffs.toString()).append(" are present but not expected");
            failureMsg.append("; ");
        }

        diffs = str_in_b_not_in_a(iProfessionItems, eProfessionItems);
        if (CollectionUtil.isNotEmpty(diffs)) {
            failureMsg.append("ProfessionItems ").append(diffs.toString()).append(" are absent but are required");
            failureMsg.append("; ");
        }
    }

    private boolean checkExtensionOcspNocheck(final StringBuilder failureMsg, final byte[] extensionValue) {
        if (Arrays.equals(DERNull, extensionValue) == false) {
            failureMsg.append("value is not DER NULL");
            failureMsg.append("; ");
        }
        return true;
    }

    private static String hex(final byte[] bytes) {
        return Hex.toHexString(bytes);
    }

    private static Set<String> str_in_b_not_in_a(final Collection<String> a, final Collection<String> b) {
        if (b == null) {
            return Collections.emptySet();
        }

        Set<String> result = new HashSet<>();
        for (String entry : b) {
            if (a == null || a.contains(entry) == false) {
                result.add(entry);
            }
        }
        return result;
    }

    static Set<Range> buildParametersMap(final RangesType ranges) {
        if (ranges == null) {
            return null;
        }

        Set<Range> ret = new HashSet<>();
        for (RangeType range : ranges.getRange()) {
            if (range.getMin() != null || range.getMax() != null) {
                ret.add(new Range(range.getMin(), range.getMax()));
            }
        }
        return ret;
    }

    private static GeneralName createGeneralName(final GeneralName reqName, final Set<GeneralNameMode> modes)
            throws BadCertTemplateException {
        int tag = reqName.getTagNo();
        GeneralNameMode mode = null;
        for (GeneralNameMode m : modes) {
            if (m.getTag().getTag() == tag) {
                mode = m;
                break;
            }
        }

        if (mode == null) {
            throw new BadCertTemplateException("generalName tag " + tag + " is not allowed");
        }

        switch (tag) {
        case GeneralName.rfc822Name:
        case GeneralName.dNSName:
        case GeneralName.uniformResourceIdentifier:
        case GeneralName.iPAddress:
        case GeneralName.registeredID:
        case GeneralName.directoryName: {
            return new GeneralName(tag, reqName.getName());
        }
        case GeneralName.otherName: {
            ASN1Sequence reqSeq = ASN1Sequence.getInstance(reqName.getName());
            ASN1ObjectIdentifier type = ASN1ObjectIdentifier.getInstance(reqSeq.getObjectAt(0));
            if (mode.getAllowedTypes().contains(type) == false) {
                throw new BadCertTemplateException("otherName.type " + type.getId() + " is not allowed");
            }

            ASN1Encodable value = ((ASN1TaggedObject) reqSeq.getObjectAt(1)).getObject();
            String text;
            if (value instanceof ASN1String == false) {
                throw new BadCertTemplateException("otherName.value is not a String");
            } else {
                text = ((ASN1String) value).getString();
            }

            ASN1EncodableVector vector = new ASN1EncodableVector();
            vector.add(type);
            vector.add(new DERTaggedObject(true, 0, new DERUTF8String(text)));
            DERSequence seq = new DERSequence(vector);

            return new GeneralName(GeneralName.otherName, seq);
        }
        case GeneralName.ediPartyName: {
            ASN1Sequence reqSeq = ASN1Sequence.getInstance(reqName.getName());

            int n = reqSeq.size();
            String nameAssigner = null;
            int idx = 0;
            if (n > 1) {
                DirectoryString ds = DirectoryString
                        .getInstance(((ASN1TaggedObject) reqSeq.getObjectAt(idx++)).getObject());
                nameAssigner = ds.getString();
            }

            DirectoryString ds = DirectoryString
                    .getInstance(((ASN1TaggedObject) reqSeq.getObjectAt(idx++)).getObject());
            String partyName = ds.getString();

            ASN1EncodableVector vector = new ASN1EncodableVector();
            if (nameAssigner != null) {
                vector.add(new DERTaggedObject(false, 0, new DirectoryString(nameAssigner)));
            }
            vector.add(new DERTaggedObject(false, 1, new DirectoryString(partyName)));
            ASN1Sequence seq = new DERSequence(vector);
            return new GeneralName(GeneralName.ediPartyName, seq);
        }
        default: {
            throw new RuntimeException("should not reach here, unknwon GeneralName tag " + tag);
        }
        } // end switch
    }

    private static Set<String> getKeyUsage(final byte[] extensionValue) {
        Set<String> usages = new HashSet<>();
        org.bouncycastle.asn1.x509.KeyUsage reqKeyUsage = org.bouncycastle.asn1.x509.KeyUsage
                .getInstance(extensionValue);
        for (KeyUsage k : KeyUsage.values()) {
            if (reqKeyUsage.hasUsages(k.getBcUsage())) {
                usages.add(k.getName());
            }
        }

        return usages;
    }

    private static Set<String> getExtKeyUsage(final byte[] extensionValue) {
        Set<String> usages = new HashSet<>();
        org.bouncycastle.asn1.x509.ExtendedKeyUsage reqKeyUsage = org.bouncycastle.asn1.x509.ExtendedKeyUsage
                .getInstance(extensionValue);
        for (KeyPurposeId usage : reqKeyUsage.getUsages()) {
            usages.add(usage.getId());
        }
        return usages;
    }

    private Set<KeyUsageControl> getKeyusage(final boolean required) {
        Set<KeyUsageControl> ret = new HashSet<>();

        Set<KeyUsageControl> controls = keyusages;
        if (controls != null) {
            for (KeyUsageControl control : controls) {
                if (control.isRequired() == required) {
                    ret.add(control);
                }
            }
        }
        return ret;
    }

    private Set<ExtKeyUsageControl> getExtKeyusage(final boolean required) {
        Set<ExtKeyUsageControl> ret = new HashSet<>();

        Set<ExtKeyUsageControl> controls = extendedKeyusages;
        if (controls != null) {
            for (ExtKeyUsageControl control : controls) {
                if (control.isRequired() == required) {
                    ret.add(control);
                }
            }
        }
        return ret;
    }

    private RDNControl getSubjectDNControl(final ASN1ObjectIdentifier type) {
        for (RDNControl control : subjectDNControls) {
            if (control.getType().equals(type)) {
                return control;
            }
        }

        return null;
    }

    private byte[] getConstantExtensionValue(final ASN1ObjectIdentifier type) {
        return constantExtensions == null ? null : constantExtensions.get(type).getValue();
    }

    private Object getExtensionValue(final ASN1ObjectIdentifier type, final ExtensionsType extensionsType) {
        for (ExtensionType m : extensionsType.getExtension()) {
            if (m.getType().getValue().equals(type.getId())) {
                if (m.getValue() == null) {
                    return null;
                }
                return m.getValue().getAny();
            }
        }

        throw new RuntimeException("should not reach here: undefined extension " + type.getId());
    }

    public static Map<ASN1ObjectIdentifier, QaExtensionValue> buildConstantExtesions(
            final ExtensionsType extensionsType) throws CertprofileException {
        if (extensionsType == null) {
            return null;
        }

        Map<ASN1ObjectIdentifier, QaExtensionValue> map = new HashMap<>();

        for (ExtensionType m : extensionsType.getExtension()) {
            if (m.getValue() == null || m.getValue().getAny() instanceof ConstantExtValue == false) {
                continue;
            }

            ConstantExtValue extConf = (ConstantExtValue) m.getValue().getAny();
            byte[] encodedValue = extConf.getValue();
            ASN1StreamParser parser = new ASN1StreamParser(encodedValue);
            try {
                parser.readObject();
            } catch (IOException e) {
                throw new CertprofileException("could not parse the constant extension value", e);
            }
            QaExtensionValue extension = new QaExtensionValue(m.isCritical(), encodedValue);
            map.put(new ASN1ObjectIdentifier(m.getType().getValue()), extension);
        }

        if (CollectionUtil.isEmpty(map)) {
            return null;
        }

        return Collections.unmodifiableMap(map);
    }

    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) == false && p.matcher(value).matches()) {
                    sorted.add(value);
                }
            }
        }
        for (String value : contentList) {
            if (sorted.contains(value) == false) {
                sorted.add(value);
            }
        }
        return sorted;
    }
}