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