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 86 public static final boolean isSystem(String alias) { 87 return alias.startsWith(PREFIX_SYSTEM); 88 } 89 public static final boolean isUser(String alias) { 90 return alias.startsWith(PREFIX_USER); 91 } 92 93 private static final File CA_CERTS_DIR_SYSTEM; 94 private static final File CA_CERTS_DIR_ADDED; 95 private static final File CA_CERTS_DIR_DELETED; 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 CA_CERTS_DIR_SYSTEM = new File(ANDROID_ROOT + "/etc/security/cacerts"); 101 CA_CERTS_DIR_ADDED = new File(ANDROID_DATA + "/misc/keychain/cacerts-added"); 102 CA_CERTS_DIR_DELETED = new File(ANDROID_DATA + "/misc/keychain/cacerts-removed"); 103 104 try { 105 CERT_FACTORY = CertificateFactory.getInstance("X509"); 106 } catch (CertificateException e) { 107 throw new AssertionError(e); 108 } 109 } 110 111 private final File systemDir; 112 private final File addedDir; 113 private final File deletedDir; 114 115 public TrustedCertificateStore() { 116 this(CA_CERTS_DIR_SYSTEM, CA_CERTS_DIR_ADDED, CA_CERTS_DIR_DELETED); 117 } 118 119 public TrustedCertificateStore(File systemDir, File addedDir, File deletedDir) { 120 this.systemDir = systemDir; 121 this.addedDir = addedDir; 122 this.deletedDir = deletedDir; 123 } 124 125 public Certificate getCertificate(String alias) { 126 return getCertificate(alias, false); 127 } 128 129 public Certificate getCertificate(String alias, boolean includeDeletedSystem) { 130 131 File file = fileForAlias(alias); 132 if (file == null || (isUser(alias) && isTombstone(file))) { 133 return null; 134 } 135 X509Certificate cert = readCertificate(file); 136 if (cert == null || (isSystem(alias) 137 && !includeDeletedSystem 138 && isDeletedSystemCertificate(cert))) { 139 // skip malformed certs as well as deleted system ones 140 return null; 141 } 142 return cert; 143 } 144 145 private File fileForAlias(String alias) { 146 if (alias == null) { 147 throw new NullPointerException("alias == null"); 148 } 149 File file; 150 if (isSystem(alias)) { 151 file = new File(systemDir, alias.substring(PREFIX_SYSTEM.length())); 152 } else if (isUser(alias)) { 153 file = new File(addedDir, alias.substring(PREFIX_USER.length())); 154 } else { 155 return null; 156 } 157 if (!file.exists() || isTombstone(file)) { 158 // silently elide tombstones 159 return null; 160 } 161 return file; 162 } 163 164 private boolean isTombstone(File file) { 165 return file.length() == 0; 166 } 167 168 private X509Certificate readCertificate(File file) { 169 if (!file.isFile()) { 170 return null; 171 } 172 InputStream is = null; 173 try { 174 is = new BufferedInputStream(new FileInputStream(file)); 175 return (X509Certificate) CERT_FACTORY.generateCertificate(is); 176 } catch (IOException e) { 177 return null; 178 } catch (CertificateException e) { 179 // reading a cert while its being installed can lead to this. 180 // just pretend like its not available yet. 181 return null; 182 } finally { 183 IoUtils.closeQuietly(is); 184 } 185 } 186 187 private void writeCertificate(File file, X509Certificate cert) 188 throws IOException, CertificateException { 189 File dir = file.getParentFile(); 190 dir.mkdirs(); 191 dir.setReadable(true, false); 192 dir.setExecutable(true, false); 193 OutputStream os = null; 194 try { 195 os = new FileOutputStream(file); 196 os.write(cert.getEncoded()); 197 } finally { 198 IoUtils.closeQuietly(os); 199 } 200 file.setReadable(true, false); 201 } 202 203 private boolean isDeletedSystemCertificate(X509Certificate x) { 204 return getCertificateFile(deletedDir, x).exists(); 205 } 206 207 public Date getCreationDate(String alias) { 208 // containsAlias check ensures the later fileForAlias result 209 // was not a deleted system cert. 210 if (!containsAlias(alias)) { 211 return null; 212 } 213 File file = fileForAlias(alias); 214 if (file == null) { 215 return null; 216 } 217 long time = file.lastModified(); 218 if (time == 0) { 219 return null; 220 } 221 return new Date(time); 222 } 223 224 public Set<String> aliases() { 225 Set<String> result = new HashSet<String>(); 226 addAliases(result, PREFIX_USER, addedDir); 227 addAliases(result, PREFIX_SYSTEM, systemDir); 228 return result; 229 } 230 231 public Set<String> userAliases() { 232 Set<String> result = new HashSet<String>(); 233 addAliases(result, PREFIX_USER, addedDir); 234 return result; 235 } 236 237 private void addAliases(Set<String> result, String prefix, File dir) { 238 String[] files = dir.list(); 239 if (files == null) { 240 return; 241 } 242 for (String filename : files) { 243 String alias = prefix + filename; 244 if (containsAlias(alias)) { 245 result.add(alias); 246 } 247 } 248 } 249 250 public Set<String> allSystemAliases() { 251 Set<String> result = new HashSet<String>(); 252 String[] files = systemDir.list(); 253 if (files == null) { 254 return result; 255 } 256 for (String filename : files) { 257 String alias = PREFIX_SYSTEM + filename; 258 if (containsAlias(alias, true)) { 259 result.add(alias); 260 } 261 } 262 return result; 263 } 264 265 public boolean containsAlias(String alias) { 266 return containsAlias(alias, false); 267 } 268 269 private boolean containsAlias(String alias, boolean includeDeletedSystem) { 270 return getCertificate(alias, includeDeletedSystem) != null; 271 } 272 273 public String getCertificateAlias(Certificate c) { 274 if (c == null || !(c instanceof X509Certificate)) { 275 return null; 276 } 277 X509Certificate x = (X509Certificate) c; 278 File user = getCertificateFile(addedDir, x); 279 if (user.exists()) { 280 return PREFIX_USER + user.getName(); 281 } 282 if (isDeletedSystemCertificate(x)) { 283 return null; 284 } 285 File system = getCertificateFile(systemDir, x); 286 if (system.exists()) { 287 return PREFIX_SYSTEM + system.getName(); 288 } 289 return null; 290 } 291 292 /** 293 * Returns true to indicate that the certificate was added by the 294 * user, false otherwise. 295 */ 296 public boolean isUserAddedCertificate(X509Certificate cert) { 297 return getCertificateFile(addedDir, cert).exists(); 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 boolean isSelfIssuedCertificate(OpenSSLX509Certificate cert) { 376 final long ctx = cert.getContext(); 377 return NativeCrypto.X509_check_issued(ctx, ctx) == 0; 378 } 379 380 /** 381 * Converts the {@code cert} to the internal OpenSSL X.509 format so we can 382 * run {@link NativeCrypto} methods on it. 383 */ 384 private static OpenSSLX509Certificate convertToOpenSSLIfNeeded(X509Certificate cert) 385 throws CertificateException { 386 if (cert == null) { 387 return null; 388 } 389 390 if (cert instanceof OpenSSLX509Certificate) { 391 return (OpenSSLX509Certificate) cert; 392 } 393 394 try { 395 return OpenSSLX509Certificate.fromX509Der(cert.getEncoded()); 396 } catch (Exception e) { 397 throw new CertificateException(e); 398 } 399 } 400 401 /** 402 * Attempt to build a certificate chain from the supplied {@code leaf} 403 * argument through the chain of issuers as high up as known. If the chain 404 * can't be completed, the most complete chain available will be returned. 405 * This means that a list with only the {@code leaf} certificate is returned 406 * if no issuer certificates could be found. 407 * 408 * @throws CertificateException if there was a problem parsing the 409 * certificates 410 */ 411 public List<X509Certificate> getCertificateChain(X509Certificate leaf) 412 throws CertificateException { 413 final List<OpenSSLX509Certificate> chain = new ArrayList<OpenSSLX509Certificate>(); 414 chain.add(convertToOpenSSLIfNeeded(leaf)); 415 416 for (int i = 0; true; i++) { 417 OpenSSLX509Certificate cert = chain.get(i); 418 if (isSelfIssuedCertificate(cert)) { 419 break; 420 } 421 OpenSSLX509Certificate issuer = convertToOpenSSLIfNeeded(findIssuer(cert)); 422 if (issuer == null) { 423 break; 424 } 425 chain.add(issuer); 426 } 427 428 return new ArrayList<X509Certificate>(chain); 429 } 430 431 // like java.security.cert.CertSelector but with X509Certificate and without cloning 432 private static interface CertSelector { 433 public boolean match(X509Certificate cert); 434 } 435 436 private <T> T findCert( 437 File dir, X500Principal subject, CertSelector selector, Class<T> desiredReturnType) { 438 439 String hash = hash(subject); 440 for (int index = 0; true; index++) { 441 File file = file(dir, hash, index); 442 if (!file.isFile()) { 443 // could not find a match, no file exists, bail 444 if (desiredReturnType == Boolean.class) { 445 return (T) Boolean.FALSE; 446 } 447 if (desiredReturnType == File.class) { 448 // we return file so that caller that wants to 449 // write knows what the next available has 450 // location is 451 return (T) file; 452 } 453 return null; 454 } 455 if (isTombstone(file)) { 456 continue; 457 } 458 X509Certificate cert = readCertificate(file); 459 if (cert == null) { 460 // skip problem certificates 461 continue; 462 } 463 if (selector.match(cert)) { 464 if (desiredReturnType == X509Certificate.class) { 465 return (T) cert; 466 } 467 if (desiredReturnType == Boolean.class) { 468 return (T) Boolean.TRUE; 469 } 470 if (desiredReturnType == File.class) { 471 return (T) file; 472 } 473 throw new AssertionError(); 474 } 475 } 476 } 477 478 private String hash(X500Principal name) { 479 int hash = NativeCrypto.X509_NAME_hash_old(name); 480 return IntegralToString.intToHexString(hash, false, 8); 481 } 482 483 private File file(File dir, String hash, int index) { 484 return new File(dir, hash + '.' + index); 485 } 486 487 /** 488 * This non-{@code KeyStoreSpi} public interface is used by the 489 * {@code KeyChainService} to install new CA certificates. It 490 * silently ignores the certificate if it already exists in the 491 * store. 492 */ 493 public void installCertificate(X509Certificate cert) throws IOException, CertificateException { 494 if (cert == null) { 495 throw new NullPointerException("cert == null"); 496 } 497 File system = getCertificateFile(systemDir, cert); 498 if (system.exists()) { 499 File deleted = getCertificateFile(deletedDir, cert); 500 if (deleted.exists()) { 501 // we have a system cert that was marked deleted. 502 // remove the deleted marker to expose the original 503 if (!deleted.delete()) { 504 throw new IOException("Could not remove " + deleted); 505 } 506 return; 507 } 508 // otherwise we just have a dup of an existing system cert. 509 // return taking no further action. 510 return; 511 } 512 File user = getCertificateFile(addedDir, cert); 513 if (user.exists()) { 514 // we have an already installed user cert, bail. 515 return; 516 } 517 // install the user cert 518 writeCertificate(user, cert); 519 } 520 521 /** 522 * This could be considered the implementation of {@code 523 * TrustedCertificateKeyStoreSpi.engineDeleteEntry} but we 524 * consider {@code TrustedCertificateKeyStoreSpi} to be read 525 * only. Instead, this is used by the {@code KeyChainService} to 526 * delete CA certificates. 527 */ 528 public void deleteCertificateEntry(String alias) throws IOException, CertificateException { 529 if (alias == null) { 530 return; 531 } 532 File file = fileForAlias(alias); 533 if (file == null) { 534 return; 535 } 536 if (isSystem(alias)) { 537 X509Certificate cert = readCertificate(file); 538 if (cert == null) { 539 // skip problem certificates 540 return; 541 } 542 File deleted = getCertificateFile(deletedDir, cert); 543 if (deleted.exists()) { 544 // already deleted system certificate 545 return; 546 } 547 // write copy of system cert to marked as deleted 548 writeCertificate(deleted, cert); 549 return; 550 } 551 if (isUser(alias)) { 552 // truncate the file to make a tombstone by opening and closing. 553 // we need ensure that we don't leave a gap before a valid cert. 554 new FileOutputStream(file).close(); 555 removeUnnecessaryTombstones(alias); 556 return; 557 } 558 // non-existant user cert, nothing to delete 559 } 560 561 private void removeUnnecessaryTombstones(String alias) throws IOException { 562 if (!isUser(alias)) { 563 throw new AssertionError(alias); 564 } 565 int dotIndex = alias.lastIndexOf('.'); 566 if (dotIndex == -1) { 567 throw new AssertionError(alias); 568 } 569 570 String hash = alias.substring(PREFIX_USER.length(), dotIndex); 571 int lastTombstoneIndex = Integer.parseInt(alias.substring(dotIndex + 1)); 572 573 if (file(addedDir, hash, lastTombstoneIndex + 1).exists()) { 574 return; 575 } 576 while (lastTombstoneIndex >= 0) { 577 File file = file(addedDir, hash, lastTombstoneIndex); 578 if (!isTombstone(file)) { 579 break; 580 } 581 if (!file.delete()) { 582 throw new IOException("Could not remove " + file); 583 } 584 lastTombstoneIndex--; 585 } 586 } 587 } 588