1 /* 2 * Copyright (C) 2011 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 org.conscrypt; 18 19 import java.io.BufferedInputStream; 20 import java.io.File; 21 import java.io.FileInputStream; 22 import java.io.FileOutputStream; 23 import java.io.IOException; 24 import java.io.InputStream; 25 import java.io.OutputStream; 26 import java.security.cert.Certificate; 27 import java.security.cert.CertificateException; 28 import java.security.cert.CertificateFactory; 29 import java.security.cert.X509Certificate; 30 import java.util.ArrayList; 31 import java.util.Collections; 32 import java.util.Date; 33 import java.util.HashSet; 34 import java.util.LinkedHashSet; 35 import java.util.List; 36 import java.util.Set; 37 import javax.security.auth.x500.X500Principal; 38 import libcore.io.IoUtils; 39 40 /** 41 * A source for trusted root certificate authority (CA) certificates 42 * supporting an immutable system CA directory along with mutable 43 * directories allowing the user addition of custom CAs and user 44 * removal of system CAs. This store supports the {@code 45 * TrustedCertificateKeyStoreSpi} wrapper to allow a traditional 46 * KeyStore interface for use with {@link 47 * javax.net.ssl.TrustManagerFactory.init}. 48 * 49 * <p>The CAs are accessed via {@code KeyStore} style aliases. Aliases 50 * are made up of a prefix identifying the source ("system:" vs 51 * "user:") and a suffix based on the OpenSSL X509_NAME_hash_old 52 * function of the CA's subject name. For example, the system CA for 53 * "C=US, O=VeriSign, Inc., OU=Class 3 Public Primary Certification 54 * Authority" could be represented as "system:7651b327.0". By using 55 * the subject hash, operations such as {@link #getCertificateAlias 56 * getCertificateAlias} can be implemented efficiently without 57 * scanning the entire store. 58 * 59 * <p>In addition to supporting the {@code 60 * TrustedCertificateKeyStoreSpi} implementation, {@code 61 * TrustedCertificateStore} also provides the additional public 62 * methods {@link #isTrustAnchor} and {@link #findIssuer} to allow 63 * efficient lookup operations for CAs again based on the file naming 64 * convention. 65 * 66 * <p>The KeyChainService users the {@link installCertificate} and 67 * {@link #deleteCertificateEntry} to install user CAs as well as 68 * delete those user CAs as well as system CAs. The deletion of system 69 * CAs is performed by placing an exact copy of that CA in the deleted 70 * directory. Such deletions are intended to persist across upgrades 71 * but not intended to mask a CA with a matching name or public key 72 * but is otherwise reissued in a system update. Reinstalling a 73 * deleted system certificate simply removes the copy from the deleted 74 * directory, reenabling the original in the system directory. 75 * 76 * <p>Note that the default mutable directory is created by init via 77 * configuration in the system/core/rootdir/init.rc file. The 78 * directive "mkdir /data/misc/keychain 0775 system system" 79 * ensures that its owner and group are the system uid and system 80 * gid and that it is world readable but only writable by the system 81 * user. 82 * 83 * @hide 84 */ 85 @Internal 86 public class TrustedCertificateStore { 87 88 private static final String PREFIX_SYSTEM = "system:"; 89 private static final String PREFIX_USER = "user:"; 90 isSystem(String alias)91 public static final boolean isSystem(String alias) { 92 return alias.startsWith(PREFIX_SYSTEM); 93 } isUser(String alias)94 public static final boolean isUser(String alias) { 95 return alias.startsWith(PREFIX_USER); 96 } 97 98 private static class PreloadHolder { 99 private static File defaultCaCertsSystemDir; 100 private static File defaultCaCertsAddedDir; 101 private static File defaultCaCertsDeletedDir; 102 103 static { 104 String ANDROID_ROOT = System.getenv("ANDROID_ROOT"); 105 String ANDROID_DATA = System.getenv("ANDROID_DATA"); 106 defaultCaCertsSystemDir = new File(ANDROID_ROOT + "/etc/security/cacerts"); setDefaultUserDirectory(new File(ANDROID_DATA + "/misc/keychain"))107 setDefaultUserDirectory(new File(ANDROID_DATA + "/misc/keychain")); 108 } 109 } 110 111 private static final CertificateFactory CERT_FACTORY; 112 static { 113 try { 114 CERT_FACTORY = CertificateFactory.getInstance("X509"); 115 } catch (CertificateException e) { 116 throw new AssertionError(e); 117 } 118 } 119 setDefaultUserDirectory(File root)120 public static void setDefaultUserDirectory(File root) { 121 PreloadHolder.defaultCaCertsAddedDir = new File(root, "cacerts-added"); 122 PreloadHolder.defaultCaCertsDeletedDir = new File(root, "cacerts-removed"); 123 } 124 125 private final File systemDir; 126 private final File addedDir; 127 private final File deletedDir; 128 TrustedCertificateStore()129 public TrustedCertificateStore() { 130 this(PreloadHolder.defaultCaCertsSystemDir, PreloadHolder.defaultCaCertsAddedDir, 131 PreloadHolder.defaultCaCertsDeletedDir); 132 } 133 TrustedCertificateStore(File systemDir, File addedDir, File deletedDir)134 public TrustedCertificateStore(File systemDir, File addedDir, File deletedDir) { 135 this.systemDir = systemDir; 136 this.addedDir = addedDir; 137 this.deletedDir = deletedDir; 138 } 139 getCertificate(String alias)140 public Certificate getCertificate(String alias) { 141 return getCertificate(alias, false); 142 } 143 getCertificate(String alias, boolean includeDeletedSystem)144 public Certificate getCertificate(String alias, boolean includeDeletedSystem) { 145 146 File file = fileForAlias(alias); 147 if (file == null || (isUser(alias) && isTombstone(file))) { 148 return null; 149 } 150 X509Certificate cert = readCertificate(file); 151 if (cert == null || (isSystem(alias) 152 && !includeDeletedSystem 153 && isDeletedSystemCertificate(cert))) { 154 // skip malformed certs as well as deleted system ones 155 return null; 156 } 157 return cert; 158 } 159 fileForAlias(String alias)160 private File fileForAlias(String alias) { 161 if (alias == null) { 162 throw new NullPointerException("alias == null"); 163 } 164 File file; 165 if (isSystem(alias)) { 166 file = new File(systemDir, alias.substring(PREFIX_SYSTEM.length())); 167 } else if (isUser(alias)) { 168 file = new File(addedDir, alias.substring(PREFIX_USER.length())); 169 } else { 170 return null; 171 } 172 if (!file.exists() || isTombstone(file)) { 173 // silently elide tombstones 174 return null; 175 } 176 return file; 177 } 178 isTombstone(File file)179 private boolean isTombstone(File file) { 180 return file.length() == 0; 181 } 182 readCertificate(File file)183 private X509Certificate readCertificate(File file) { 184 if (!file.isFile()) { 185 return null; 186 } 187 InputStream is = null; 188 try { 189 is = new BufferedInputStream(new FileInputStream(file)); 190 return (X509Certificate) CERT_FACTORY.generateCertificate(is); 191 } catch (IOException e) { 192 return null; 193 } catch (CertificateException e) { 194 // reading a cert while its being installed can lead to this. 195 // just pretend like its not available yet. 196 return null; 197 } finally { 198 IoUtils.closeQuietly(is); 199 } 200 } 201 writeCertificate(File file, X509Certificate cert)202 private void writeCertificate(File file, X509Certificate cert) 203 throws IOException, CertificateException { 204 File dir = file.getParentFile(); 205 dir.mkdirs(); 206 dir.setReadable(true, false); 207 dir.setExecutable(true, false); 208 OutputStream os = null; 209 try { 210 os = new FileOutputStream(file); 211 os.write(cert.getEncoded()); 212 } finally { 213 IoUtils.closeQuietly(os); 214 } 215 file.setReadable(true, false); 216 } 217 isDeletedSystemCertificate(X509Certificate x)218 private boolean isDeletedSystemCertificate(X509Certificate x) { 219 return getCertificateFile(deletedDir, x).exists(); 220 } 221 getCreationDate(String alias)222 public Date getCreationDate(String alias) { 223 // containsAlias check ensures the later fileForAlias result 224 // was not a deleted system cert. 225 if (!containsAlias(alias)) { 226 return null; 227 } 228 File file = fileForAlias(alias); 229 if (file == null) { 230 return null; 231 } 232 long time = file.lastModified(); 233 if (time == 0) { 234 return null; 235 } 236 return new Date(time); 237 } 238 aliases()239 public Set<String> aliases() { 240 Set<String> result = new HashSet<String>(); 241 addAliases(result, PREFIX_USER, addedDir); 242 addAliases(result, PREFIX_SYSTEM, systemDir); 243 return result; 244 } 245 userAliases()246 public Set<String> userAliases() { 247 Set<String> result = new HashSet<String>(); 248 addAliases(result, PREFIX_USER, addedDir); 249 return result; 250 } 251 addAliases(Set<String> result, String prefix, File dir)252 private void addAliases(Set<String> result, String prefix, File dir) { 253 String[] files = dir.list(); 254 if (files == null) { 255 return; 256 } 257 for (String filename : files) { 258 String alias = prefix + filename; 259 if (containsAlias(alias)) { 260 result.add(alias); 261 } 262 } 263 } 264 allSystemAliases()265 public Set<String> allSystemAliases() { 266 Set<String> result = new HashSet<String>(); 267 String[] files = systemDir.list(); 268 if (files == null) { 269 return result; 270 } 271 for (String filename : files) { 272 String alias = PREFIX_SYSTEM + filename; 273 if (containsAlias(alias, true)) { 274 result.add(alias); 275 } 276 } 277 return result; 278 } 279 containsAlias(String alias)280 public boolean containsAlias(String alias) { 281 return containsAlias(alias, false); 282 } 283 containsAlias(String alias, boolean includeDeletedSystem)284 private boolean containsAlias(String alias, boolean includeDeletedSystem) { 285 return getCertificate(alias, includeDeletedSystem) != null; 286 } 287 getCertificateAlias(Certificate c)288 public String getCertificateAlias(Certificate c) { 289 return getCertificateAlias(c, false); 290 } 291 getCertificateAlias(Certificate c, boolean includeDeletedSystem)292 public String getCertificateAlias(Certificate c, boolean includeDeletedSystem) { 293 if (c == null || !(c instanceof X509Certificate)) { 294 return null; 295 } 296 X509Certificate x = (X509Certificate) c; 297 File user = getCertificateFile(addedDir, x); 298 if (user.exists()) { 299 return PREFIX_USER + user.getName(); 300 } 301 if (!includeDeletedSystem && isDeletedSystemCertificate(x)) { 302 return null; 303 } 304 File system = getCertificateFile(systemDir, x); 305 if (system.exists()) { 306 return PREFIX_SYSTEM + system.getName(); 307 } 308 return null; 309 } 310 311 /** 312 * Returns true to indicate that the certificate was added by the 313 * user, false otherwise. 314 */ isUserAddedCertificate(X509Certificate cert)315 public boolean isUserAddedCertificate(X509Certificate cert) { 316 return getCertificateFile(addedDir, cert).exists(); 317 } 318 319 /** 320 * Returns a File for where the certificate is found if it exists 321 * or where it should be installed if it does not exist. The 322 * caller can disambiguate these cases by calling {@code 323 * File.exists()} on the result. 324 * 325 * @VisibleForTesting 326 */ getCertificateFile(File dir, final X509Certificate x)327 public File getCertificateFile(File dir, final X509Certificate x) { 328 // compare X509Certificate.getEncoded values 329 CertSelector selector = new CertSelector() { 330 @Override 331 public boolean match(X509Certificate cert) { 332 return cert.equals(x); 333 } 334 }; 335 return findCert(dir, x.getSubjectX500Principal(), selector, File.class); 336 } 337 338 /** 339 * This non-{@code KeyStoreSpi} public interface is used by {@code 340 * TrustManagerImpl} to locate a CA certificate with the same name 341 * and public key as the provided {@code X509Certificate}. We 342 * match on the name and public key and not the entire certificate 343 * since a CA may be reissued with the same name and PublicKey but 344 * with other differences (for example when switching signature 345 * from md2WithRSAEncryption to SHA1withRSA) 346 */ getTrustAnchor(final X509Certificate c)347 public X509Certificate getTrustAnchor(final X509Certificate c) { 348 // compare X509Certificate.getPublicKey values 349 CertSelector selector = new CertSelector() { 350 @Override 351 public boolean match(X509Certificate ca) { 352 return ca.getPublicKey().equals(c.getPublicKey()); 353 } 354 }; 355 X509Certificate user = findCert(addedDir, 356 c.getSubjectX500Principal(), 357 selector, 358 X509Certificate.class); 359 if (user != null) { 360 return user; 361 } 362 X509Certificate system = findCert(systemDir, 363 c.getSubjectX500Principal(), 364 selector, 365 X509Certificate.class); 366 if (system != null && !isDeletedSystemCertificate(system)) { 367 return system; 368 } 369 return null; 370 } 371 372 /** 373 * This non-{@code KeyStoreSpi} public interface is used by {@code 374 * TrustManagerImpl} to locate the CA certificate that signed the 375 * provided {@code X509Certificate}. 376 */ findIssuer(final X509Certificate c)377 public X509Certificate findIssuer(final X509Certificate c) { 378 // match on verified issuer of Certificate 379 CertSelector selector = new CertSelector() { 380 @Override 381 public boolean match(X509Certificate ca) { 382 try { 383 c.verify(ca.getPublicKey()); 384 return true; 385 } catch (Exception e) { 386 return false; 387 } 388 } 389 }; 390 X500Principal issuer = c.getIssuerX500Principal(); 391 X509Certificate user = findCert(addedDir, issuer, selector, X509Certificate.class); 392 if (user != null) { 393 return user; 394 } 395 X509Certificate system = findCert(systemDir, issuer, selector, X509Certificate.class); 396 if (system != null && !isDeletedSystemCertificate(system)) { 397 return system; 398 } 399 return null; 400 } 401 findAllIssuers(final X509Certificate c)402 public Set<X509Certificate> findAllIssuers(final X509Certificate c) { 403 Set<X509Certificate> issuers = null; 404 CertSelector selector = new CertSelector() { 405 @Override 406 public boolean match(X509Certificate ca) { 407 try { 408 c.verify(ca.getPublicKey()); 409 return true; 410 } catch (Exception e) { 411 return false; 412 } 413 } 414 }; 415 X500Principal issuer = c.getIssuerX500Principal(); 416 Set<X509Certificate> userAddedCerts = findCert(addedDir, issuer, selector, Set.class); 417 if (userAddedCerts != null) { 418 issuers = userAddedCerts; 419 } 420 selector = new CertSelector() { 421 @Override 422 public boolean match(X509Certificate ca) { 423 try { 424 if (isDeletedSystemCertificate(ca)) { 425 return false; 426 } 427 c.verify(ca.getPublicKey()); 428 return true; 429 } catch (Exception e) { 430 return false; 431 } 432 } 433 }; 434 Set<X509Certificate> systemCerts = findCert(systemDir, issuer, selector, Set.class); 435 if (systemCerts != null) { 436 if (issuers != null) { 437 issuers.addAll(systemCerts); 438 } else { 439 issuers = systemCerts; 440 } 441 } 442 return (issuers != null) ? issuers : Collections.<X509Certificate>emptySet(); 443 } 444 isSelfIssuedCertificate(OpenSSLX509Certificate cert)445 private static boolean isSelfIssuedCertificate(OpenSSLX509Certificate cert) { 446 final long ctx = cert.getContext(); 447 return NativeCrypto.X509_check_issued(ctx, ctx) == 0; 448 } 449 450 /** 451 * Converts the {@code cert} to the internal OpenSSL X.509 format so we can 452 * run {@link NativeCrypto} methods on it. 453 */ convertToOpenSSLIfNeeded(X509Certificate cert)454 private static OpenSSLX509Certificate convertToOpenSSLIfNeeded(X509Certificate cert) 455 throws CertificateException { 456 if (cert == null) { 457 return null; 458 } 459 460 if (cert instanceof OpenSSLX509Certificate) { 461 return (OpenSSLX509Certificate) cert; 462 } 463 464 try { 465 return OpenSSLX509Certificate.fromX509Der(cert.getEncoded()); 466 } catch (Exception e) { 467 throw new CertificateException(e); 468 } 469 } 470 471 /** 472 * Attempt to build a certificate chain from the supplied {@code leaf} 473 * argument through the chain of issuers as high up as known. If the chain 474 * can't be completed, the most complete chain available will be returned. 475 * This means that a list with only the {@code leaf} certificate is returned 476 * if no issuer certificates could be found. 477 * 478 * @throws CertificateException if there was a problem parsing the 479 * certificates 480 */ getCertificateChain(X509Certificate leaf)481 public List<X509Certificate> getCertificateChain(X509Certificate leaf) 482 throws CertificateException { 483 final LinkedHashSet<OpenSSLX509Certificate> chain 484 = new LinkedHashSet<OpenSSLX509Certificate>(); 485 OpenSSLX509Certificate cert = convertToOpenSSLIfNeeded(leaf); 486 chain.add(cert); 487 488 while (true) { 489 if (isSelfIssuedCertificate(cert)) { 490 break; 491 } 492 cert = convertToOpenSSLIfNeeded(findIssuer(cert)); 493 if (cert == null || chain.contains(cert)) { 494 break; 495 } 496 chain.add(cert); 497 } 498 499 return new ArrayList<X509Certificate>(chain); 500 } 501 502 // like java.security.cert.CertSelector but with X509Certificate and without cloning 503 private static interface CertSelector { match(X509Certificate cert)504 public boolean match(X509Certificate cert); 505 } 506 findCert( File dir, X500Principal subject, CertSelector selector, Class<T> desiredReturnType)507 private <T> T findCert( 508 File dir, X500Principal subject, CertSelector selector, Class<T> desiredReturnType) { 509 510 Set<X509Certificate> certs = null; 511 String hash = hash(subject); 512 for (int index = 0; true; index++) { 513 File file = file(dir, hash, index); 514 if (!file.isFile()) { 515 // could not find a match, no file exists, bail 516 if (desiredReturnType == Boolean.class) { 517 return (T) Boolean.FALSE; 518 } 519 if (desiredReturnType == File.class) { 520 // we return file so that caller that wants to 521 // write knows what the next available has 522 // location is 523 return (T) file; 524 } 525 if (desiredReturnType == Set.class) { 526 return (T) certs; 527 } 528 return null; 529 } 530 if (isTombstone(file)) { 531 continue; 532 } 533 X509Certificate cert = readCertificate(file); 534 if (cert == null) { 535 // skip problem certificates 536 continue; 537 } 538 if (selector.match(cert)) { 539 if (desiredReturnType == X509Certificate.class) { 540 return (T) cert; 541 } else if (desiredReturnType == Boolean.class) { 542 return (T) Boolean.TRUE; 543 } else if (desiredReturnType == File.class) { 544 return (T) file; 545 } else if (desiredReturnType == Set.class) { 546 if (certs == null) { 547 certs = new HashSet<X509Certificate>(); 548 } 549 certs.add((X509Certificate) cert); 550 } else { 551 throw new AssertionError(); 552 } 553 } 554 } 555 } 556 hash(X500Principal name)557 private String hash(X500Principal name) { 558 int hash = NativeCrypto.X509_NAME_hash_old(name); 559 return Hex.intToHexString(hash, 8); 560 } 561 file(File dir, String hash, int index)562 private File file(File dir, String hash, int index) { 563 return new File(dir, hash + '.' + index); 564 } 565 566 /** 567 * This non-{@code KeyStoreSpi} public interface is used by the 568 * {@code KeyChainService} to install new CA certificates. It 569 * silently ignores the certificate if it already exists in the 570 * store. 571 */ installCertificate(X509Certificate cert)572 public void installCertificate(X509Certificate cert) throws IOException, CertificateException { 573 if (cert == null) { 574 throw new NullPointerException("cert == null"); 575 } 576 File system = getCertificateFile(systemDir, cert); 577 if (system.exists()) { 578 File deleted = getCertificateFile(deletedDir, cert); 579 if (deleted.exists()) { 580 // we have a system cert that was marked deleted. 581 // remove the deleted marker to expose the original 582 if (!deleted.delete()) { 583 throw new IOException("Could not remove " + deleted); 584 } 585 return; 586 } 587 // otherwise we just have a dup of an existing system cert. 588 // return taking no further action. 589 return; 590 } 591 File user = getCertificateFile(addedDir, cert); 592 if (user.exists()) { 593 // we have an already installed user cert, bail. 594 return; 595 } 596 // install the user cert 597 writeCertificate(user, cert); 598 } 599 600 /** 601 * This could be considered the implementation of {@code 602 * TrustedCertificateKeyStoreSpi.engineDeleteEntry} but we 603 * consider {@code TrustedCertificateKeyStoreSpi} to be read 604 * only. Instead, this is used by the {@code KeyChainService} to 605 * delete CA certificates. 606 */ deleteCertificateEntry(String alias)607 public void deleteCertificateEntry(String alias) throws IOException, CertificateException { 608 if (alias == null) { 609 return; 610 } 611 File file = fileForAlias(alias); 612 if (file == null) { 613 return; 614 } 615 if (isSystem(alias)) { 616 X509Certificate cert = readCertificate(file); 617 if (cert == null) { 618 // skip problem certificates 619 return; 620 } 621 File deleted = getCertificateFile(deletedDir, cert); 622 if (deleted.exists()) { 623 // already deleted system certificate 624 return; 625 } 626 // write copy of system cert to marked as deleted 627 writeCertificate(deleted, cert); 628 return; 629 } 630 if (isUser(alias)) { 631 // truncate the file to make a tombstone by opening and closing. 632 // we need ensure that we don't leave a gap before a valid cert. 633 new FileOutputStream(file).close(); 634 removeUnnecessaryTombstones(alias); 635 return; 636 } 637 // non-existant user cert, nothing to delete 638 } 639 removeUnnecessaryTombstones(String alias)640 private void removeUnnecessaryTombstones(String alias) throws IOException { 641 if (!isUser(alias)) { 642 throw new AssertionError(alias); 643 } 644 int dotIndex = alias.lastIndexOf('.'); 645 if (dotIndex == -1) { 646 throw new AssertionError(alias); 647 } 648 649 String hash = alias.substring(PREFIX_USER.length(), dotIndex); 650 int lastTombstoneIndex = Integer.parseInt(alias.substring(dotIndex + 1)); 651 652 if (file(addedDir, hash, lastTombstoneIndex + 1).exists()) { 653 return; 654 } 655 while (lastTombstoneIndex >= 0) { 656 File file = file(addedDir, hash, lastTombstoneIndex); 657 if (!isTombstone(file)) { 658 break; 659 } 660 if (!file.delete()) { 661 throw new IOException("Could not remove " + file); 662 } 663 lastTombstoneIndex--; 664 } 665 } 666 } 667