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