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