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