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