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