/* * * Copyright (c) 2013 - 2017 Lijun Liao * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License version 3 * as published by the Free Software Foundation with the addition of the * following permission added to Section 15 as permitted in Section 7(a): * * FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY * THE AUTHOR LIJUN LIAO. LIJUN LIAO DISCLAIMS THE WARRANTY OF NON INFRINGEMENT * OF THIRD PARTY RIGHTS. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. * * The interactive user interfaces in modified source and object code versions * of this program must display Appropriate Legal Notices, as required under * Section 5 of the GNU Affero General Public License. * * You can be released from the requirements of the license by purchasing * a commercial license. Buying such a license is mandatory as soon as you * develop commercial activities involving the XiPKI software without * disclosing the source code of your own applications. * * For more information, please contact Lijun Liao at this * address: lijun.liao@gmail.com */ package org.xipki.pki.ca.qa; import java.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.util.Calendar; 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 org.bouncycastle.asn1.ASN1GeneralizedTime; import org.bouncycastle.asn1.ASN1ObjectIdentifier; import org.bouncycastle.asn1.ASN1Primitive; import org.bouncycastle.asn1.ASN1StreamParser; import org.bouncycastle.asn1.ASN1UTCTime; import org.bouncycastle.asn1.x500.X500Name; import org.bouncycastle.asn1.x509.AlgorithmIdentifier; import org.bouncycastle.asn1.x509.Certificate; import org.bouncycastle.asn1.x509.Extension; import org.bouncycastle.asn1.x509.Extensions; import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; import org.bouncycastle.asn1.x509.TBSCertificate; import org.bouncycastle.asn1.x509.Time; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.xipki.commons.common.qa.ValidationIssue; import org.xipki.commons.common.qa.ValidationResult; import org.xipki.commons.common.util.CollectionUtil; import org.xipki.commons.common.util.LogUtil; import org.xipki.commons.common.util.ParamUtil; import org.xipki.commons.security.util.AlgorithmUtil; import org.xipki.commons.security.util.X509Util; import org.xipki.pki.ca.api.profile.CertValidity; import org.xipki.pki.ca.api.profile.CertprofileException; import org.xipki.pki.ca.api.profile.Range; import org.xipki.pki.ca.api.profile.x509.X509CertVersion; import org.xipki.pki.ca.certprofile.XmlX509Certprofile; import org.xipki.pki.ca.certprofile.XmlX509CertprofileUtil; import org.xipki.pki.ca.certprofile.x509.jaxb.ConstantExtValue; import org.xipki.pki.ca.certprofile.x509.jaxb.ExtensionType; import org.xipki.pki.ca.certprofile.x509.jaxb.ExtensionsType; import org.xipki.pki.ca.certprofile.x509.jaxb.RangeType; import org.xipki.pki.ca.certprofile.x509.jaxb.RangesType; import org.xipki.pki.ca.certprofile.x509.jaxb.X509ProfileType; import org.xipki.pki.ca.qa.internal.QaExtensionValue; /** * @author Lijun Liao * @since 2.0.0 */ public class X509CertprofileQa { private static final Logger LOG = LoggerFactory.getLogger(X509CertprofileQa.class); private static final TimeZone UTC = TimeZone.getTimeZone("UTC"); private static final long SECOND = 1000L; private static final long MAX_CERT_TIME_MS = 253402300799982L; //9999-12-31-23-59-59 private static final long EPOCHTIME_2050010100 = 2524608000L; private final SubjectChecker subjectChecker; private final PublicKeyChecker publicKeyChecker; private final ExtensionsChecker extensionsChecker; private final XmlX509Certprofile certProfile; public X509CertprofileQa(final String data) throws CertprofileException { this(ParamUtil.requireNonNull("data", data).getBytes()); } public X509CertprofileQa(final byte[] dataBytes) throws CertprofileException { ParamUtil.requireNonNull("dataBytes", dataBytes); try { X509ProfileType conf = XmlX509CertprofileUtil.parse( new ByteArrayInputStream(dataBytes)); certProfile = new XmlX509Certprofile(); certProfile.initialize(conf); this.publicKeyChecker = new PublicKeyChecker(certProfile.getKeyAlgorithms()); this.subjectChecker = new SubjectChecker(certProfile.getSpecialBehavior(), certProfile.getSubjectControl()); this.extensionsChecker = new ExtensionsChecker(conf, certProfile); } catch (RuntimeException ex) { LogUtil.error(LOG, ex); throw new CertprofileException( "RuntimeException thrown while initializing certprofile: " + ex.getMessage()); } } // constructor public ValidationResult checkCert(final byte[] certBytes, final X509IssuerInfo issuerInfo, final X500Name requestedSubject, final SubjectPublicKeyInfo requestedPublicKey, final Extensions requestedExtensions) { ParamUtil.requireNonNull("certBytes", certBytes); ParamUtil.requireNonNull("issuerInfo", issuerInfo); ParamUtil.requireNonNull("requestedSubject", requestedSubject); ParamUtil.requireNonNull("requestedPublicKey", requestedPublicKey); List<ValidationIssue> resultIssues = new LinkedList<ValidationIssue>(); Certificate bcCert; TBSCertificate tbsCert; X509Certificate cert; ValidationIssue issue; // certificate size issue = new ValidationIssue("X509.SIZE", "certificate size"); resultIssues.add(issue); Integer maxSize = certProfile.getMaxSize(); if (maxSize != 0) { int size = certBytes.length; if (size > maxSize) { issue.setFailureMessage(String.format( "certificate exceeds the maximal allowed size: %d > %d", size, maxSize)); } } // certificate encoding issue = new ValidationIssue("X509.ENCODING", "certificate encoding"); resultIssues.add(issue); try { bcCert = Certificate.getInstance(certBytes); tbsCert = bcCert.getTBSCertificate(); cert = X509Util.parseCert(certBytes); } catch (CertificateException ex) { issue.setFailureMessage("certificate is not corrected encoded"); return new ValidationResult(resultIssues); } // syntax version issue = new ValidationIssue("X509.VERSION", "certificate version"); resultIssues.add(issue); int versionNumber = tbsCert.getVersionNumber(); X509CertVersion expVersion = certProfile.getVersion(); if (versionNumber != expVersion.getVersionNumber()) { issue.setFailureMessage("is '" + versionNumber + "' but expected '" + expVersion.getVersionNumber() + "'"); } // serialNumber issue = new ValidationIssue("X509.serialNumber", "certificate serial number"); resultIssues.add(issue); BigInteger serialNumber = tbsCert.getSerialNumber().getValue(); if (serialNumber.signum() != 1) { issue.setFailureMessage("not positive"); } else { if (serialNumber.bitLength() >= 160) { issue.setFailureMessage("serial number has more than 20 octets"); } } // signatureAlgorithm List<String> signatureAlgorithms = certProfile.getSignatureAlgorithms(); if (CollectionUtil.isNonEmpty(signatureAlgorithms)) { issue = new ValidationIssue("X509.SIGALG", "signature algorithm"); resultIssues.add(issue); AlgorithmIdentifier sigAlgId = bcCert.getSignatureAlgorithm(); AlgorithmIdentifier tbsSigAlgId = tbsCert.getSignature(); if (!tbsSigAlgId.equals(sigAlgId)) { issue.setFailureMessage( "Certificate.tbsCertificate.signature != Certificate.signatureAlgorithm"); } try { String sigAlgo = AlgorithmUtil.getSignatureAlgoName(sigAlgId); if (!issue.isFailed()) { if (!signatureAlgorithms.contains(sigAlgo)) { issue.setFailureMessage("signatureAlgorithm '" + sigAlgo + "' is not allowed"); } } // check parameters if (!issue.isFailed()) { AlgorithmIdentifier expSigAlgId = AlgorithmUtil.getSigAlgId(sigAlgo); if (!expSigAlgId.equals(sigAlgId)) { issue.setFailureMessage("invalid parameters"); } } } catch (NoSuchAlgorithmException ex) { issue.setFailureMessage("unsupported signature algorithm " + sigAlgId.getAlgorithm().getId()); } } // notBefore encoding issue = new ValidationIssue("X509.NOTBEFORE.ENCODING", "notBefore encoding"); checkTime(tbsCert.getStartDate(), issue); // notAfter encoding issue = new ValidationIssue("X509.NOTAFTER.ENCODING", "notAfter encoding"); checkTime(tbsCert.getStartDate(), issue); // notBefore if (certProfile.isNotBeforeMidnight()) { issue = new ValidationIssue("X509.NOTBEFORE", "notBefore midnight"); resultIssues.add(issue); Calendar cal = Calendar.getInstance(UTC); cal.setTime(cert.getNotBefore()); int hourOfDay = cal.get(Calendar.HOUR_OF_DAY); int minute = cal.get(Calendar.MINUTE); int second = cal.get(Calendar.SECOND); if (hourOfDay != 0 || minute != 0 || second != 0) { issue.setFailureMessage(" '" + cert.getNotBefore() + "' is not midnight time (UTC)"); } } // validity issue = new ValidationIssue("X509.VALIDITY", "cert validity"); resultIssues.add(issue); if (cert.getNotAfter().before(cert.getNotBefore())) { issue.setFailureMessage("notAfter must not be before notBefore"); } else if (cert.getNotBefore().before(issuerInfo.getCaNotBefore())) { issue.setFailureMessage("notBefore must not be before CA's notBefore"); } else { CertValidity validity = certProfile.getValidity(); Date expectedNotAfter = validity.add(cert.getNotBefore()); if (expectedNotAfter.getTime() > MAX_CERT_TIME_MS) { expectedNotAfter = new Date(MAX_CERT_TIME_MS); } if (issuerInfo.isCutoffNotAfter() && expectedNotAfter.after(issuerInfo.getCaNotAfter())) { expectedNotAfter = issuerInfo.getCaNotAfter(); } if (Math.abs(expectedNotAfter.getTime() - cert.getNotAfter().getTime()) > 60 * SECOND) { issue.setFailureMessage("cert validity is not within " + validity.toString()); } } // subjectPublicKeyInfo resultIssues.addAll(publicKeyChecker.checkPublicKey(bcCert.getSubjectPublicKeyInfo(), requestedPublicKey)); // Signature issue = new ValidationIssue("X509.SIG", "whether certificate is signed by CA"); resultIssues.add(issue); try { cert.verify(issuerInfo.getCert().getPublicKey(), "BC"); } catch (Exception ex) { issue.setFailureMessage("invalid signature"); } // issuer issue = new ValidationIssue("X509.ISSUER", "certificate issuer"); resultIssues.add(issue); if (!cert.getIssuerX500Principal().equals(issuerInfo.getCert().getSubjectX500Principal())) { issue.setFailureMessage( "issue in certificate does not equal the subject of CA certificate"); } // subject resultIssues.addAll(subjectChecker.checkSubject(bcCert.getSubject(), requestedSubject)); // issuerUniqueID issue = new ValidationIssue("X509.IssuerUniqueID", "issuerUniqueID"); resultIssues.add(issue); if (tbsCert.getIssuerUniqueId() != null) { issue.setFailureMessage("is present but not permitted"); } // subjectUniqueID issue = new ValidationIssue("X509.SubjectUniqueID", "subjectUniqueID"); resultIssues.add(issue); if (tbsCert.getSubjectUniqueId() != null) { issue.setFailureMessage("is present but not permitted"); } // extensions issue = new ValidationIssue("X509.GrantedSubject", "grantedSubject"); resultIssues.add(issue); resultIssues.addAll( extensionsChecker.checkExtensions(bcCert, issuerInfo, requestedExtensions, requestedSubject)); return new ValidationResult(resultIssues); } // method checkCert 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; } 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)) { continue; } ASN1ObjectIdentifier oid = new ASN1ObjectIdentifier(m.getType().getValue()); if (Extension.subjectAlternativeName.equals(oid) || Extension.subjectInfoAccess.equals(oid) || Extension.biometricInfo.equals(oid)) { continue; } ConstantExtValue extConf = (ConstantExtValue) m.getValue().getAny(); byte[] encodedValue = extConf.getValue(); ASN1StreamParser parser = new ASN1StreamParser(encodedValue); try { parser.readObject(); } catch (IOException ex) { throw new CertprofileException("could not parse the constant extension value", ex); } QaExtensionValue extension = new QaExtensionValue(m.isCritical(), encodedValue); map.put(oid, extension); } if (CollectionUtil.isEmpty(map)) { return null; } return Collections.unmodifiableMap(map); } // method buildConstantExtesions private static void checkTime(Time time, ValidationIssue issue) { ASN1Primitive asn1Time = time.toASN1Primitive(); if (time.getDate().getTime() / 1000 < EPOCHTIME_2050010100) { if (!(asn1Time instanceof ASN1UTCTime)) { issue.setFailureMessage("not encoded as UTCTime"); } } else { if (!(asn1Time instanceof ASN1GeneralizedTime)) { issue.setFailureMessage("not encoded as GeneralizedTime"); } } } }