| /* |
| * Copyright (C) 2017 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.server.locksettings.recoverablekeystore.certificate; |
| |
| import android.annotation.IntDef; |
| import android.annotation.Nullable; |
| |
| import com.android.internal.annotations.VisibleForTesting; |
| |
| import org.w3c.dom.Document; |
| import org.w3c.dom.Element; |
| import org.w3c.dom.Node; |
| import org.w3c.dom.NodeList; |
| import org.xml.sax.SAXException; |
| |
| import java.io.ByteArrayInputStream; |
| import java.io.IOException; |
| import java.io.InputStream; |
| import java.lang.annotation.Retention; |
| import java.lang.annotation.RetentionPolicy; |
| import java.security.InvalidAlgorithmParameterException; |
| import java.security.InvalidKeyException; |
| import java.security.NoSuchAlgorithmException; |
| import java.security.PublicKey; |
| import java.security.Signature; |
| import java.security.SignatureException; |
| import java.security.cert.CertPath; |
| import java.security.cert.CertPathBuilder; |
| import java.security.cert.CertPathBuilderException; |
| import java.security.cert.CertPathValidator; |
| import java.security.cert.CertPathValidatorException; |
| import java.security.cert.CertStore; |
| import java.security.cert.CertificateException; |
| import java.security.cert.CertificateFactory; |
| import java.security.cert.CollectionCertStoreParameters; |
| import java.security.cert.PKIXBuilderParameters; |
| import java.security.cert.PKIXParameters; |
| import java.security.cert.TrustAnchor; |
| import java.security.cert.X509CertSelector; |
| import java.security.cert.X509Certificate; |
| import java.util.ArrayList; |
| import java.util.Base64; |
| import java.util.Date; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Set; |
| |
| import javax.xml.parsers.DocumentBuilderFactory; |
| import javax.xml.parsers.ParserConfigurationException; |
| |
| /** Utility functions related to parsing and validating public-key certificates. */ |
| public final class CertUtils { |
| |
| private static final String CERT_FORMAT = "X.509"; |
| private static final String CERT_PATH_ALG = "PKIX"; |
| private static final String CERT_STORE_ALG = "Collection"; |
| private static final String SIGNATURE_ALG = "SHA256withRSA"; |
| |
| @Retention(RetentionPolicy.SOURCE) |
| @IntDef({MUST_EXIST_UNENFORCED, MUST_EXIST_EXACTLY_ONE, MUST_EXIST_AT_LEAST_ONE}) |
| @interface MustExist {} |
| static final int MUST_EXIST_UNENFORCED = 0; |
| static final int MUST_EXIST_EXACTLY_ONE = 1; |
| static final int MUST_EXIST_AT_LEAST_ONE = 2; |
| |
| private CertUtils() {} |
| |
| /** |
| * Decodes a byte array containing an encoded X509 certificate. |
| * |
| * @param certBytes the byte array containing the encoded X509 certificate |
| * @return the decoded X509 certificate |
| * @throws CertParsingException if any parsing error occurs |
| */ |
| static X509Certificate decodeCert(byte[] certBytes) throws CertParsingException { |
| return decodeCert(new ByteArrayInputStream(certBytes)); |
| } |
| |
| /** |
| * Decodes an X509 certificate from an {@code InputStream}. |
| * |
| * @param inStream the input stream containing the encoded X509 certificate |
| * @return the decoded X509 certificate |
| * @throws CertParsingException if any parsing error occurs |
| */ |
| static X509Certificate decodeCert(InputStream inStream) throws CertParsingException { |
| CertificateFactory certFactory; |
| try { |
| certFactory = CertificateFactory.getInstance(CERT_FORMAT); |
| } catch (CertificateException e) { |
| // Should not happen, as X.509 is mandatory for all providers. |
| throw new RuntimeException(e); |
| } |
| try { |
| return (X509Certificate) certFactory.generateCertificate(inStream); |
| } catch (CertificateException e) { |
| throw new CertParsingException(e); |
| } |
| } |
| |
| /** |
| * Parses a byte array as the content of an XML file, and returns the root node of the XML file. |
| * |
| * @param xmlBytes the byte array that is the XML file content |
| * @return the root node of the XML file |
| * @throws CertParsingException if any parsing error occurs |
| */ |
| static Element getXmlRootNode(byte[] xmlBytes) throws CertParsingException { |
| try { |
| Document document = |
| DocumentBuilderFactory.newInstance() |
| .newDocumentBuilder() |
| .parse(new ByteArrayInputStream(xmlBytes)); |
| document.getDocumentElement().normalize(); |
| return document.getDocumentElement(); |
| } catch (SAXException | ParserConfigurationException | IOException e) { |
| throw new CertParsingException(e); |
| } |
| } |
| |
| /** |
| * Gets the text contents of certain XML child nodes, given a XML root node and a list of tags |
| * representing the path to locate the child nodes. The whitespaces and newlines in the text |
| * contents are stripped away. |
| * |
| * <p>For example, the list of tags [tag1, tag2, tag3] represents the XML tree like the |
| * following: |
| * |
| * <pre> |
| * <root> |
| * <tag1> |
| * <tag2> |
| * <tag3>abc</tag3> |
| * <tag3>def</tag3> |
| * </tag2> |
| * </tag1> |
| * <root> |
| * </pre> |
| * |
| * @param mustExist whether and how many nodes must exist. If the number of child nodes does not |
| * satisfy the requirement, CertParsingException will be thrown. |
| * @param rootNode the root node that serves as the starting point to locate the child nodes |
| * @param nodeTags the list of tags representing the relative path from the root node |
| * @return a list of strings that are the text contents of the child nodes |
| * @throws CertParsingException if any parsing error occurs |
| */ |
| static List<String> getXmlNodeContents(@MustExist int mustExist, Element rootNode, |
| String... nodeTags) |
| throws CertParsingException { |
| if (nodeTags.length == 0) { |
| throw new CertParsingException("The tag list must not be empty"); |
| } |
| |
| // Go down through all the intermediate node tags (except the last tag for the leaf nodes). |
| // Note that this implementation requires that at most one path exists for the given |
| // intermediate node tags. |
| Element parent = rootNode; |
| for (int i = 0; i < nodeTags.length - 1; i++) { |
| String tag = nodeTags[i]; |
| List<Element> children = getXmlDirectChildren(parent, tag); |
| if ((children.size() == 0 && mustExist != MUST_EXIST_UNENFORCED) |
| || children.size() > 1) { |
| throw new CertParsingException( |
| "The XML file must contain exactly one path with the tag " + tag); |
| } |
| if (children.size() == 0) { |
| return new ArrayList<>(); |
| } |
| parent = children.get(0); |
| } |
| |
| // Then collect the contents of the leaf nodes. |
| List<Element> leafs = getXmlDirectChildren(parent, nodeTags[nodeTags.length - 1]); |
| if (mustExist == MUST_EXIST_EXACTLY_ONE && leafs.size() != 1) { |
| throw new CertParsingException( |
| "The XML file must contain exactly one node with the path " |
| + String.join("/", nodeTags)); |
| } |
| if (mustExist == MUST_EXIST_AT_LEAST_ONE && leafs.size() == 0) { |
| throw new CertParsingException( |
| "The XML file must contain at least one node with the path " |
| + String.join("/", nodeTags)); |
| } |
| List<String> result = new ArrayList<>(); |
| for (Element leaf : leafs) { |
| // Remove whitespaces and newlines. |
| result.add(leaf.getTextContent().replaceAll("\\s", "")); |
| } |
| return result; |
| } |
| |
| /** Get the direct child nodes with a given tag. */ |
| private static List<Element> getXmlDirectChildren(Element parent, String tag) { |
| // Cannot use Element.getElementsByTagName because it will return all descendant elements |
| // with the tag name, i.e. not only the direct child nodes. |
| List<Element> children = new ArrayList<>(); |
| NodeList childNodes = parent.getChildNodes(); |
| for (int i = 0; i < childNodes.getLength(); i++) { |
| Node node = childNodes.item(i); |
| if (node.getNodeType() == Node.ELEMENT_NODE && node.getNodeName().equals(tag)) { |
| children.add((Element) node); |
| } |
| } |
| return children; |
| } |
| |
| /** |
| * Decodes a base64-encoded string. |
| * |
| * @param str the base64-encoded string |
| * @return the decoding decoding result |
| * @throws CertParsingException if the input string is not a properly base64-encoded string |
| */ |
| public static byte[] decodeBase64(String str) throws CertParsingException { |
| try { |
| return Base64.getDecoder().decode(str); |
| } catch (IllegalArgumentException e) { |
| throw new CertParsingException(e); |
| } |
| } |
| |
| /** |
| * Verifies a public-key signature that is computed by RSA with SHA256. |
| * |
| * @param signerPublicKey the public key of the original signer |
| * @param signature the public-key signature |
| * @param signedBytes the bytes that have been signed |
| * @throws CertValidationException if the signature verification fails |
| */ |
| static void verifyRsaSha256Signature( |
| PublicKey signerPublicKey, byte[] signature, byte[] signedBytes) |
| throws CertValidationException { |
| Signature verifier; |
| try { |
| verifier = Signature.getInstance(SIGNATURE_ALG); |
| } catch (NoSuchAlgorithmException e) { |
| // Should not happen, as SHA256withRSA is mandatory for all providers. |
| throw new RuntimeException(e); |
| } |
| try { |
| verifier.initVerify(signerPublicKey); |
| verifier.update(signedBytes); |
| if (!verifier.verify(signature)) { |
| throw new CertValidationException("The signature is invalid"); |
| } |
| } catch (InvalidKeyException | SignatureException e) { |
| throw new CertValidationException(e); |
| } |
| } |
| |
| /** |
| * Validates a leaf certificate, and returns the certificate path if the certificate is valid. |
| * If the given validation date is null, the current date will be used. |
| * |
| * @param validationDate the date for which the validity of the certificate should be |
| * determined |
| * @param trustedRoot the certificate of the trusted root CA |
| * @param intermediateCerts the list of certificates of possible intermediate CAs |
| * @param leafCert the leaf certificate that is to be validated |
| * @return the certificate path if the leaf cert is valid |
| * @throws CertValidationException if {@code leafCert} is invalid (e.g., is expired, or has |
| * invalid signature) |
| */ |
| static CertPath validateCert( |
| @Nullable Date validationDate, |
| X509Certificate trustedRoot, |
| List<X509Certificate> intermediateCerts, |
| X509Certificate leafCert) |
| throws CertValidationException { |
| PKIXParameters pkixParams = |
| buildPkixParams(validationDate, trustedRoot, intermediateCerts, leafCert); |
| CertPath certPath = buildCertPath(pkixParams); |
| |
| CertPathValidator certPathValidator; |
| try { |
| certPathValidator = CertPathValidator.getInstance(CERT_PATH_ALG); |
| } catch (NoSuchAlgorithmException e) { |
| // Should not happen, as PKIX is mandatory for all providers. |
| throw new RuntimeException(e); |
| } |
| try { |
| certPathValidator.validate(certPath, pkixParams); |
| } catch (CertPathValidatorException | InvalidAlgorithmParameterException e) { |
| throw new CertValidationException(e); |
| } |
| return certPath; |
| } |
| |
| /** |
| * Validates a given {@code CertPath} against the trusted root certificate. |
| * |
| * @param trustedRoot the trusted root certificate |
| * @param certPath the certificate path to be validated |
| * @throws CertValidationException if the given certificate path is invalid, e.g., is expired, |
| * or does not have a valid signature |
| */ |
| public static void validateCertPath(X509Certificate trustedRoot, CertPath certPath) |
| throws CertValidationException { |
| validateCertPath(/*validationDate=*/ null, trustedRoot, certPath); |
| } |
| |
| /** |
| * Validates a given {@code CertPath} against a given {@code validationDate}. If the given |
| * validation date is null, the current date will be used. |
| */ |
| @VisibleForTesting |
| static void validateCertPath(@Nullable Date validationDate, X509Certificate trustedRoot, |
| CertPath certPath) throws CertValidationException { |
| if (certPath.getCertificates().isEmpty()) { |
| throw new CertValidationException("The given certificate path is empty"); |
| } |
| if (!(certPath.getCertificates().get(0) instanceof X509Certificate)) { |
| throw new CertValidationException( |
| "The given certificate path does not contain X509 certificates"); |
| } |
| |
| List<X509Certificate> certificates = (List<X509Certificate>) certPath.getCertificates(); |
| X509Certificate leafCert = certificates.get(0); |
| List<X509Certificate> intermediateCerts = |
| certificates.subList(/*fromIndex=*/ 1, certificates.size()); |
| |
| validateCert(validationDate, trustedRoot, intermediateCerts, leafCert); |
| } |
| |
| @VisibleForTesting |
| static CertPath buildCertPath(PKIXParameters pkixParams) throws CertValidationException { |
| CertPathBuilder certPathBuilder; |
| try { |
| certPathBuilder = CertPathBuilder.getInstance(CERT_PATH_ALG); |
| } catch (NoSuchAlgorithmException e) { |
| // Should not happen, as PKIX is mandatory for all providers. |
| throw new RuntimeException(e); |
| } |
| try { |
| return certPathBuilder.build(pkixParams).getCertPath(); |
| } catch (CertPathBuilderException | InvalidAlgorithmParameterException e) { |
| throw new CertValidationException(e); |
| } |
| } |
| |
| @VisibleForTesting |
| static PKIXParameters buildPkixParams( |
| @Nullable Date validationDate, |
| X509Certificate trustedRoot, |
| List<X509Certificate> intermediateCerts, |
| X509Certificate leafCert) |
| throws CertValidationException { |
| // Create a TrustAnchor from the trusted root certificate. |
| Set<TrustAnchor> trustedAnchors = new HashSet<>(); |
| trustedAnchors.add(new TrustAnchor(trustedRoot, null)); |
| |
| // Create a CertStore from the list of intermediate certificates. |
| List<X509Certificate> certs = new ArrayList<>(intermediateCerts); |
| certs.add(leafCert); |
| CertStore certStore; |
| try { |
| certStore = |
| CertStore.getInstance(CERT_STORE_ALG, new CollectionCertStoreParameters(certs)); |
| } catch (NoSuchAlgorithmException e) { |
| // Should not happen, as Collection is mandatory for all providers. |
| throw new RuntimeException(e); |
| } catch (InvalidAlgorithmParameterException e) { |
| throw new CertValidationException(e); |
| } |
| |
| // Create a CertSelector from the leaf certificate. |
| X509CertSelector certSelector = new X509CertSelector(); |
| certSelector.setCertificate(leafCert); |
| |
| // Build a PKIXParameters from TrustAnchor, CertStore, and CertSelector. |
| PKIXBuilderParameters pkixParams; |
| try { |
| pkixParams = new PKIXBuilderParameters(trustedAnchors, certSelector); |
| } catch (InvalidAlgorithmParameterException e) { |
| throw new CertValidationException(e); |
| } |
| pkixParams.addCertStore(certStore); |
| |
| // If validationDate is null, the current time will be used. |
| pkixParams.setDate(validationDate); |
| pkixParams.setRevocationEnabled(false); |
| |
| return pkixParams; |
| } |
| } |