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 87 public static final boolean isSystem(String alias) { 88 return alias.startsWith(PREFIX_SYSTEM); 89 } 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"); 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 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 120 public TrustedCertificateStore() { 121 this(defaultCaCertsSystemDir, defaultCaCertsAddedDir, defaultCaCertsDeletedDir); 122 } 123 124 public TrustedCertificateStore(File systemDir, File addedDir, File deletedDir) { 125 this.systemDir = systemDir; 126 this.addedDir = addedDir; 127 this.deletedDir = deletedDir; 128 } 129 130 public Certificate getCertificate(String alias) { 131 return getCertificate(alias, false); 132 } 133 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 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 169 private boolean isTombstone(File file) { 170 return file.length() == 0; 171 } 172 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 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 208 private boolean isDeletedSystemCertificate(X509Certificate x) { 209 return getCertificateFile(deletedDir, x).exists(); 210 } 211 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 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 236 public Set<String> userAliases() { 237 Set<String> result = new HashSet<String>(); 238 addAliases(result, PREFIX_USER, addedDir); 239 return result; 240 } 241 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 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 270 public boolean containsAlias(String alias) { 271 return containsAlias(alias, false); 272 } 273 274 private boolean containsAlias(String alias, boolean includeDeletedSystem) { 275 return getCertificate(alias, includeDeletedSystem) != null; 276 } 277 278 public String getCertificateAlias(Certificate c) { 279 return getCertificateAlias(c, false); 280 } 281 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 */ 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 private File getCertificateFile(File dir, final X509Certificate x) { 316 // compare X509Certificate.getEncoded values 317 CertSelector selector = new CertSelector() { 318 @Override 319 public boolean match(X509Certificate cert) { 320 return cert.equals(x); 321 } 322 }; 323 return findCert(dir, x.getSubjectX500Principal(), selector, File.class); 324 } 325 326 /** 327 * This non-{@code KeyStoreSpi} public interface is used by {@code 328 * TrustManagerImpl} to locate a CA certificate with the same name 329 * and public key as the provided {@code X509Certificate}. We 330 * match on the name and public key and not the entire certificate 331 * since a CA may be reissued with the same name and PublicKey but 332 * with other differences (for example when switching signature 333 * from md2WithRSAEncryption to SHA1withRSA) 334 */ 335 public X509Certificate getTrustAnchor(final X509Certificate c) { 336 // compare X509Certificate.getPublicKey values 337 CertSelector selector = new CertSelector() { 338 @Override 339 public boolean match(X509Certificate ca) { 340 return ca.getPublicKey().equals(c.getPublicKey()); 341 } 342 }; 343 X509Certificate user = findCert(addedDir, 344 c.getSubjectX500Principal(), 345 selector, 346 X509Certificate.class); 347 if (user != null) { 348 return user; 349 } 350 X509Certificate system = findCert(systemDir, 351 c.getSubjectX500Principal(), 352 selector, 353 X509Certificate.class); 354 if (system != null && !isDeletedSystemCertificate(system)) { 355 return system; 356 } 357 return null; 358 } 359 360 /** 361 * This non-{@code KeyStoreSpi} public interface is used by {@code 362 * TrustManagerImpl} to locate the CA certificate that signed the 363 * provided {@code X509Certificate}. 364 */ 365 public X509Certificate findIssuer(final X509Certificate c) { 366 // match on verified issuer of Certificate 367 CertSelector selector = new CertSelector() { 368 @Override 369 public boolean match(X509Certificate ca) { 370 try { 371 c.verify(ca.getPublicKey()); 372 return true; 373 } catch (Exception e) { 374 return false; 375 } 376 } 377 }; 378 X500Principal issuer = c.getIssuerX500Principal(); 379 X509Certificate user = findCert(addedDir, issuer, selector, X509Certificate.class); 380 if (user != null) { 381 return user; 382 } 383 X509Certificate system = findCert(systemDir, issuer, selector, X509Certificate.class); 384 if (system != null && !isDeletedSystemCertificate(system)) { 385 return system; 386 } 387 return null; 388 } 389 390 private static boolean isSelfIssuedCertificate(OpenSSLX509Certificate cert) { 391 final long ctx = cert.getContext(); 392 return NativeCrypto.X509_check_issued(ctx, ctx) == 0; 393 } 394 395 /** 396 * Converts the {@code cert} to the internal OpenSSL X.509 format so we can 397 * run {@link NativeCrypto} methods on it. 398 */ 399 private static OpenSSLX509Certificate convertToOpenSSLIfNeeded(X509Certificate cert) 400 throws CertificateException { 401 if (cert == null) { 402 return null; 403 } 404 405 if (cert instanceof OpenSSLX509Certificate) { 406 return (OpenSSLX509Certificate) cert; 407 } 408 409 try { 410 return OpenSSLX509Certificate.fromX509Der(cert.getEncoded()); 411 } catch (Exception e) { 412 throw new CertificateException(e); 413 } 414 } 415 416 /** 417 * Attempt to build a certificate chain from the supplied {@code leaf} 418 * argument through the chain of issuers as high up as known. If the chain 419 * can't be completed, the most complete chain available will be returned. 420 * This means that a list with only the {@code leaf} certificate is returned 421 * if no issuer certificates could be found. 422 * 423 * @throws CertificateException if there was a problem parsing the 424 * certificates 425 */ 426 public List<X509Certificate> getCertificateChain(X509Certificate leaf) 427 throws CertificateException { 428 final LinkedHashSet<OpenSSLX509Certificate> chain 429 = new LinkedHashSet<OpenSSLX509Certificate>(); 430 OpenSSLX509Certificate cert = convertToOpenSSLIfNeeded(leaf); 431 chain.add(cert); 432 433 while (true) { 434 if (isSelfIssuedCertificate(cert)) { 435 break; 436 } 437 cert = convertToOpenSSLIfNeeded(findIssuer(cert)); 438 if (cert == null || chain.contains(cert)) { 439 break; 440 } 441 chain.add(cert); 442 } 443 444 return new ArrayList<X509Certificate>(chain); 445 } 446 447 // like java.security.cert.CertSelector but with X509Certificate and without cloning 448 private static interface CertSelector { 449 public boolean match(X509Certificate cert); 450 } 451 452 private <T> T findCert( 453 File dir, X500Principal subject, CertSelector selector, Class<T> desiredReturnType) { 454 455 String hash = hash(subject); 456 for (int index = 0; true; index++) { 457 File file = file(dir, hash, index); 458 if (!file.isFile()) { 459 // could not find a match, no file exists, bail 460 if (desiredReturnType == Boolean.class) { 461 return (T) Boolean.FALSE; 462 } 463 if (desiredReturnType == File.class) { 464 // we return file so that caller that wants to 465 // write knows what the next available has 466 // location is 467 return (T) file; 468 } 469 return null; 470 } 471 if (isTombstone(file)) { 472 continue; 473 } 474 X509Certificate cert = readCertificate(file); 475 if (cert == null) { 476 // skip problem certificates 477 continue; 478 } 479 if (selector.match(cert)) { 480 if (desiredReturnType == X509Certificate.class) { 481 return (T) cert; 482 } 483 if (desiredReturnType == Boolean.class) { 484 return (T) Boolean.TRUE; 485 } 486 if (desiredReturnType == File.class) { 487 return (T) file; 488 } 489 throw new AssertionError(); 490 } 491 } 492 } 493 494 private String hash(X500Principal name) { 495 int hash = NativeCrypto.X509_NAME_hash_old(name); 496 return IntegralToString.intToHexString(hash, false, 8); 497 } 498 499 private File file(File dir, String hash, int index) { 500 return new File(dir, hash + '.' + index); 501 } 502 503 /** 504 * This non-{@code KeyStoreSpi} public interface is used by the 505 * {@code KeyChainService} to install new CA certificates. It 506 * silently ignores the certificate if it already exists in the 507 * store. 508 */ 509 public void installCertificate(X509Certificate cert) throws IOException, CertificateException { 510 if (cert == null) { 511 throw new NullPointerException("cert == null"); 512 } 513 File system = getCertificateFile(systemDir, cert); 514 if (system.exists()) { 515 File deleted = getCertificateFile(deletedDir, cert); 516 if (deleted.exists()) { 517 // we have a system cert that was marked deleted. 518 // remove the deleted marker to expose the original 519 if (!deleted.delete()) { 520 throw new IOException("Could not remove " + deleted); 521 } 522 return; 523 } 524 // otherwise we just have a dup of an existing system cert. 525 // return taking no further action. 526 return; 527 } 528 File user = getCertificateFile(addedDir, cert); 529 if (user.exists()) { 530 // we have an already installed user cert, bail. 531 return; 532 } 533 // install the user cert 534 writeCertificate(user, cert); 535 } 536 537 /** 538 * This could be considered the implementation of {@code 539 * TrustedCertificateKeyStoreSpi.engineDeleteEntry} but we 540 * consider {@code TrustedCertificateKeyStoreSpi} to be read 541 * only. Instead, this is used by the {@code KeyChainService} to 542 * delete CA certificates. 543 */ 544 public void deleteCertificateEntry(String alias) throws IOException, CertificateException { 545 if (alias == null) { 546 return; 547 } 548 File file = fileForAlias(alias); 549 if (file == null) { 550 return; 551 } 552 if (isSystem(alias)) { 553 X509Certificate cert = readCertificate(file); 554 if (cert == null) { 555 // skip problem certificates 556 return; 557 } 558 File deleted = getCertificateFile(deletedDir, cert); 559 if (deleted.exists()) { 560 // already deleted system certificate 561 return; 562 } 563 // write copy of system cert to marked as deleted 564 writeCertificate(deleted, cert); 565 return; 566 } 567 if (isUser(alias)) { 568 // truncate the file to make a tombstone by opening and closing. 569 // we need ensure that we don't leave a gap before a valid cert. 570 new FileOutputStream(file).close(); 571 removeUnnecessaryTombstones(alias); 572 return; 573 } 574 // non-existant user cert, nothing to delete 575 } 576 577 private void removeUnnecessaryTombstones(String alias) throws IOException { 578 if (!isUser(alias)) { 579 throw new AssertionError(alias); 580 } 581 int dotIndex = alias.lastIndexOf('.'); 582 if (dotIndex == -1) { 583 throw new AssertionError(alias); 584 } 585 586 String hash = alias.substring(PREFIX_USER.length(), dotIndex); 587 int lastTombstoneIndex = Integer.parseInt(alias.substring(dotIndex + 1)); 588 589 if (file(addedDir, hash, lastTombstoneIndex + 1).exists()) { 590 return; 591 } 592 while (lastTombstoneIndex >= 0) { 593 File file = file(addedDir, hash, lastTombstoneIndex); 594 if (!isTombstone(file)) { 595 break; 596 } 597 if (!file.delete()) { 598 throw new IOException("Could not remove " + file); 599 } 600 lastTombstoneIndex--; 601 } 602 } 603 } 604