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