• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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      * @param validationDate use null for current time
309      * @throws CertValidationException if the given certificate path is invalid, e.g., is expired,
310      *                                 or does not have a valid signature
311      */
validateCertPath(X509Certificate trustedRoot, CertPath certPath, @Nullable Date validationDate)312     public static void validateCertPath(X509Certificate trustedRoot, CertPath certPath,
313                 @Nullable Date validationDate) throws CertValidationException {
314         validateCertPath(validationDate, trustedRoot, certPath);
315     }
316 
317     /**
318      * Validates a given {@code CertPath} against a given {@code validationDate}. If the given
319      * validation date is null, the current date will be used.
320      */
321     @VisibleForTesting
validateCertPath(@ullable Date validationDate, X509Certificate trustedRoot, CertPath certPath)322     static void validateCertPath(@Nullable Date validationDate, X509Certificate trustedRoot,
323             CertPath certPath) throws CertValidationException {
324         if (certPath.getCertificates().isEmpty()) {
325             throw new CertValidationException("The given certificate path is empty");
326         }
327         if (!(certPath.getCertificates().get(0) instanceof X509Certificate)) {
328             throw new CertValidationException(
329                     "The given certificate path does not contain X509 certificates");
330         }
331 
332         List<X509Certificate> certificates = (List<X509Certificate>) certPath.getCertificates();
333         X509Certificate leafCert = certificates.get(0);
334         List<X509Certificate> intermediateCerts =
335                 certificates.subList(/*fromIndex=*/ 1, certificates.size());
336 
337         validateCert(validationDate, trustedRoot, intermediateCerts, leafCert);
338     }
339 
340     @VisibleForTesting
buildCertPath(PKIXParameters pkixParams)341     static CertPath buildCertPath(PKIXParameters pkixParams) throws CertValidationException {
342         CertPathBuilder certPathBuilder;
343         try {
344             certPathBuilder = CertPathBuilder.getInstance(CERT_PATH_ALG);
345         } catch (NoSuchAlgorithmException e) {
346             // Should not happen, as PKIX is mandatory for all providers.
347             throw new RuntimeException(e);
348         }
349         try {
350             return certPathBuilder.build(pkixParams).getCertPath();
351         } catch (CertPathBuilderException | InvalidAlgorithmParameterException e) {
352             throw new CertValidationException(e);
353         }
354     }
355 
356     @VisibleForTesting
buildPkixParams( @ullable Date validationDate, X509Certificate trustedRoot, List<X509Certificate> intermediateCerts, X509Certificate leafCert)357     static PKIXParameters buildPkixParams(
358             @Nullable Date validationDate,
359             X509Certificate trustedRoot,
360             List<X509Certificate> intermediateCerts,
361             X509Certificate leafCert)
362             throws CertValidationException {
363         // Create a TrustAnchor from the trusted root certificate.
364         Set<TrustAnchor> trustedAnchors = new HashSet<>();
365         trustedAnchors.add(new TrustAnchor(trustedRoot, null));
366 
367         // Create a CertStore from the list of intermediate certificates.
368         List<X509Certificate> certs = new ArrayList<>(intermediateCerts);
369         certs.add(leafCert);
370         CertStore certStore;
371         try {
372             certStore =
373                     CertStore.getInstance(CERT_STORE_ALG, new CollectionCertStoreParameters(certs));
374         } catch (NoSuchAlgorithmException e) {
375             // Should not happen, as Collection is mandatory for all providers.
376             throw new RuntimeException(e);
377         } catch (InvalidAlgorithmParameterException e) {
378             throw new CertValidationException(e);
379         }
380 
381         // Create a CertSelector from the leaf certificate.
382         X509CertSelector certSelector = new X509CertSelector();
383         certSelector.setCertificate(leafCert);
384 
385         // Build a PKIXParameters from TrustAnchor, CertStore, and CertSelector.
386         PKIXBuilderParameters pkixParams;
387         try {
388             pkixParams = new PKIXBuilderParameters(trustedAnchors, certSelector);
389         } catch (InvalidAlgorithmParameterException e) {
390             throw new CertValidationException(e);
391         }
392         pkixParams.addCertStore(certStore);
393 
394         // If validationDate is null, the current time will be used.
395         pkixParams.setDate(validationDate);
396         pkixParams.setRevocationEnabled(false);
397 
398         return pkixParams;
399     }
400 }
401