1/*
2 * Copyright (C) 2017 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.server.locksettings.recoverablekeystore.certificate;
18
19import static javax.xml.xpath.XPathConstants.NODESET;
20
21import android.annotation.IntDef;
22import android.annotation.Nullable;
23
24import com.android.internal.annotations.VisibleForTesting;
25
26import java.io.ByteArrayInputStream;
27import java.io.IOException;
28import java.io.InputStream;
29import java.lang.annotation.Retention;
30import java.lang.annotation.RetentionPolicy;
31import java.security.InvalidAlgorithmParameterException;
32import java.security.InvalidKeyException;
33import java.security.NoSuchAlgorithmException;
34import java.security.PublicKey;
35import java.security.Signature;
36import java.security.SignatureException;
37import java.security.cert.CertPath;
38import java.security.cert.CertPathBuilder;
39import java.security.cert.CertPathBuilderException;
40import java.security.cert.CertPathValidator;
41import java.security.cert.CertPathValidatorException;
42import java.security.cert.CertStore;
43import java.security.cert.Certificate;
44import java.security.cert.CertificateException;
45import java.security.cert.CertificateFactory;
46import java.security.cert.CollectionCertStoreParameters;
47import java.security.cert.PKIXBuilderParameters;
48import java.security.cert.PKIXParameters;
49import java.security.cert.TrustAnchor;
50import java.security.cert.X509CertSelector;
51import java.security.cert.X509Certificate;
52import java.util.ArrayList;
53import java.util.Base64;
54import java.util.Date;
55import java.util.HashSet;
56import java.util.List;
57import java.util.Set;
58
59import javax.xml.parsers.DocumentBuilderFactory;
60import javax.xml.parsers.ParserConfigurationException;
61import javax.xml.xpath.XPath;
62import javax.xml.xpath.XPathExpressionException;
63import javax.xml.xpath.XPathFactory;
64
65import org.w3c.dom.Document;
66import org.w3c.dom.Element;
67import org.w3c.dom.Node;
68import org.w3c.dom.NodeList;
69import org.xml.sax.SAXException;
70
71/** Utility functions related to parsing and validating public-key certificates. */
72public final class CertUtils {
73
74    private static final String CERT_FORMAT = "X.509";
75    private static final String CERT_PATH_ALG = "PKIX";
76    private static final String CERT_STORE_ALG = "Collection";
77    private static final String SIGNATURE_ALG = "SHA256withRSA";
78
79    @Retention(RetentionPolicy.SOURCE)
80    @IntDef({MUST_EXIST_UNENFORCED, MUST_EXIST_EXACTLY_ONE, MUST_EXIST_AT_LEAST_ONE})
81    @interface MustExist {}
82    static final int MUST_EXIST_UNENFORCED = 0;
83    static final int MUST_EXIST_EXACTLY_ONE = 1;
84    static final int MUST_EXIST_AT_LEAST_ONE = 2;
85
86    private CertUtils() {}
87
88    /**
89     * Decodes a byte array containing an encoded X509 certificate.
90     *
91     * @param certBytes the byte array containing the encoded X509 certificate
92     * @return the decoded X509 certificate
93     * @throws CertParsingException if any parsing error occurs
94     */
95    static X509Certificate decodeCert(byte[] certBytes) throws CertParsingException {
96        return decodeCert(new ByteArrayInputStream(certBytes));
97    }
98
99    /**
100     * Decodes an X509 certificate from an {@code InputStream}.
101     *
102     * @param inStream the input stream containing the encoded X509 certificate
103     * @return the decoded X509 certificate
104     * @throws CertParsingException if any parsing error occurs
105     */
106    static X509Certificate decodeCert(InputStream inStream) throws CertParsingException {
107        CertificateFactory certFactory;
108        try {
109            certFactory = CertificateFactory.getInstance(CERT_FORMAT);
110        } catch (CertificateException e) {
111            // Should not happen, as X.509 is mandatory for all providers.
112            throw new RuntimeException(e);
113        }
114        try {
115            return (X509Certificate) certFactory.generateCertificate(inStream);
116        } catch (CertificateException e) {
117            throw new CertParsingException(e);
118        }
119    }
120
121    /**
122     * Parses a byte array as the content of an XML file, and returns the root node of the XML file.
123     *
124     * @param xmlBytes the byte array that is the XML file content
125     * @return the root node of the XML file
126     * @throws CertParsingException if any parsing error occurs
127     */
128    static Element getXmlRootNode(byte[] xmlBytes) throws CertParsingException {
129        try {
130            Document document =
131                    DocumentBuilderFactory.newInstance()
132                            .newDocumentBuilder()
133                            .parse(new ByteArrayInputStream(xmlBytes));
134            document.getDocumentElement().normalize();
135            return document.getDocumentElement();
136        } catch (SAXException | ParserConfigurationException | IOException e) {
137            throw new CertParsingException(e);
138        }
139    }
140
141    /**
142     * Gets the text contents of certain XML child nodes, given a XML root node and a list of tags
143     * representing the path to locate the child nodes. The whitespaces and newlines in the text
144     * contents are stripped away.
145     *
146     * <p>For example, the list of tags [tag1, tag2, tag3] represents the XML tree like the
147     * following:
148     *
149     * <pre>
150     *   <root>
151     *     <tag1>
152     *       <tag2>
153     *         <tag3>abc</tag3>
154     *         <tag3>def</tag3>
155     *       </tag2>
156     *     </tag1>
157     *   <root>
158     * </pre>
159     *
160     * @param mustExist whether and how many nodes must exist. If the number of child nodes does not
161     *                  satisfy the requirement, CertParsingException will be thrown.
162     * @param rootNode  the root node that serves as the starting point to locate the child nodes
163     * @param nodeTags  the list of tags representing the relative path from the root node
164     * @return a list of strings that are the text contents of the child nodes
165     * @throws CertParsingException if any parsing error occurs
166     */
167    static List<String> getXmlNodeContents(@MustExist int mustExist, Element rootNode,
168            String... nodeTags)
169            throws CertParsingException {
170        String expression = String.join("/", nodeTags);
171
172        XPath xPath = XPathFactory.newInstance().newXPath();
173        NodeList nodeList;
174        try {
175            nodeList = (NodeList) xPath.compile(expression).evaluate(rootNode, NODESET);
176        } catch (XPathExpressionException e) {
177            throw new CertParsingException(e);
178        }
179
180        switch (mustExist) {
181            case MUST_EXIST_UNENFORCED:
182                break;
183
184            case MUST_EXIST_EXACTLY_ONE:
185                if (nodeList.getLength() != 1) {
186                    throw new CertParsingException(
187                            "The XML file must contain exactly one node with the path "
188                                    + expression);
189                }
190                break;
191
192            case MUST_EXIST_AT_LEAST_ONE:
193                if (nodeList.getLength() == 0) {
194                    throw new CertParsingException(
195                            "The XML file must contain at least one node with the path "
196                                    + expression);
197                }
198                break;
199
200            default:
201                throw new UnsupportedOperationException(
202                        "This value of MustExist is not supported: " + mustExist);
203        }
204
205        List<String> result = new ArrayList<>();
206        for (int i = 0; i < nodeList.getLength(); i++) {
207            Node node = nodeList.item(i);
208            // Remove whitespaces and newlines.
209            result.add(node.getTextContent().replaceAll("\\s", ""));
210        }
211        return result;
212    }
213
214    /**
215     * Decodes a base64-encoded string.
216     *
217     * @param str the base64-encoded string
218     * @return the decoding decoding result
219     * @throws CertParsingException if the input string is not a properly base64-encoded string
220     */
221    public static byte[] decodeBase64(String str) throws CertParsingException {
222        try {
223            return Base64.getDecoder().decode(str);
224        } catch (IllegalArgumentException e) {
225            throw new CertParsingException(e);
226        }
227    }
228
229    /**
230     * Verifies a public-key signature that is computed by RSA with SHA256.
231     *
232     * @param signerPublicKey the public key of the original signer
233     * @param signature       the public-key signature
234     * @param signedBytes     the bytes that have been signed
235     * @throws CertValidationException if the signature verification fails
236     */
237    static void verifyRsaSha256Signature(
238            PublicKey signerPublicKey, byte[] signature, byte[] signedBytes)
239            throws CertValidationException {
240        Signature verifier;
241        try {
242            verifier = Signature.getInstance(SIGNATURE_ALG);
243        } catch (NoSuchAlgorithmException e) {
244            // Should not happen, as SHA256withRSA is mandatory for all providers.
245            throw new RuntimeException(e);
246        }
247        try {
248            verifier.initVerify(signerPublicKey);
249            verifier.update(signedBytes);
250            if (!verifier.verify(signature)) {
251                throw new CertValidationException("The signature is invalid");
252            }
253        } catch (InvalidKeyException | SignatureException e) {
254            throw new CertValidationException(e);
255        }
256    }
257
258    /**
259     * Validates a leaf certificate, and returns the certificate path if the certificate is valid.
260     * If the given validation date is null, the current date will be used.
261     *
262     * @param validationDate    the date for which the validity of the certificate should be
263     *                          determined
264     * @param trustedRoot       the certificate of the trusted root CA
265     * @param intermediateCerts the list of certificates of possible intermediate CAs
266     * @param leafCert          the leaf certificate that is to be validated
267     * @return the certificate path if the leaf cert is valid
268     * @throws CertValidationException if {@code leafCert} is invalid (e.g., is expired, or has
269     *                                 invalid signature)
270     */
271    static CertPath validateCert(
272            @Nullable Date validationDate,
273            X509Certificate trustedRoot,
274            List<X509Certificate> intermediateCerts,
275            X509Certificate leafCert)
276            throws CertValidationException {
277        PKIXParameters pkixParams =
278                buildPkixParams(validationDate, trustedRoot, intermediateCerts, leafCert);
279        CertPath certPath = buildCertPath(pkixParams);
280
281        CertPathValidator certPathValidator;
282        try {
283            certPathValidator = CertPathValidator.getInstance(CERT_PATH_ALG);
284        } catch (NoSuchAlgorithmException e) {
285            // Should not happen, as PKIX is mandatory for all providers.
286            throw new RuntimeException(e);
287        }
288        try {
289            certPathValidator.validate(certPath, pkixParams);
290        } catch (CertPathValidatorException | InvalidAlgorithmParameterException e) {
291            throw new CertValidationException(e);
292        }
293        return certPath;
294    }
295
296    /**
297     * Validates a given {@code CertPath} against the trusted root certificate.
298     *
299     * @param trustedRoot the trusted root certificate
300     * @param certPath the certificate path to be validated
301     * @throws CertValidationException if the given certificate path is invalid, e.g., is expired,
302     *                                 or does not have a valid signature
303     */
304    public static void validateCertPath(X509Certificate trustedRoot, CertPath certPath)
305            throws CertValidationException {
306        validateCertPath(/*validationDate=*/ null, trustedRoot, certPath);
307    }
308
309    /**
310     * Validates a given {@code CertPath} against a given {@code validationDate}. If the given
311     * validation date is null, the current date will be used.
312     */
313    @VisibleForTesting
314    static void validateCertPath(@Nullable Date validationDate, X509Certificate trustedRoot,
315            CertPath certPath) throws CertValidationException {
316        if (certPath.getCertificates().isEmpty()) {
317            throw new CertValidationException("The given certificate path is empty");
318        }
319        if (!(certPath.getCertificates().get(0) instanceof X509Certificate)) {
320            throw new CertValidationException(
321                    "The given certificate path does not contain X509 certificates");
322        }
323
324        List<X509Certificate> certificates = (List<X509Certificate>) certPath.getCertificates();
325        X509Certificate leafCert = certificates.get(0);
326        List<X509Certificate> intermediateCerts =
327                certificates.subList(/*fromIndex=*/ 1, certificates.size());
328
329        validateCert(validationDate, trustedRoot, intermediateCerts, leafCert);
330    }
331
332    @VisibleForTesting
333    static CertPath buildCertPath(PKIXParameters pkixParams) throws CertValidationException {
334        CertPathBuilder certPathBuilder;
335        try {
336            certPathBuilder = CertPathBuilder.getInstance(CERT_PATH_ALG);
337        } catch (NoSuchAlgorithmException e) {
338            // Should not happen, as PKIX is mandatory for all providers.
339            throw new RuntimeException(e);
340        }
341        try {
342            return certPathBuilder.build(pkixParams).getCertPath();
343        } catch (CertPathBuilderException | InvalidAlgorithmParameterException e) {
344            throw new CertValidationException(e);
345        }
346    }
347
348    @VisibleForTesting
349    static PKIXParameters buildPkixParams(
350            @Nullable Date validationDate,
351            X509Certificate trustedRoot,
352            List<X509Certificate> intermediateCerts,
353            X509Certificate leafCert)
354            throws CertValidationException {
355        // Create a TrustAnchor from the trusted root certificate.
356        Set<TrustAnchor> trustedAnchors = new HashSet<>();
357        trustedAnchors.add(new TrustAnchor(trustedRoot, null));
358
359        // Create a CertStore from the list of intermediate certificates.
360        List<X509Certificate> certs = new ArrayList<>(intermediateCerts);
361        certs.add(leafCert);
362        CertStore certStore;
363        try {
364            certStore =
365                    CertStore.getInstance(CERT_STORE_ALG, new CollectionCertStoreParameters(certs));
366        } catch (NoSuchAlgorithmException e) {
367            // Should not happen, as Collection is mandatory for all providers.
368            throw new RuntimeException(e);
369        } catch (InvalidAlgorithmParameterException e) {
370            throw new CertValidationException(e);
371        }
372
373        // Create a CertSelector from the leaf certificate.
374        X509CertSelector certSelector = new X509CertSelector();
375        certSelector.setCertificate(leafCert);
376
377        // Build a PKIXParameters from TrustAnchor, CertStore, and CertSelector.
378        PKIXBuilderParameters pkixParams;
379        try {
380            pkixParams = new PKIXBuilderParameters(trustedAnchors, certSelector);
381        } catch (InvalidAlgorithmParameterException e) {
382            throw new CertValidationException(e);
383        }
384        pkixParams.addCertStore(certStore);
385
386        // If validationDate is null, the current time will be used.
387        pkixParams.setDate(validationDate);
388        pkixParams.setRevocationEnabled(false);
389
390        return pkixParams;
391    }
392}
393