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