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