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.apache.harmony.xnet.provider.jsse; 18 19 import org.apache.harmony.security.x501.Name; 20 import org.apache.harmony.security.x509.AuthorityKeyIdentifier; 21 import org.apache.harmony.security.x509.GeneralName; 22 import org.apache.harmony.security.x509.GeneralNames; 23 import org.apache.harmony.security.x509.SubjectKeyIdentifier; 24 import java.io.BufferedInputStream; 25 import java.io.File; 26 import java.io.FileInputStream; 27 import java.io.FileOutputStream; 28 import java.io.IOException; 29 import java.io.InputStream; 30 import java.io.OutputStream; 31 import java.math.BigInteger; 32 import java.security.cert.Certificate; 33 import java.security.cert.CertificateException; 34 import java.security.cert.CertificateFactory; 35 import java.security.cert.X509Certificate; 36 import java.util.ArrayList; 37 import java.util.Arrays; 38 import java.util.Date; 39 import java.util.HashSet; 40 import java.util.List; 41 import java.util.Set; 42 import javax.security.auth.x500.X500Principal; 43 import libcore.io.IoUtils; 44 import 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 */ 89 public 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 true to indicate that the certificate was added by the 302 * user, false otherwise. 303 */ 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 */ 314 private File getCertificateFile(File dir, final X509Certificate x) { 315 // compare X509Certificate.getEncoded values 316 CertSelector selector = new CertSelector() { 317 @Override public boolean match(X509Certificate cert) { 318 return cert.equals(x); 319 } 320 }; 321 return findCert(dir, x.getSubjectX500Principal(), selector, File.class); 322 } 323 324 /** 325 * This non-{@code KeyStoreSpi} public interface is used by {@code 326 * TrustManagerImpl} to locate a CA certificate with the same name 327 * and public key as the provided {@code X509Certificate}. We 328 * match on the name and public key and not the entire certificate 329 * since a CA may be reissued with the same name and PublicKey but 330 * with other differences (for example when switching signature 331 * from md2WithRSAEncryption to SHA1withRSA) 332 */ 333 public boolean isTrustAnchor(final X509Certificate c) { 334 // compare X509Certificate.getPublicKey values 335 CertSelector selector = new CertSelector() { 336 @Override public boolean match(X509Certificate ca) { 337 return ca.getPublicKey().equals(c.getPublicKey()); 338 } 339 }; 340 boolean user = findCert(addedDir, 341 c.getSubjectX500Principal(), 342 selector, 343 Boolean.class); 344 if (user) { 345 return true; 346 } 347 X509Certificate system = findCert(systemDir, 348 c.getSubjectX500Principal(), 349 selector, 350 X509Certificate.class); 351 return system != null && !isDeletedSystemCertificate(system); 352 } 353 354 /** 355 * This non-{@code KeyStoreSpi} public interface is used by {@code 356 * TrustManagerImpl} to locate the CA certificate that signed the 357 * provided {@code X509Certificate}. 358 */ 359 public X509Certificate findIssuer(final X509Certificate c) { 360 // match on verified issuer of Certificate 361 CertSelector selector = new CertSelector() { 362 @Override public boolean match(X509Certificate ca) { 363 try { 364 c.verify(ca.getPublicKey()); 365 return true; 366 } catch (Exception e) { 367 return false; 368 } 369 } 370 }; 371 X500Principal issuer = c.getIssuerX500Principal(); 372 X509Certificate user = findCert(addedDir, issuer, selector, X509Certificate.class); 373 if (user != null) { 374 return user; 375 } 376 X509Certificate system = findCert(systemDir, issuer, selector, X509Certificate.class); 377 if (system != null && !isDeletedSystemCertificate(system)) { 378 return system; 379 } 380 return null; 381 } 382 383 private static AuthorityKeyIdentifier getAuthorityKeyIdentifier(X509Certificate cert) { 384 final byte[] akidBytes = cert.getExtensionValue("2.5.29.35"); 385 if (akidBytes == null) { 386 return null; 387 } 388 389 try { 390 return AuthorityKeyIdentifier.decode(akidBytes); 391 } catch (IOException e) { 392 return null; 393 } 394 } 395 396 private static SubjectKeyIdentifier getSubjectKeyIdentifier(X509Certificate cert) { 397 final byte[] skidBytes = cert.getExtensionValue("2.5.29.14"); 398 if (skidBytes == null) { 399 return null; 400 } 401 402 try { 403 return SubjectKeyIdentifier.decode(skidBytes); 404 } catch (IOException e) { 405 return null; 406 } 407 } 408 409 private static boolean isSelfSignedCertificate(X509Certificate cert) { 410 if (!Objects.equal(cert.getSubjectX500Principal(), cert.getIssuerX500Principal())) { 411 return false; 412 } 413 414 final AuthorityKeyIdentifier akid = getAuthorityKeyIdentifier(cert); 415 if (akid != null) { 416 final byte[] akidKeyId = akid.getKeyIdentifier(); 417 if (akidKeyId != null) { 418 final SubjectKeyIdentifier skid = getSubjectKeyIdentifier(cert); 419 if (!Arrays.equals(akidKeyId, skid.getKeyIdentifier())) { 420 return false; 421 } 422 } 423 424 final BigInteger akidSerial = akid.getAuthorityCertSerialNumber(); 425 if (akidSerial != null && !akidSerial.equals(cert.getSerialNumber())) { 426 return false; 427 } 428 429 final GeneralNames possibleIssuerNames = akid.getAuthorityCertIssuer(); 430 if (possibleIssuerNames != null) { 431 GeneralName issuerName = null; 432 433 /* Get the first Directory Name (DN) to match how OpenSSL works. */ 434 for (GeneralName possibleName : possibleIssuerNames.getNames()) { 435 if (possibleName.getTag() == GeneralName.DIR_NAME) { 436 issuerName = possibleName; 437 break; 438 } 439 } 440 441 if (issuerName != null) { 442 final String issuerCanonical = ((Name) issuerName.getName()) 443 .getName(X500Principal.CANONICAL); 444 445 try { 446 final String subjectCanonical = new Name(cert.getSubjectX500Principal() 447 .getEncoded()).getName(X500Principal.CANONICAL); 448 if (!issuerCanonical.equals(subjectCanonical)) { 449 return false; 450 } 451 } catch (IOException ignored) { 452 } 453 } 454 } 455 } 456 457 return true; 458 } 459 460 /** 461 * Attempt to build a certificate chain from the supplied {@code leaf} 462 * argument through the chain of issuers as high up as known. If the chain 463 * can't be completed, the most complete chain available will be returned. 464 * This means that a list with only the {@code leaf} certificate is returned 465 * if no issuer certificates could be found. 466 */ 467 public List<X509Certificate> getCertificateChain(X509Certificate leaf) { 468 final List<X509Certificate> chain = new ArrayList<X509Certificate>(); 469 chain.add(leaf); 470 471 for (int i = 0; true; i++) { 472 X509Certificate cert = chain.get(i); 473 if (isSelfSignedCertificate(cert)) { 474 break; 475 } 476 X509Certificate issuer = findIssuer(cert); 477 if (issuer == null) { 478 break; 479 } 480 chain.add(issuer); 481 } 482 483 return chain; 484 } 485 486 // like java.security.cert.CertSelector but with X509Certificate and without cloning 487 private static interface CertSelector { 488 public boolean match(X509Certificate cert); 489 } 490 491 private <T> T findCert( 492 File dir, X500Principal subject, CertSelector selector, Class<T> desiredReturnType) { 493 494 String hash = hash(subject); 495 for (int index = 0; true; index++) { 496 File file = file(dir, hash, index); 497 if (!file.isFile()) { 498 // could not find a match, no file exists, bail 499 if (desiredReturnType == Boolean.class) { 500 return (T) Boolean.FALSE; 501 } 502 if (desiredReturnType == File.class) { 503 // we return file so that caller that wants to 504 // write knows what the next available has 505 // location is 506 return (T) file; 507 } 508 return null; 509 } 510 if (isTombstone(file)) { 511 continue; 512 } 513 X509Certificate cert = readCertificate(file); 514 if (cert == null) { 515 // skip problem certificates 516 continue; 517 } 518 if (selector.match(cert)) { 519 if (desiredReturnType == X509Certificate.class) { 520 return (T) cert; 521 } 522 if (desiredReturnType == Boolean.class) { 523 return (T) Boolean.TRUE; 524 } 525 if (desiredReturnType == File.class) { 526 return (T) file; 527 } 528 throw new AssertionError(); 529 } 530 } 531 } 532 533 private String hash(X500Principal name) { 534 int hash = NativeCrypto.X509_NAME_hash_old(name); 535 return IntegralToString.intToHexString(hash, false, 8); 536 } 537 538 private File file(File dir, String hash, int index) { 539 return new File(dir, hash + '.' + index); 540 } 541 542 /** 543 * This non-{@code KeyStoreSpi} public interface is used by the 544 * {@code KeyChainService} to install new CA certificates. It 545 * silently ignores the certificate if it already exists in the 546 * store. 547 */ 548 public void installCertificate(X509Certificate cert) throws IOException, CertificateException { 549 if (cert == null) { 550 throw new NullPointerException("cert == null"); 551 } 552 File system = getCertificateFile(systemDir, cert); 553 if (system.exists()) { 554 File deleted = getCertificateFile(deletedDir, cert); 555 if (deleted.exists()) { 556 // we have a system cert that was marked deleted. 557 // remove the deleted marker to expose the original 558 if (!deleted.delete()) { 559 throw new IOException("Could not remove " + deleted); 560 } 561 return; 562 } 563 // otherwise we just have a dup of an existing system cert. 564 // return taking no further action. 565 return; 566 } 567 File user = getCertificateFile(addedDir, cert); 568 if (user.exists()) { 569 // we have an already installed user cert, bail. 570 return; 571 } 572 // install the user cert 573 writeCertificate(user, cert); 574 } 575 576 /** 577 * This could be considered the implementation of {@code 578 * TrustedCertificateKeyStoreSpi.engineDeleteEntry} but we 579 * consider {@code TrustedCertificateKeyStoreSpi} to be read 580 * only. Instead, this is used by the {@code KeyChainService} to 581 * delete CA certificates. 582 */ 583 public void deleteCertificateEntry(String alias) throws IOException, CertificateException { 584 if (alias == null) { 585 return; 586 } 587 File file = fileForAlias(alias); 588 if (file == null) { 589 return; 590 } 591 if (isSystem(alias)) { 592 X509Certificate cert = readCertificate(file); 593 if (cert == null) { 594 // skip problem certificates 595 return; 596 } 597 File deleted = getCertificateFile(deletedDir, cert); 598 if (deleted.exists()) { 599 // already deleted system certificate 600 return; 601 } 602 // write copy of system cert to marked as deleted 603 writeCertificate(deleted, cert); 604 return; 605 } 606 if (isUser(alias)) { 607 // truncate the file to make a tombstone by opening and closing. 608 // we need ensure that we don't leave a gap before a valid cert. 609 new FileOutputStream(file).close(); 610 removeUnnecessaryTombstones(alias); 611 return; 612 } 613 // non-existant user cert, nothing to delete 614 } 615 616 private void removeUnnecessaryTombstones(String alias) throws IOException { 617 if (!isUser(alias)) { 618 throw new AssertionError(alias); 619 } 620 int dotIndex = alias.lastIndexOf('.'); 621 if (dotIndex == -1) { 622 throw new AssertionError(alias); 623 } 624 625 String hash = alias.substring(PREFIX_USER.length(), dotIndex); 626 int lastTombstoneIndex = Integer.parseInt(alias.substring(dotIndex + 1)); 627 628 if (file(addedDir, hash, lastTombstoneIndex + 1).exists()) { 629 return; 630 } 631 while (lastTombstoneIndex >= 0) { 632 File file = file(addedDir, hash, lastTombstoneIndex); 633 if (!isTombstone(file)) { 634 break; 635 } 636 if (!file.delete()) { 637 throw new IOException("Could not remove " + file); 638 } 639 lastTombstoneIndex--; 640 } 641 } 642 } 643