diff options
11 files changed, 1417 insertions, 487 deletions
diff --git a/luni/src/main/java/java/security/cert/PKIXParameters.java b/luni/src/main/java/java/security/cert/PKIXParameters.java index 7ca9443..450bdd1 100644 --- a/luni/src/main/java/java/security/cert/PKIXParameters.java +++ b/luni/src/main/java/java/security/cert/PKIXParameters.java @@ -68,6 +68,11 @@ public class PKIXParameters implements CertPathParameters { private boolean policyQualifiersRejected = true; /** + * @hide For use by IndexedPKIXParameters which lazily discovers TrustAnchors + */ + protected PKIXParameters() {} + + /** * Creates a new {@code PKIXParameters} instance with the specified set of * <i>trusted</i> certificate authorities. * diff --git a/luni/src/main/java/javax/security/auth/x500/X500Principal.java b/luni/src/main/java/javax/security/auth/x500/X500Principal.java index f13dd4f..e6453e9 100644 --- a/luni/src/main/java/javax/security/auth/x500/X500Principal.java +++ b/luni/src/main/java/javax/security/auth/x500/X500Principal.java @@ -128,7 +128,7 @@ public final class X500Principal implements Serializable, Principal { try { dn = new Name(name); } catch (IOException e) { - throw incorrectInputName(e); + throw incorrectInputName(e, name); } } @@ -139,12 +139,12 @@ public final class X500Principal implements Serializable, Principal { try { dn = new Name(substituteNameFromMap(name, keywordMap)); } catch (IOException e) { - throw incorrectInputName(e); + throw incorrectInputName(e, name); } } - private IllegalArgumentException incorrectInputName(IOException e) { - IllegalArgumentException iae = new IllegalArgumentException("Incorrect input name"); + private IllegalArgumentException incorrectInputName(IOException e, String name) { + IllegalArgumentException iae = new IllegalArgumentException("Incorrect input name:" + name); iae.initCause(e); throw iae; } @@ -277,7 +277,7 @@ public final class X500Principal implements Serializable, Principal { } return resultName.toString(); } else { - throw new IllegalArgumentException("invalid format specified"); + throw new IllegalArgumentException("invalid format specified: " + format); } } diff --git a/luni/src/main/java/org/apache/harmony/xnet/provider/jsse/IndexedPKIXParameters.java b/luni/src/main/java/org/apache/harmony/xnet/provider/jsse/IndexedPKIXParameters.java index 90baa87..e8acf18 100644 --- a/luni/src/main/java/org/apache/harmony/xnet/provider/jsse/IndexedPKIXParameters.java +++ b/luni/src/main/java/org/apache/harmony/xnet/provider/jsse/IndexedPKIXParameters.java @@ -23,9 +23,10 @@ import java.security.cert.PKIXParameters; import java.security.cert.TrustAnchor; import java.security.cert.X509Certificate; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -39,14 +40,30 @@ public final class IndexedPKIXParameters extends PKIXParameters { private final Map<X500Principal, List<TrustAnchor>> subjectToTrustAnchors = new HashMap<X500Principal, List<TrustAnchor>>(); + public IndexedPKIXParameters() {} + public IndexedPKIXParameters(Set<TrustAnchor> anchors) throws InvalidAlgorithmParameterException { super(anchors); index(); } + @Override public Set<TrustAnchor> getTrustAnchors() { + Set<TrustAnchor> result = new HashSet<TrustAnchor>(); + synchronized (subjectToTrustAnchors) { + for (List<TrustAnchor> trustAnchors : subjectToTrustAnchors.values()) { + result.addAll(trustAnchors); + } + } + return Collections.unmodifiableSet(result); + } + + @Override public void setTrustAnchors(Set<TrustAnchor> trustAnchors) { + throw new UnsupportedOperationException(); + } + private void index() { - for (TrustAnchor anchor : getTrustAnchors()) { + for (TrustAnchor anchor : super.getTrustAnchors()) { index(anchor); } } diff --git a/luni/src/main/java/org/apache/harmony/xnet/provider/jsse/OpenSSLSocketImpl.java b/luni/src/main/java/org/apache/harmony/xnet/provider/jsse/OpenSSLSocketImpl.java index 609df94..68ab952 100644 --- a/luni/src/main/java/org/apache/harmony/xnet/provider/jsse/OpenSSLSocketImpl.java +++ b/luni/src/main/java/org/apache/harmony/xnet/provider/jsse/OpenSSLSocketImpl.java @@ -671,6 +671,8 @@ public class OpenSSLSocketImpl } catch (CertificateException e) { throw e; + } catch (RuntimeException e) { + throw e; } catch (Exception e) { throw new RuntimeException(e); } diff --git a/luni/src/main/java/org/apache/harmony/xnet/provider/jsse/RootKeyStoreSpi.java b/luni/src/main/java/org/apache/harmony/xnet/provider/jsse/RootKeyStoreSpi.java index bac01df..9ae4139 100644 --- a/luni/src/main/java/org/apache/harmony/xnet/provider/jsse/RootKeyStoreSpi.java +++ b/luni/src/main/java/org/apache/harmony/xnet/provider/jsse/RootKeyStoreSpi.java @@ -16,63 +16,21 @@ package org.apache.harmony.xnet.provider.jsse; -import java.io.BufferedInputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.security.Key; import java.security.KeyStoreSpi; -import java.security.PublicKey; -import java.security.cert.CertSelector; import java.security.cert.Certificate; -import java.security.cert.CertificateException; -import java.security.cert.CertificateFactory; -import java.security.cert.X509Certificate; -import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.Enumeration; -import javax.security.auth.x500.X500Principal; -import libcore.io.IoUtils; /** - * A Root Certificate Authority (CA) store for Android - * - * This KeyStoreSpi provides a read-only view of the - * TrustedCertificateEntry objects found in the - * $ANDROID_ROOT/etc/security/cacerts/ directory. The alias names used - * correspond to filenames in that directory, which themselves are - * named based on the OpenSSL X509_NAME_hash_old function. The - * property that the filenames are based on a hash of the subject name - * allows operations such as engineGetCertificateAlias to be - * implemented efficiently without scanning the entire store. - * - * In addition to the KeyStoreSpi, RootKeyStoreSpi also provides the - * additional public methods {@link #isTrustAnchor isTrustAnchor} and - * {@link #findIssuer findIssuer} which allow efficient lookup - * operations for CAs again based on the file naming convention. + * A KeyStoreSpi wrapper for the TrustedCertificateStore. */ public final class RootKeyStoreSpi extends KeyStoreSpi { - private static final File CA_CERTS_DIR - = new File(System.getenv("ANDROID_ROOT") + "/etc/security/cacerts"); - - private static final CertificateFactory CERT_FACTORY; - static { - try { - CERT_FACTORY = CertificateFactory.getInstance("X509"); - } catch (CertificateException e) { - throw new AssertionError(e); - } - } - - public RootKeyStoreSpi() { - if (!CA_CERTS_DIR.isDirectory()) { - throw new IllegalStateException(CA_CERTS_DIR + " is not a directory"); - } - } + private final TrustedCertificateStore store = new TrustedCertificateStore(); @Override public Key engineGetKey(String alias, char[] password) { if (alias == null) { @@ -89,42 +47,11 @@ public final class RootKeyStoreSpi extends KeyStoreSpi { } @Override public Certificate engineGetCertificate(String alias) { - if (alias == null) { - throw new NullPointerException("alias == null"); - } - return getCertificate(new File(CA_CERTS_DIR, alias)); - } - - private static X509Certificate getCertificate(File file) { - if (!file.isFile()) { - return null; - } - InputStream is = null; - try { - is = new BufferedInputStream(new FileInputStream(file)); - return (X509Certificate) CERT_FACTORY.generateCertificate(is); - } catch (IOException e) { - return null; - } catch (CertificateException e) { - throw new AssertionError(e); - } finally { - IoUtils.closeQuietly(is); - } + return store.getCertificate(alias); } @Override public Date engineGetCreationDate(String alias) { - if (alias == null) { - throw new NullPointerException("alias == null"); - } - File file = new File(CA_CERTS_DIR, alias); - if (!file.isFile()) { - return null; - } - long time = file.lastModified(); - if (time == 0) { - return null; - } - return new Date(time); + return store.getCreationDate(alias); } @Override public void engineSetKeyEntry( @@ -148,18 +75,15 @@ public final class RootKeyStoreSpi extends KeyStoreSpi { } @Override public Enumeration<String> engineAliases() { - return Collections.enumeration(Arrays.asList(CA_CERTS_DIR.list())); + return Collections.enumeration(store.aliases()); } @Override public boolean engineContainsAlias(String alias) { - if (alias == null) { - throw new NullPointerException("alias == null"); - } - return new File(CA_CERTS_DIR, alias).isFile(); + return store.containsAlias(alias); } @Override public int engineSize() { - return CA_CERTS_DIR.list().length; + return store.aliases().size(); } @Override public boolean engineIsKeyEntry(String alias) { @@ -174,101 +98,7 @@ public final class RootKeyStoreSpi extends KeyStoreSpi { } @Override public String engineGetCertificateAlias(Certificate c) { - if (c == null || !(c instanceof X509Certificate)) { - return null; - } - final X509Certificate x = (X509Certificate) c; - // compare X509Certificate.getEncoded values - CertSelector selector = new CertSelector() { - public boolean match(Certificate cert) { - return cert.equals(x); - } - public Object clone() { - throw new UnsupportedOperationException(); - } - }; - return findCert(x.getSubjectX500Principal(), selector, String.class); - } - - /** - * This non-{@code KeyStoreSpi} public interface is used by {@code - * TrustManagerImpl} to locate a CA certificate with the same - * public key as the provided {@code X509Certificate}. We match on - * public key and not the certificate itself since a CA may be - * reissued with the same PublicKey but different signature (for - * example when switching signature from md2WithRSAEncryption to - * SHA1withRSA) - */ - public static final boolean isTrustAnchor(final X509Certificate x) { - // compare X509Certificate.getPublicKey values - CertSelector selector = new CertSelector() { - public boolean match(Certificate c) { - X509Certificate ca = (X509Certificate)c; - PublicKey caPublic = ca.getPublicKey(); - PublicKey certPublic = x.getPublicKey(); - return caPublic != null && certPublic != null && caPublic.equals(certPublic); - } - public Object clone() { - throw new UnsupportedOperationException(); - } - }; - return findCert(x.getSubjectX500Principal(), selector, Boolean.class); - } - - /** - * This non-{@code KeyStoreSpi} public interface is used by {@code - * TrustManagerImpl} to locate the CA certificate that signed the - * provided {@code X509Certificate}. - */ - public static final X509Certificate findIssuer(final X509Certificate x) { - // match on verified issuer of Certificate - CertSelector selector = new CertSelector() { - public boolean match(Certificate c) { - X509Certificate ca = (X509Certificate)c; - try { - x.verify(ca.getPublicKey()); - return true; - } catch (Exception e) { - return false; - } - } - public Object clone() { - throw new UnsupportedOperationException(); - } - }; - return findCert(x.getIssuerX500Principal(), selector, X509Certificate.class); - } - - private static <T> T findCert( - X500Principal subject, CertSelector selector, Class<T> desiredReturnType) { - - int intHash = NativeCrypto.X509_NAME_hash_old(subject); - String strHash = IntegralToString.intToHexString(intHash, false, 8); - - for (int index = 0; true; index++) { - String alias = strHash + "." + index; - File file = new File(CA_CERTS_DIR, alias); - if (!file.isFile()) { - // could not find a match, no file exists, bail - if (desiredReturnType == Boolean.class) { - return (T) Boolean.FALSE; - } - return null; - } - X509Certificate cert = getCertificate(file); - if (selector.match(cert)) { - if (desiredReturnType == X509Certificate.class) { - return (T) cert; - } - if (desiredReturnType == Boolean.class) { - return (T) Boolean.TRUE; - } - if (desiredReturnType == String.class) { - return (T) alias; - } - throw new AssertionError(); - } - } + return store.getCertificateAlias(c); } @Override public void engineStore(OutputStream stream, char[] password) { diff --git a/luni/src/main/java/org/apache/harmony/xnet/provider/jsse/TrustManagerImpl.java b/luni/src/main/java/org/apache/harmony/xnet/provider/jsse/TrustManagerImpl.java index 6c2552d..2a56618 100644 --- a/luni/src/main/java/org/apache/harmony/xnet/provider/jsse/TrustManagerImpl.java +++ b/luni/src/main/java/org/apache/harmony/xnet/provider/jsse/TrustManagerImpl.java @@ -47,10 +47,17 @@ import javax.net.ssl.X509TrustManager; public final class TrustManagerImpl implements X509TrustManager { /** - * The AndroidCAStore if non-null, null otherwise + * The AndroidCAStore if non-null, null otherwise. */ private final KeyStore rootKeyStore; + /** + * The backing store for the AndroidCAStore if non-null. This will + * be null when the rootKeyStore is null, implying we are not + * using the AndroidCAStore. + */ + private final TrustedCertificateStore trustedCertificateStore; + private final CertPathValidator validator; private final IndexedPKIXParameters params; @@ -79,6 +86,7 @@ public final class TrustManagerImpl implements X509TrustManager { CertPathValidator validatorLocal = null; CertificateFactory factoryLocal = null; KeyStore rootKeyStoreLocal = null; + TrustedCertificateStore trustedCertificateStoreLocal = null; IndexedPKIXParameters paramsLocal = null; X509Certificate[] acceptedIssuersLocal = null; Exception errLocal = null; @@ -86,26 +94,25 @@ public final class TrustManagerImpl implements X509TrustManager { validatorLocal = CertPathValidator.getInstance("PKIX"); factoryLocal = CertificateFactory.getInstance("X509"); - Set<TrustAnchor> trustAnchors; // if we have an AndroidCAStore, we will lazily load CAs if ("AndroidCAStore".equals(keyStore.getType())) { rootKeyStoreLocal = keyStore; + trustedCertificateStoreLocal = new TrustedCertificateStore(); acceptedIssuersLocal = null; - // Note we need to include at least one TrustAnchor - // for the IndexedPKIXParameters super class to be happy. - trustAnchors = trustAnchors(acceptedIssuers(keyStore, true)); + paramsLocal = new IndexedPKIXParameters(); } else { rootKeyStoreLocal = null; - acceptedIssuersLocal = acceptedIssuers(keyStore, false); - trustAnchors = trustAnchors(acceptedIssuersLocal); + trustedCertificateStoreLocal = null; + acceptedIssuersLocal = acceptedIssuers(keyStore); + paramsLocal = new IndexedPKIXParameters(trustAnchors(acceptedIssuersLocal)); } - paramsLocal = new IndexedPKIXParameters(trustAnchors); paramsLocal.setRevocationEnabled(false); } catch (Exception e) { errLocal = e; } this.rootKeyStore = rootKeyStoreLocal; + this.trustedCertificateStore = trustedCertificateStoreLocal; this.validator = validatorLocal; this.factory = factoryLocal; this.params = paramsLocal; @@ -113,7 +120,7 @@ public final class TrustManagerImpl implements X509TrustManager { this.err = errLocal; } - private static X509Certificate[] acceptedIssuers(KeyStore ks, boolean onlyOne) + private static X509Certificate[] acceptedIssuers(KeyStore ks) throws KeyStoreException { // Note that unlike the PKIXParameters code to create a Set of // TrustAnchors from a KeyStore, this version takes from both @@ -130,9 +137,6 @@ public final class TrustManagerImpl implements X509TrustManager { final X509Certificate cert = (X509Certificate) ks.getCertificate(alias); if (cert != null) { trusted.add(cert); - if (onlyOne) { - break; - } } } return trusted.toArray(new X509Certificate[trusted.size()]); @@ -171,12 +175,6 @@ public final class TrustManagerImpl implements X509TrustManager { return; } - if (rootKeyStore != null) { - // check if we need to add a missing TrustAnchor value to - // the IndexedPKIXParameters from the KeyStore - optionallyAddTrustAnchorFromKeyStore(newChain[newChain.length-1]); - } - CertPath certPath = factory.generateCertPath(Arrays.asList(newChain)); if (!Arrays.equals(chain[0].getEncoded(), certPath.getCertificates().get(0).getEncoded())) { @@ -184,6 +182,20 @@ public final class TrustManagerImpl implements X509TrustManager { // are using pretty remote code) throw new CertificateException("Certificate chain error"); } + + if (trustedCertificateStore != null) { + // check if we need to add a missing TrustAnchor value to + // the IndexedPKIXParameters from the KeyStore. + boolean found = optionallyAddTrustAnchorToIndex(newChain[newChain.length-1]); + // If we can't find the TrustAnchor, we throw + // CertPathValidatorException to avoid an + // ExtendedPKIXParameters constructor error in validate(). + if (!found) { + throw new CertificateException(new CertPathValidatorException( + "Trust anchor for certification path not found.", null, certPath, -1)); + } + } + try { validator.validate(certPath, params); // Add intermediate CAs to the index to tolerate sites @@ -265,7 +277,7 @@ public final class TrustManagerImpl implements X509TrustManager { return Arrays.copyOf(chain, chainLength); } - private void optionallyAddTrustAnchorFromKeyStore(X509Certificate lastCert) { + private boolean optionallyAddTrustAnchorToIndex(X509Certificate lastCert) { TrustAnchor trustAnchor; try { // returns null if no match based on issuer @@ -277,15 +289,18 @@ public final class TrustManagerImpl implements X509TrustManager { // to the IndexedPKIXParameters. trustAnchor = null; } - if (trustAnchor == null) { - // we have a KeyStore and the issuer of the last cert in - // the chain seems to be missing from the - // IndexedPKIXParameters, check the KeyStore for a hit - X509Certificate issuer = RootKeyStoreSpi.findIssuer(lastCert); - if (issuer != null) { - index(issuer); - } + if (trustAnchor != null) { + return true; + } + // we have a KeyStore and the issuer of the last cert in + // the chain seems to be missing from the + // IndexedPKIXParameters, check the KeyStore for a hit + X509Certificate issuer = trustedCertificateStore.findIssuer(lastCert); + if (issuer != null) { + index(issuer); + return true; } + return false; } /** @@ -298,14 +313,14 @@ public final class TrustManagerImpl implements X509TrustManager { if (isTrustAnchor) { return true; } - if (rootKeyStore == null) { - // not trusted and no KeyStore to check + if (trustedCertificateStore == null) { + // not trusted and no TrustedCertificateStore to check return false; } // probe KeyStore for a cert. AndroidCAStore stores its // contents hashed by cert subject on the filesystem to make // this faster than scanning all key store entries. - if (RootKeyStoreSpi.isTrustAnchor(cert)) { + if (trustedCertificateStore.isTrustAnchor(cert)) { // add new TrustAnchor to params index to avoid // checking filesystem next time around. index(cert); @@ -326,7 +341,7 @@ public final class TrustManagerImpl implements X509TrustManager { if (result == null) { // single-check idiom try { - acceptedIssuers = result = acceptedIssuers(rootKeyStore, false); + acceptedIssuers = result = acceptedIssuers(rootKeyStore); } catch (KeyStoreException e) { acceptedIssuers = result = new X509Certificate[0]; } diff --git a/luni/src/main/java/org/apache/harmony/xnet/provider/jsse/TrustedCertificateStore.java b/luni/src/main/java/org/apache/harmony/xnet/provider/jsse/TrustedCertificateStore.java new file mode 100644 index 0000000..f440eec --- /dev/null +++ b/luni/src/main/java/org/apache/harmony/xnet/provider/jsse/TrustedCertificateStore.java @@ -0,0 +1,499 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.harmony.xnet.provider.jsse; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.KeyStoreSpi; +import java.security.PublicKey; +import java.security.cert.CertSelector; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.Collections; +import java.util.Date; +import java.util.HashSet; +import java.util.Set; +import javax.security.auth.x500.X500Principal; +import libcore.io.IoUtils; + +/** + * A source for trusted root certificate authority (CA) certificates + * supporting an immutable system CA directory along with mutable + * directories allowing the user addition of custom CAs and user + * removal of system CAs. This store supports the RootKeyStoreSpi + * wrapper to allow a traditional KeyStore interface for use with + * {@link javax.net.ssl.TrustManagerFactory.init}. + * + * <p>The CAs are accessed via {@code KeyStore} style aliases. Aliases + * are made up of a prefix identifying the source ("system:" vs + * "user:") and a suffix based on the OpenSSL X509_NAME_hash_old + * function of the CA's subject name. For example, the system CA for + * "C=US, O=VeriSign, Inc., OU=Class 3 Public Primary Certification + * Authority" could be represented as "system:7651b327.0". By using + * the subject hash, operations such as {@link #getCertificateAlias + * getCertificateAlias} can be implemented efficiently without + * scanning the entire store. + * + * <p>In addition to supporting the {@code RootKeyStoreSpi} + * implementation, {@code TrustedCertificateStore} also provides the + * additional public methods {@link #isTrustAnchor} and {@link + * #findIssuer} to allow efficient lookup operations for CAs again + * based on the file naming convention. + * + * <p>The KeyChainService users the {@link installCertificate} and + * {@link #deleteCertificateEntry} to install user CAs as well as + * delete those user CAs as well as system CAs. The deletion of system + * CAs is performed by placing an exact copy of that CA in the deleted + * directory. Such deletions are intended to persist across upgrades + * but not intended to mask a CA with a matching name or public key + * but is otherwise reissued in a system update. Reinstalling a + * deleted system certificate simply removes the copy from the deleted + * directory, reenabling the original in the system directory. + * + * <p>Note that the default mutable directory is created by init via + * configuration in the system/core/rootdir/init.rc file. The + * directive "mkdir /data/misc/keychain 0775 keychain keychain" + * ensures that its owner and group are the keychain uid and keychain + * gid and that it is world readable but only writable by the keychain + * user. + */ +public final class TrustedCertificateStore { + + private static final String PREFIX_SYSTEM = "system:"; + private static final String PREFIX_USER = "user:"; + + public static final boolean isSystem(String alias) { + return alias.startsWith(PREFIX_SYSTEM); + } + public static final boolean isUser(String alias) { + return alias.startsWith(PREFIX_USER); + } + + private static final File CA_CERTS_DIR_SYSTEM; + private static final File CA_CERTS_DIR_ADDED; + private static final File CA_CERTS_DIR_DELETED; + private static final CertificateFactory CERT_FACTORY; + static { + String ANDROID_ROOT = System.getenv("ANDROID_ROOT"); + String ANDROID_DATA = System.getenv("ANDROID_DATA"); + CA_CERTS_DIR_SYSTEM = new File(ANDROID_ROOT + "/etc/security/cacerts"); + CA_CERTS_DIR_ADDED = new File(ANDROID_DATA + "/misc/keychain/cacerts-added"); + CA_CERTS_DIR_DELETED = new File(ANDROID_DATA + "/misc/keychain/cacerts-removed"); + + try { + CERT_FACTORY = CertificateFactory.getInstance("X509"); + } catch (CertificateException e) { + throw new AssertionError(e); + } + } + + private final File systemDir; + private final File addedDir; + private final File deletedDir; + + public TrustedCertificateStore() { + this(CA_CERTS_DIR_SYSTEM, CA_CERTS_DIR_ADDED, CA_CERTS_DIR_DELETED); + } + + public TrustedCertificateStore(File systemDir, File addedDir, File deletedDir) { + if (!systemDir.isDirectory()) { + throw new IllegalStateException(systemDir + " is not a directory"); + } + + this.systemDir = systemDir; + this.addedDir = addedDir; + this.deletedDir = deletedDir; + } + + public Certificate getCertificate(String alias) { + File file = fileForAlias(alias); + if (file == null || (isUser(alias) && isTombstone(file))) { + return null; + } + X509Certificate cert = readCertificate(file); + if (cert == null || (isSystem(alias) && isDeletedSystemCertificate(cert))) { + // skip malformed certs as well as deleted system ones + return null; + } + return cert; + } + + private File fileForAlias(String alias) { + if (alias == null) { + throw new NullPointerException("alias == null"); + } + File file; + if (isSystem(alias)) { + file = new File(systemDir, alias.substring(PREFIX_SYSTEM.length())); + } else if (isUser(alias)) { + file = new File(addedDir, alias.substring(PREFIX_USER.length())); + } else { + return null; + } + if (!file.exists() || isTombstone(file)) { + // silently elide tombstones + return null; + } + return file; + } + + private boolean isTombstone(File file) { + return file.length() == 0; + } + + private X509Certificate readCertificate(File file) { + if (!file.isFile()) { + return null; + } + InputStream is = null; + try { + is = new BufferedInputStream(new FileInputStream(file)); + return (X509Certificate) CERT_FACTORY.generateCertificate(is); + } catch (IOException e) { + return null; + } catch (CertificateException e) { + // reading a cert while its being installed can lead to this. + // just pretend like its not available yet. + return null; + } finally { + IoUtils.closeQuietly(is); + } + } + + private void writeCertificate(File file, X509Certificate cert) + throws IOException, CertificateException { + File dir = file.getParentFile(); + dir.mkdirs(); + dir.setReadable(true, false); + dir.setExecutable(true, false); + OutputStream os = null; + try { + os = new FileOutputStream(file); + os.write(cert.getEncoded()); + } finally { + os.close(); + } + file.setReadable(true, false); + } + + private boolean isDeletedSystemCertificate(X509Certificate x) { + return getCertificateFile(deletedDir, x).exists(); + } + + public Date getCreationDate(String alias) { + // containsAlias check ensures the later fileForAlias result + // was not a deleted system cert. + if (!containsAlias(alias)) { + return null; + } + File file = fileForAlias(alias); + if (file == null) { + return null; + } + long time = file.lastModified(); + if (time == 0) { + return null; + } + return new Date(time); + } + + public Set<String> aliases() { + Set<String> result = new HashSet<String>(); + addAliases(result, PREFIX_USER, addedDir); + addAliases(result, PREFIX_SYSTEM, systemDir); + return result; + } + + private void addAliases(Set<String> result, String prefix, File dir) { + String[] files = dir.list(); + if (files == null) { + return; + } + for (String filename : files) { + String alias = prefix + filename; + if (containsAlias(alias)) { + result.add(alias); + } + } + } + + public boolean containsAlias(String alias) { + return getCertificate(alias) != null; + } + + public String getCertificateAlias(Certificate c) { + if (c == null || !(c instanceof X509Certificate)) { + return null; + } + X509Certificate x = (X509Certificate) c; + File user = getCertificateFile(addedDir, x); + if (user.exists()) { + return PREFIX_USER + user.getName(); + } + if (isDeletedSystemCertificate(x)) { + return null; + } + File system = getCertificateFile(systemDir, x); + if (system.exists()) { + return PREFIX_SYSTEM + system.getName(); + } + return null; + } + + /** + * Returns a File for where the certificate is found if it exists + * or where it should be installed if it does not exist. The + * caller can disambiguate these cases by calling {@code + * File.exists()} on the result. + */ + private File getCertificateFile(File dir, final X509Certificate x) { + // compare X509Certificate.getEncoded values + CertSelector selector = new CertSelector() { + @Override public boolean match(X509Certificate cert) { + return cert.equals(x); + } + }; + return findCert(dir, x.getSubjectX500Principal(), selector, File.class); + } + + /** + * This non-{@code KeyStoreSpi} public interface is used by {@code + * TrustManagerImpl} to locate a CA certificate with the same name + * and public key as the provided {@code X509Certificate}. We + * match on the name and public key and not the entire certificate + * since a CA may be reissued with the same name and PublicKey but + * with other differences (for example when switching signature + * from md2WithRSAEncryption to SHA1withRSA) + */ + public boolean isTrustAnchor(final X509Certificate c) { + // compare X509Certificate.getPublicKey values + CertSelector selector = new CertSelector() { + @Override public boolean match(X509Certificate ca) { + return ca.getPublicKey().equals(c.getPublicKey()); + } + }; + boolean user = findCert(addedDir, + c.getSubjectX500Principal(), + selector, + Boolean.class); + if (user) { + return true; + } + X509Certificate system = findCert(systemDir, + c.getSubjectX500Principal(), + selector, + X509Certificate.class); + return system != null && !isDeletedSystemCertificate(system); + } + + /** + * This non-{@code KeyStoreSpi} public interface is used by {@code + * TrustManagerImpl} to locate the CA certificate that signed the + * provided {@code X509Certificate}. + */ + public X509Certificate findIssuer(final X509Certificate c) { + // match on verified issuer of Certificate + CertSelector selector = new CertSelector() { + @Override public boolean match(X509Certificate ca) { + try { + c.verify(ca.getPublicKey()); + return true; + } catch (Exception e) { + return false; + } + } + }; + X509Certificate user = findCert(addedDir, + c.getIssuerX500Principal(), + selector, + X509Certificate.class); + if (user != null) { + return user; + } + X509Certificate system = findCert(systemDir, + c.getIssuerX500Principal(), + selector, + X509Certificate.class); + if (system != null && !isDeletedSystemCertificate(system)) { + return system; + } + return null; + } + + // like java.security.cert.CertSelector but with X509Certificate and without cloning + private static interface CertSelector { + public boolean match(X509Certificate cert); + } + + private <T> T findCert( + File dir, X500Principal subject, CertSelector selector, Class<T> desiredReturnType) { + + String hash = hash(subject); + for (int index = 0; true; index++) { + File file = file(dir, hash, index); + if (!file.isFile()) { + // could not find a match, no file exists, bail + if (desiredReturnType == Boolean.class) { + return (T) Boolean.FALSE; + } + if (desiredReturnType == File.class) { + // we return file so that caller that wants to + // write knows what the next available has + // location is + return (T) file; + } + return null; + } + if (isTombstone(file)) { + continue; + } + X509Certificate cert = readCertificate(file); + if (cert == null) { + // skip problem certificates + continue; + } + if (selector.match(cert)) { + if (desiredReturnType == X509Certificate.class) { + return (T) cert; + } + if (desiredReturnType == Boolean.class) { + return (T) Boolean.TRUE; + } + if (desiredReturnType == File.class) { + return (T) file; + } + throw new AssertionError(); + } + } + } + + private String hash(X500Principal name) { + int hash = NativeCrypto.X509_NAME_hash_old(name); + return IntegralToString.intToHexString(hash, false, 8); + } + + private File file(File dir, String hash, int index) { + return new File(dir, hash + '.' + index); + } + + /** + * This non-{@code KeyStoreSpi} public interface is used by the + * {@code KeyChainService} to install new CA certificates. It + * silently ignores the certificate if it already exists in the + * store. + */ + public void installCertificate(X509Certificate cert) throws IOException, CertificateException { + if (cert == null) { + throw new NullPointerException("cert == null"); + } + File system = getCertificateFile(systemDir, cert); + if (system.exists()) { + File deleted = getCertificateFile(deletedDir, cert); + if (deleted.exists()) { + // we have a system cert that was marked deleted. + // remove the deleted marker to expose the original + if (!deleted.delete()) { + throw new IOException("Could not remove " + deleted); + } + return; + } + // otherwise we just have a dup of an existing system cert. + // return taking no further action. + return; + } + File user = getCertificateFile(addedDir, cert); + if (user.exists()) { + // we have an already installed user cert, bail. + return; + } + // install the user cert + writeCertificate(user, cert); + } + + /** + * This could be considered the implementation of {@code + * RootKeyStoreSpi.engineDeleteEntry} but we consider + * RootKeyStoreSpi to be read only. Instead, this is used by the + * {@code KeyChainService} to delete CA certificates. + */ + public void deleteCertificateEntry(String alias) throws IOException, CertificateException { + if (alias == null) { + return; + } + File file = fileForAlias(alias); + if (file == null) { + return; + } + if (isSystem(alias)) { + X509Certificate cert = readCertificate(file); + if (cert == null) { + // skip problem certificates + return; + } + File deleted = getCertificateFile(deletedDir, cert); + if (deleted.exists()) { + // already deleted system certificate + return; + } + // write copy of system cert to marked as deleted + writeCertificate(deleted, cert); + return; + } + if (isUser(alias)) { + // truncate the file to make a tombstone by opening and closing. + // we need ensure that we don't leave a gap before a valid cert. + new FileOutputStream(file).close(); + removeUnnecessaryTombstones(alias); + return; + } + // non-existant user cert, nothing to delete + } + + private void removeUnnecessaryTombstones(String alias) throws IOException { + if (!isUser(alias)) { + throw new AssertionError(alias); + } + int dotIndex = alias.lastIndexOf('.'); + if (dotIndex == -1) { + throw new AssertionError(alias); + } + + String hash = alias.substring(PREFIX_USER.length(), dotIndex); + int lastTombstoneIndex = Integer.parseInt(alias.substring(dotIndex + 1)); + + if (file(addedDir, hash, lastTombstoneIndex + 1).exists()) { + return; + } + while (lastTombstoneIndex >= 0) { + File file = file(addedDir, hash, lastTombstoneIndex); + if (!isTombstone(file)) { + break; + } + if (!file.delete()) { + throw new IOException("Could not remove " + file); + } + lastTombstoneIndex--; + } + } +} diff --git a/luni/src/test/java/libcore/java/security/KeyStoreTest.java b/luni/src/test/java/libcore/java/security/KeyStoreTest.java index f255392..af66ded 100644 --- a/luni/src/test/java/libcore/java/security/KeyStoreTest.java +++ b/luni/src/test/java/libcore/java/security/KeyStoreTest.java @@ -54,16 +54,10 @@ import libcore.io.IoUtils; public class KeyStoreTest extends TestCase { - private static final PrivateKeyEntry PRIVATE_KEY; - private static final PrivateKeyEntry PRIVATE_KEY_2; - static { - try { - PRIVATE_KEY = TestKeyStore.getServer().getPrivateKey("RSA", "RSA"); - PRIVATE_KEY_2 = TestKeyStore.getClientCertificate().getPrivateKey("RSA", "RSA"); - } catch (Exception e) { - throw new RuntimeException(e); - } - } + private static final PrivateKeyEntry PRIVATE_KEY + = TestKeyStore.getServer().getPrivateKey("RSA", "RSA"); + private static final PrivateKeyEntry PRIVATE_KEY_2 + = TestKeyStore.getClientCertificate().getPrivateKey("RSA", "RSA"); private static final SecretKey SECRET_KEY = generateSecretKey(); private static final SecretKey SECRET_KEY_2 = generateSecretKey(); diff --git a/luni/src/test/java/org/apache/harmony/xnet/provider/jsse/NativeCryptoTest.java b/luni/src/test/java/org/apache/harmony/xnet/provider/jsse/NativeCryptoTest.java index c0be701..b5f8a82 100644 --- a/luni/src/test/java/org/apache/harmony/xnet/provider/jsse/NativeCryptoTest.java +++ b/luni/src/test/java/org/apache/harmony/xnet/provider/jsse/NativeCryptoTest.java @@ -24,8 +24,7 @@ import java.net.SocketTimeoutException; import java.security.KeyStore.PrivateKeyEntry; import java.security.KeyStore; import java.security.KeyStoreException; -import libcore.java.security.StandardNames; -import libcore.java.security.TestKeyStore; +import java.security.cert.Certificate; import java.security.cert.CertificateEncodingException; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; @@ -42,6 +41,8 @@ import javax.net.ssl.SSLException; import javax.net.ssl.SSLProtocolException; import javax.security.auth.x500.X500Principal; import junit.framework.TestCase; +import libcore.java.security.StandardNames; +import libcore.java.security.TestKeyStore; import org.apache.harmony.xnet.provider.jsse.CipherSuite; import org.apache.harmony.xnet.provider.jsse.NativeCrypto.SSLHandshakeCallbacks; @@ -107,28 +108,23 @@ public class NativeCryptoTest extends TestCase { NativeCrypto.SSL_CTX_free(c); } - private static final PrivateKeyEntry SERVER_PRIVATE_KEY_ENTRY; - private static final byte[] SERVER_PRIVATE_KEY; - private static final byte[][] SERVER_CERTIFICATES; - private static final PrivateKeyEntry CLIENT_PRIVATE_KEY_ENTRY; - private static final byte[] CLIENT_PRIVATE_KEY; - private static final byte[][] CLIENT_CERTIFICATES; - - static { + private static final PrivateKeyEntry SERVER_PRIVATE_KEY_ENTRY + = TestKeyStore.getServer().getPrivateKey("RSA", "RSA"); + private static final byte[] SERVER_PRIVATE_KEY + = SERVER_PRIVATE_KEY_ENTRY.getPrivateKey().getEncoded(); + private static final byte[][] SERVER_CERTIFICATES + = encodeCertificates(SERVER_PRIVATE_KEY_ENTRY.getCertificateChain()); + private static final PrivateKeyEntry CLIENT_PRIVATE_KEY_ENTRY + = TestKeyStore.getClientCertificate().getPrivateKey("RSA", "RSA"); + private static final byte[] CLIENT_PRIVATE_KEY + = CLIENT_PRIVATE_KEY_ENTRY.getPrivateKey().getEncoded(); + private static final byte[][] CLIENT_CERTIFICATES + = encodeCertificates(CLIENT_PRIVATE_KEY_ENTRY.getCertificateChain()); + + private static byte[][] encodeCertificates (Certificate[] certificates) { try { - SERVER_PRIVATE_KEY_ENTRY - = TestKeyStore.getServer().getPrivateKey("RSA", "RSA"); - SERVER_PRIVATE_KEY - = SERVER_PRIVATE_KEY_ENTRY.getPrivateKey().getEncoded(); - SERVER_CERTIFICATES = NativeCrypto.encodeCertificates( - SERVER_PRIVATE_KEY_ENTRY.getCertificateChain()); - CLIENT_PRIVATE_KEY_ENTRY - = TestKeyStore.getClientCertificate().getPrivateKey("RSA", "RSA"); - CLIENT_PRIVATE_KEY - = CLIENT_PRIVATE_KEY_ENTRY.getPrivateKey().getEncoded(); - CLIENT_CERTIFICATES = NativeCrypto.encodeCertificates( - CLIENT_PRIVATE_KEY_ENTRY.getCertificateChain()); - } catch (Exception e) { + return NativeCrypto.encodeCertificates(certificates); + } catch (CertificateEncodingException e) { throw new RuntimeException(e); } } @@ -1786,4 +1782,13 @@ public class NativeCryptoTest extends TestCase { // positively testing by test_i2d_SSL_SESSION } + + public void test_X509_NAME_hashes() { + // ensure these hash functions are stable over time since the + // /system/etc/security/cacerts CA filenames have to be + // consistent with the output. + X500Principal name = new X500Principal("CN=localhost"); + assertEquals(-1372642656, NativeCrypto.X509_NAME_hash(name)); // SHA1 + assertEquals(-1626170662, NativeCrypto.X509_NAME_hash_old(name)); // MD5 + } } diff --git a/luni/src/test/java/org/apache/harmony/xnet/provider/jsse/TrustedCertificateStoreTest.java b/luni/src/test/java/org/apache/harmony/xnet/provider/jsse/TrustedCertificateStoreTest.java new file mode 100644 index 0000000..7bac8e8 --- /dev/null +++ b/luni/src/test/java/org/apache/harmony/xnet/provider/jsse/TrustedCertificateStoreTest.java @@ -0,0 +1,513 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.harmony.xnet.provider.jsse; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.OutputStream; +import java.security.KeyStore; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.NoSuchElementException; +import java.util.Set; +import javax.security.auth.x500.X500Principal; +import junit.framework.TestCase; +import libcore.java.security.TestKeyStore; + +public class TrustedCertificateStoreTest extends TestCase { + + private static final File DIR_TEMP = new File(System.getProperty("java.io.tmpdir")); + private static final File DIR_TEST = new File(DIR_TEMP, "test"); + private static final File DIR_SYSTEM = new File(DIR_TEST, "system"); + private static final File DIR_ADDED = new File(DIR_TEST, "added"); + private static final File DIR_DELETED = new File(DIR_TEST, "removed"); + + private static final X509Certificate CA1 = TestKeyStore.getClient().getRootCertificate("RSA"); + private static final X509Certificate CA2 + = TestKeyStore.getClientCA2().getRootCertificate("RSA"); + + private static final KeyStore.PrivateKeyEntry PRIVATE + = TestKeyStore.getServer().getPrivateKey("RSA", "RSA"); + private static final X509Certificate[] CHAIN = (X509Certificate[])PRIVATE.getCertificateChain(); + + private static final X509Certificate CA3_WITH_CA1_SUBJECT + = new TestKeyStore.Builder() + .aliasPrefix("unused") + .subject(CA1.getSubjectX500Principal()) + .ca(true) + .build().getRootCertificate("RSA"); + + + private static final String ALIAS_SYSTEM_CA1 = alias(false, CA1, 0); + private static final String ALIAS_SYSTEM_CA2 = alias(false, CA2, 0); + private static final String ALIAS_USER_CA1 = alias(true, CA1, 0); + private static final String ALIAS_USER_CA2 = alias(true, CA2, 0); + + private static final String ALIAS_SYSTEM_CHAIN0 = alias(false, CHAIN[0], 0); + private static final String ALIAS_SYSTEM_CHAIN1 = alias(false, CHAIN[1], 0); + private static final String ALIAS_SYSTEM_CHAIN2 = alias(false, CHAIN[2], 0); + private static final String ALIAS_USER_CHAIN0 = alias(true, CHAIN[0], 0); + private static final String ALIAS_USER_CHAIN1 = alias(true, CHAIN[1], 0); + private static final String ALIAS_USER_CHAIN2 = alias(true, CHAIN[2], 0); + + private static final String ALIAS_SYSTEM_CA3 = alias(false, CA3_WITH_CA1_SUBJECT, 0); + private static final String ALIAS_SYSTEM_CA3_COLLISION + = alias(false, CA3_WITH_CA1_SUBJECT, 1); + private static final String ALIAS_USER_CA3 = alias(true, CA3_WITH_CA1_SUBJECT, 0); + private static final String ALIAS_USER_CA3_COLLISION + = alias(true, CA3_WITH_CA1_SUBJECT, 1); + + private TrustedCertificateStore store; + + @Override protected void setUp() { + setupStore(); + } + + private void setupStore() { + DIR_SYSTEM.mkdirs(); + createStore(); + } + + private void createStore() { + store = new TrustedCertificateStore(DIR_SYSTEM, DIR_ADDED, DIR_DELETED); + } + + @Override protected void tearDown() { + cleanStore(); + } + + private void cleanStore() { + for (File dir : new File[] { DIR_SYSTEM, DIR_ADDED, DIR_DELETED, DIR_TEST }) { + File[] files = dir.listFiles(); + if (files == null) { + continue; + } + for (File file : files) { + assertTrue(file.delete()); + } + } + store = null; + } + + private void resetStore() { + cleanStore(); + setupStore(); + } + + public void testEmptyDirectories() throws Exception { + assertEmpty(); + } + + public void testOneSystemOneDeleted() throws Exception { + install(CA1, ALIAS_SYSTEM_CA1); + store.deleteCertificateEntry(ALIAS_SYSTEM_CA1); + assertEmpty(); + assertDeleted(CA1, ALIAS_SYSTEM_CA1); + } + + public void testTwoSystemTwoDeleted() throws Exception { + install(CA1, ALIAS_SYSTEM_CA1); + store.deleteCertificateEntry(ALIAS_SYSTEM_CA1); + install(CA2, ALIAS_SYSTEM_CA2); + store.deleteCertificateEntry(ALIAS_SYSTEM_CA2); + assertEmpty(); + assertDeleted(CA1, ALIAS_SYSTEM_CA1); + assertDeleted(CA2, ALIAS_SYSTEM_CA2); + } + + public void testPartialFileIsIgnored() throws Exception { + File file = file(ALIAS_SYSTEM_CA1); + OutputStream os = new FileOutputStream(file); + os.write(0); + os.close(); + assertTrue(file.exists()); + assertEmpty(); + assertTrue(file.exists()); + } + + private void assertEmpty() throws Exception { + try { + store.getCertificate(null); + fail(); + } catch (NullPointerException expected) { + } + assertNull(store.getCertificate("")); + + try { + store.getCreationDate(null); + fail(); + } catch (NullPointerException expected) { + } + assertNull(store.getCreationDate("")); + + Set<String> s = store.aliases(); + assertNotNull(s); + assertTrue(s.isEmpty()); + assertAliases(); + + try { + store.containsAlias(null); + fail(); + } catch (NullPointerException expected) { + } + assertFalse(store.containsAlias("")); + + assertNull(store.getCertificateAlias(null)); + assertNull(store.getCertificateAlias(CA1)); + + try { + store.isTrustAnchor(null); + fail(); + } catch (NullPointerException expected) { + } + assertFalse(store.isTrustAnchor(CA1)); + + try { + store.findIssuer(null); + fail(); + } catch (NullPointerException expected) { + } + assertNull(store.findIssuer(CA1)); + + try { + store.installCertificate(null); + fail(); + } catch (NullPointerException expected) { + } + + store.deleteCertificateEntry(null); + store.deleteCertificateEntry(""); + + String[] userFiles = DIR_ADDED.list(); + assertTrue(userFiles == null || userFiles.length == 0); + } + + public void testTwoSystem() throws Exception { + testTwo(CA1, ALIAS_SYSTEM_CA1, + CA2, ALIAS_SYSTEM_CA2); + } + + public void testTwoUser() throws Exception { + testTwo(CA1, ALIAS_USER_CA1, + CA2, ALIAS_USER_CA2); + } + + public void testOneSystemOneUser() throws Exception { + testTwo(CA1, ALIAS_SYSTEM_CA1, + CA2, ALIAS_USER_CA2); + } + + public void testTwoSystemSameSubject() throws Exception { + testTwo(CA1, ALIAS_SYSTEM_CA1, + CA3_WITH_CA1_SUBJECT, ALIAS_SYSTEM_CA3_COLLISION); + } + + public void testTwoUserSameSubject() throws Exception { + testTwo(CA1, ALIAS_USER_CA1, + CA3_WITH_CA1_SUBJECT, ALIAS_USER_CA3_COLLISION); + + store.deleteCertificateEntry(ALIAS_USER_CA1); + assertDeleted(CA1, ALIAS_USER_CA1); + assertTombstone(ALIAS_USER_CA1); + assertRootCA(CA3_WITH_CA1_SUBJECT, ALIAS_USER_CA3_COLLISION); + assertAliases(ALIAS_USER_CA3_COLLISION); + + store.deleteCertificateEntry(ALIAS_USER_CA3_COLLISION); + assertDeleted(CA3_WITH_CA1_SUBJECT, ALIAS_USER_CA3_COLLISION); + assertNoTombstone(ALIAS_USER_CA3_COLLISION); + assertNoTombstone(ALIAS_USER_CA1); + assertEmpty(); + } + + public void testOneSystemOneUserSameSubject() throws Exception { + testTwo(CA1, ALIAS_SYSTEM_CA1, + CA3_WITH_CA1_SUBJECT, ALIAS_USER_CA3); + testTwo(CA1, ALIAS_USER_CA1, + CA3_WITH_CA1_SUBJECT, ALIAS_SYSTEM_CA3); + } + + private void testTwo(X509Certificate x1, String alias1, + X509Certificate x2, String alias2) { + install(x1, alias1); + install(x2, alias2); + assertRootCA(x1, alias1); + assertRootCA(x2, alias2); + assertAliases(alias1, alias2); + } + + + public void testOneSystemOneUserOneDeleted() throws Exception { + install(CA1, ALIAS_SYSTEM_CA1); + store.installCertificate(CA2); + store.deleteCertificateEntry(ALIAS_SYSTEM_CA1); + assertDeleted(CA1, ALIAS_SYSTEM_CA1); + assertRootCA(CA2, ALIAS_USER_CA2); + assertAliases(ALIAS_USER_CA2); + } + + public void testOneSystemOneUserOneDeletedSameSubject() throws Exception { + install(CA1, ALIAS_SYSTEM_CA1); + store.installCertificate(CA3_WITH_CA1_SUBJECT); + store.deleteCertificateEntry(ALIAS_SYSTEM_CA1); + assertDeleted(CA1, ALIAS_SYSTEM_CA1); + assertRootCA(CA3_WITH_CA1_SUBJECT, ALIAS_USER_CA3); + assertAliases(ALIAS_USER_CA3); + } + + public void testUserMaskingSystem() throws Exception { + install(CA1, ALIAS_SYSTEM_CA1); + install(CA1, ALIAS_USER_CA1); + assertMasked(CA1, ALIAS_SYSTEM_CA1); + assertRootCA(CA1, ALIAS_USER_CA1); + assertAliases(ALIAS_SYSTEM_CA1, ALIAS_USER_CA1); + } + + public void testChain() throws Exception { + testChain(ALIAS_SYSTEM_CHAIN1, ALIAS_SYSTEM_CHAIN2); + testChain(ALIAS_SYSTEM_CHAIN1, ALIAS_USER_CHAIN2); + testChain(ALIAS_USER_CHAIN1, ALIAS_SYSTEM_CA1); + testChain(ALIAS_USER_CHAIN1, ALIAS_USER_CHAIN2); + } + + private void testChain(String alias1, String alias2) throws Exception { + install(CHAIN[1], alias1); + install(CHAIN[2], alias2); + assertIntermediateCA(CHAIN[1], alias1); + assertRootCA(CHAIN[2], alias2); + assertAliases(alias1, alias2); + assertEquals(CHAIN[2], store.findIssuer(CHAIN[1])); + assertEquals(CHAIN[1], store.findIssuer(CHAIN[0])); + resetStore(); + } + + public void testMissingSystemDirectory() { + cleanStore(); + try { + createStore(); + fail(); + } catch (IllegalStateException expected) { + } + } + + public void testWithExistingUserDirectories() throws Exception { + DIR_ADDED.mkdirs(); + DIR_DELETED.mkdirs(); + install(CA1, ALIAS_SYSTEM_CA1); + assertRootCA(CA1, ALIAS_SYSTEM_CA1); + assertAliases(ALIAS_SYSTEM_CA1); + } + + public void testIsTrustAnchorWithReissuedCA() throws Exception { + PublicKey publicKey = PRIVATE.getCertificate().getPublicKey(); + PrivateKey privateKey = PRIVATE.getPrivateKey(); + String name = "CN=CA4"; + X509Certificate ca1 = TestKeyStore.createCA(publicKey, privateKey, name); + Thread.sleep(1 * 1000); // wait to ensure CAs vary by expiration + X509Certificate ca2 = TestKeyStore.createCA(publicKey, privateKey, name); + assertFalse(ca1.equals(ca2)); + + String systemAlias = alias(false, ca1, 0); + install(ca1, systemAlias); + assertRootCA(ca1, systemAlias); + assertTrue(store.isTrustAnchor(ca2)); + assertEquals(ca1, store.findIssuer(ca2)); + resetStore(); + + String userAlias = alias(true, ca1, 0); + store.installCertificate(ca1); + assertRootCA(ca1, userAlias); + assertTrue(store.isTrustAnchor(ca2)); + assertEquals(ca1, store.findIssuer(ca2)); + resetStore(); + } + + public void testInstallEmpty() throws Exception { + store.installCertificate(CA1); + assertRootCA(CA1, ALIAS_USER_CA1); + assertAliases(ALIAS_USER_CA1); + + // reinstalling should not change anything + store.installCertificate(CA1); + assertRootCA(CA1, ALIAS_USER_CA1); + assertAliases(ALIAS_USER_CA1); + } + + public void testInstallEmptySystemExists() throws Exception { + install(CA1, ALIAS_SYSTEM_CA1); + assertRootCA(CA1, ALIAS_SYSTEM_CA1); + assertAliases(ALIAS_SYSTEM_CA1); + + // reinstalling should not affect system CA + store.installCertificate(CA1); + assertRootCA(CA1, ALIAS_SYSTEM_CA1); + assertAliases(ALIAS_SYSTEM_CA1); + + } + + public void testInstallEmptyDeletedSystemExists() throws Exception { + install(CA1, ALIAS_SYSTEM_CA1); + store.deleteCertificateEntry(ALIAS_SYSTEM_CA1); + assertEmpty(); + assertDeleted(CA1, ALIAS_SYSTEM_CA1); + + // installing should restore deleted system CA + store.installCertificate(CA1); + assertRootCA(CA1, ALIAS_SYSTEM_CA1); + assertAliases(ALIAS_SYSTEM_CA1); + } + + public void testDeleteEmpty() throws Exception { + store.deleteCertificateEntry(ALIAS_SYSTEM_CA1); + assertEmpty(); + assertDeleted(CA1, ALIAS_SYSTEM_CA1); + } + + public void testDeleteUser() throws Exception { + store.installCertificate(CA1); + assertRootCA(CA1, ALIAS_USER_CA1); + assertAliases(ALIAS_USER_CA1); + + store.deleteCertificateEntry(ALIAS_USER_CA1); + assertEmpty(); + assertDeleted(CA1, ALIAS_USER_CA1); + assertNoTombstone(ALIAS_USER_CA1); + } + + public void testDeleteSystem() throws Exception { + install(CA1, ALIAS_SYSTEM_CA1); + assertRootCA(CA1, ALIAS_SYSTEM_CA1); + assertAliases(ALIAS_SYSTEM_CA1); + + store.deleteCertificateEntry(ALIAS_SYSTEM_CA1); + assertEmpty(); + assertDeleted(CA1, ALIAS_SYSTEM_CA1); + + // deleting again should not change anything + store.deleteCertificateEntry(ALIAS_SYSTEM_CA1); + assertEmpty(); + assertDeleted(CA1, ALIAS_SYSTEM_CA1); + } + + private void assertRootCA(X509Certificate x, String alias) { + assertIntermediateCA(x, alias); + assertEquals(x, store.findIssuer(x)); + } + + private void assertTrusted(X509Certificate x, String alias) { + assertEquals(x, store.getCertificate(alias)); + assertEquals(file(alias).lastModified(), store.getCreationDate(alias).getTime()); + assertTrue(store.containsAlias(alias)); + assertTrue(store.isTrustAnchor(x)); + } + + private void assertIntermediateCA(X509Certificate x, String alias) { + assertTrusted(x, alias); + assertEquals(alias, store.getCertificateAlias(x)); + } + + private void assertMasked(X509Certificate x, String alias) { + assertTrusted(x, alias); + assertFalse(alias.equals(store.getCertificateAlias(x))); + } + + private void assertDeleted(X509Certificate x, String alias) { + assertNull(store.getCertificate(alias)); + assertFalse(store.containsAlias(alias)); + assertNull(store.getCertificateAlias(x)); + assertFalse(store.isTrustAnchor(x)); + } + + private void assertTombstone(String alias) { + assertTrue(TrustedCertificateStore.isUser(alias)); + File file = file(alias); + assertTrue(file.exists()); + assertEquals(0, file.length()); + } + + private void assertNoTombstone(String alias) { + assertTrue(TrustedCertificateStore.isUser(alias)); + assertFalse(file(alias).exists()); + } + + private void assertAliases(String... aliases) { + Set<String> expected = new HashSet<String>(Arrays.asList(aliases)); + Set<String> actual = new HashSet<String>(); + for (String alias : store.aliases()) { + if (TrustedCertificateStore.isSystem(alias) || TrustedCertificateStore.isUser(alias)) { + actual.add(alias); + } else { + throw new AssertionError(alias); + } + } + assertEquals(expected, actual); + } + + /** + * format a certificate alias + */ + private static String alias(boolean user, X509Certificate x, int index) { + String prefix = user ? "user:" : "system:"; + + X500Principal subject = x.getSubjectX500Principal(); + int intHash = NativeCrypto.X509_NAME_hash_old(subject); + String strHash = IntegralToString.intToHexString(intHash, false, 8); + + return prefix + strHash + '.' + index; + } + + /** + * Install certificate under specified alias + */ + private static void install(X509Certificate x, String alias) { + try { + File file = file(alias); + file.getParentFile().mkdirs(); + OutputStream out = new FileOutputStream(file); + out.write(x.getEncoded()); + out.close(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * Compute file for an alias + */ + private static File file(String alias) { + File dir; + if (TrustedCertificateStore.isSystem(alias)) { + dir = DIR_SYSTEM; + } else if (TrustedCertificateStore.isUser(alias)) { + dir = DIR_ADDED; + } else { + throw new IllegalArgumentException(alias); + } + + int index = alias.lastIndexOf(":"); + if (index == -1) { + throw new IllegalArgumentException(alias); + } + String filename = alias.substring(index+1); + + return new File(dir, filename); + } +} diff --git a/support/src/test/java/libcore/java/security/TestKeyStore.java b/support/src/test/java/libcore/java/security/TestKeyStore.java index 353ca20..6bb44a4 100644 --- a/support/src/test/java/libcore/java/security/TestKeyStore.java +++ b/support/src/test/java/libcore/java/security/TestKeyStore.java @@ -24,22 +24,19 @@ import com.android.org.bouncycastle.asn1.x509.GeneralSubtree; import com.android.org.bouncycastle.asn1.x509.KeyUsage; import com.android.org.bouncycastle.asn1.x509.NameConstraints; import com.android.org.bouncycastle.asn1.x509.X509Extensions; -import com.android.org.bouncycastle.asn1.x509.X509Name; -import com.android.org.bouncycastle.jce.X509Principal; import com.android.org.bouncycastle.jce.provider.BouncyCastleProvider; import com.android.org.bouncycastle.x509.X509V3CertificateGenerator; import java.io.ByteArrayInputStream; import java.io.PrintStream; import java.math.BigInteger; import java.net.InetAddress; -import java.net.UnknownHostException; import java.security.KeyFactory; import java.security.KeyPair; import java.security.KeyPairGenerator; -import java.security.KeyStore; import java.security.KeyStore.PasswordProtection; import java.security.KeyStore.PrivateKeyEntry; import java.security.KeyStore.TrustedCertificateEntry; +import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.Principal; @@ -56,13 +53,13 @@ import java.security.spec.PKCS8EncodedKeySpec; import java.util.ArrayList; import java.util.Collections; import java.util.Date; -import java.util.Hashtable; import java.util.List; import java.util.Vector; import javax.net.ssl.KeyManager; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; +import javax.security.auth.x500.X500Principal; import junit.framework.Assert; import libcore.javax.net.ssl.TestKeyManager; import libcore.javax.net.ssl.TestTrustManager; @@ -103,8 +100,7 @@ public final class TestKeyStore extends Assert { public final TestKeyManager keyManager; public final TestTrustManager trustManager; - private TestKeyStore(KeyStore keyStore, char[] storePassword, char[] keyPassword) - throws Exception { + private TestKeyStore(KeyStore keyStore, char[] storePassword, char[] keyPassword) { this.keyStore = keyStore; this.storePassword = storePassword; this.keyPassword = keyPassword; @@ -114,19 +110,26 @@ public final class TestKeyStore extends Assert { this.trustManager = (TestTrustManager)trustManagers[0]; } - public static KeyManager[] createKeyManagers(KeyStore keyStore, char[] storePassword) - throws Exception { - String kmfa = KeyManagerFactory.getDefaultAlgorithm(); - KeyManagerFactory kmf = KeyManagerFactory.getInstance(kmfa); - kmf.init(keyStore, storePassword); - return TestKeyManager.wrap(kmf.getKeyManagers()); + public static KeyManager[] createKeyManagers(KeyStore keyStore, char[] storePassword) { + try { + String kmfa = KeyManagerFactory.getDefaultAlgorithm(); + KeyManagerFactory kmf = KeyManagerFactory.getInstance(kmfa); + kmf.init(keyStore, storePassword); + return TestKeyManager.wrap(kmf.getKeyManagers()); + } catch (Exception e) { + throw new RuntimeException(e); + } } - public static TrustManager[] createTrustManagers(final KeyStore keyStore) throws Exception { - String tmfa = TrustManagerFactory.getDefaultAlgorithm(); - TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfa); - tmf.init(keyStore); - return TestTrustManager.wrap(tmf.getTrustManagers()); + public static TrustManager[] createTrustManagers(final KeyStore keyStore) { + try { + String tmfa = TrustManagerFactory.getDefaultAlgorithm(); + TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfa); + tmf.init(keyStore); + return TestTrustManager.wrap(tmf.getTrustManagers()); + } catch (Exception e) { + throw new RuntimeException(e); + } } /** @@ -136,40 +139,36 @@ public final class TestKeyStore extends Assert { if (ROOT_CA != null) { return; } - try { - ROOT_CA = new Builder() - .aliasPrefix("RootCA") - .subject("Test Root Certificate Authority") - .ca(true) - .build(); - TestKeyStore intermediateCa = new Builder() - .aliasPrefix("IntermediateCA") - .subject("Test Intermediate Certificate Authority") - .ca(true) - .signer(ROOT_CA.getPrivateKey("RSA", "RSA")) - .rootCa(ROOT_CA.getRootCertificate("RSA")) - .build(); - SERVER = new Builder() - .aliasPrefix("server") - .signer(intermediateCa.getPrivateKey("RSA", "RSA")) - .rootCa(intermediateCa.getRootCertificate("RSA")) - .build(); - CLIENT = new TestKeyStore(createClient(intermediateCa.keyStore), null, null); - CLIENT_CERTIFICATE = new Builder() - .aliasPrefix("client") - .subject("test@user") - .signer(intermediateCa.getPrivateKey("RSA", "RSA")) - .rootCa(intermediateCa.getRootCertificate("RSA")) - .build(); - TestKeyStore rootCa2 = new Builder() - .aliasPrefix("RootCA2") - .subject("Test Root Certificate Authority 2") - .ca(true) - .build(); - CLIENT_2 = new TestKeyStore(createClient(rootCa2.keyStore), null, null); - } catch (Exception e) { - throw new RuntimeException(e); - } + ROOT_CA = new Builder() + .aliasPrefix("RootCA") + .subject("CN=Test Root Certificate Authority") + .ca(true) + .build(); + TestKeyStore intermediateCa = new Builder() + .aliasPrefix("IntermediateCA") + .subject("CN=Test Intermediate Certificate Authority") + .ca(true) + .signer(ROOT_CA.getPrivateKey("RSA", "RSA")) + .rootCa(ROOT_CA.getRootCertificate("RSA")) + .build(); + SERVER = new Builder() + .aliasPrefix("server") + .signer(intermediateCa.getPrivateKey("RSA", "RSA")) + .rootCa(intermediateCa.getRootCertificate("RSA")) + .build(); + CLIENT = new TestKeyStore(createClient(intermediateCa.keyStore), null, null); + CLIENT_CERTIFICATE = new Builder() + .aliasPrefix("client") + .subject("emailAddress=test@user") + .signer(intermediateCa.getPrivateKey("RSA", "RSA")) + .rootCa(intermediateCa.getRootCertificate("RSA")) + .build(); + TestKeyStore rootCa2 = new Builder() + .aliasPrefix("RootCA2") + .subject("CN=Test Root Certificate Authority 2") + .ca(true) + .build(); + CLIENT_2 = new TestKeyStore(createClient(rootCa2.keyStore), null, null); } /** @@ -218,7 +217,7 @@ public final class TestKeyStore extends Assert { private char[] storePassword; private char[] keyPassword; private String aliasPrefix; - private X509Principal subject; + private X500Principal subject; private int keyUsage; private boolean ca; private PrivateKeyEntry signer; @@ -228,7 +227,7 @@ public final class TestKeyStore extends Assert { = new Vector<GeneralSubtree>(); private final Vector<GeneralSubtree> excludedNameConstraints = new Vector<GeneralSubtree>(); - public Builder() throws Exception { + public Builder() { subject = localhost(); } @@ -251,13 +250,13 @@ public final class TestKeyStore extends Assert { * Sets the subject common name. The default is the local host's * canonical name. */ - public Builder subject(X509Principal subject) { + public Builder subject(X500Principal subject) { this.subject = subject; return this; } public Builder subject(String commonName) { - return subject(x509Principal(commonName)); + return subject(new X500Principal(commonName)); } /** {@link KeyUsage} bit mask for 2.5.29.15 extension */ @@ -308,35 +307,39 @@ public final class TestKeyStore extends Assert { new GeneralName(GeneralName.iPAddress, new DEROctetString(ipAddress))); } - public TestKeyStore build() throws Exception { - if (StandardNames.IS_RI) { - // JKS does not allow null password - if (storePassword == null) { - storePassword = "password".toCharArray(); - } - if (keyPassword == null) { - keyPassword = "password".toCharArray(); + public TestKeyStore build() { + try { + if (StandardNames.IS_RI) { + // JKS does not allow null password + if (storePassword == null) { + storePassword = "password".toCharArray(); + } + if (keyPassword == null) { + keyPassword = "password".toCharArray(); + } } - } - KeyStore keyStore = createKeyStore(); - for (String keyAlgorithm : keyAlgorithms) { - String publicAlias = aliasPrefix + "-public-" + keyAlgorithm; - String privateAlias = aliasPrefix + "-private-" + keyAlgorithm; - if (keyAlgorithm.equals("EC_RSA") && signer == null && rootCa == null) { - createKeys(keyStore, keyAlgorithm, publicAlias, privateAlias, - privateKey(keyStore, keyPassword, "RSA", "RSA")); - continue; + KeyStore keyStore = createKeyStore(); + for (String keyAlgorithm : keyAlgorithms) { + String publicAlias = aliasPrefix + "-public-" + keyAlgorithm; + String privateAlias = aliasPrefix + "-private-" + keyAlgorithm; + if (keyAlgorithm.equals("EC_RSA") && signer == null && rootCa == null) { + createKeys(keyStore, keyAlgorithm, publicAlias, privateAlias, + privateKey(keyStore, keyPassword, "RSA", "RSA")); + continue; + } + createKeys(keyStore, keyAlgorithm, publicAlias, privateAlias, signer); } - createKeys(keyStore, keyAlgorithm, publicAlias, privateAlias, signer); - } - if (rootCa != null) { - keyStore.setCertificateEntry(aliasPrefix - + "-root-ca-" - + rootCa.getPublicKey().getAlgorithm(), - rootCa); + if (rootCa != null) { + keyStore.setCertificateEntry(aliasPrefix + + "-root-ca-" + + rootCa.getPublicKey().getAlgorithm(), + rootCa); + } + return new TestKeyStore(keyStore, storePassword, keyPassword); + } catch (Exception e) { + throw new RuntimeException(e); } - return new TestKeyStore(keyStore, storePassword, keyPassword); } /** @@ -382,20 +385,15 @@ public final class TestKeyStore extends Assert { } else { // 1.) we make the keys int keySize; - String signatureAlgorithm; if (keyAlgorithm.equals("RSA")) { keySize = StandardNames.IS_RI ? 1024 : 512; // 512 breaks SSL_RSA_EXPORT_* on RI - signatureAlgorithm = "sha1WithRSA"; } else if (keyAlgorithm.equals("DSA")) { keySize = 512; - signatureAlgorithm = "sha1WithDSA"; } else if (keyAlgorithm.equals("EC")) { keySize = 256; - signatureAlgorithm = "sha1WithECDSA"; } else if (keyAlgorithm.equals("EC_RSA")) { keySize = 256; keyAlgorithm = "EC"; - signatureAlgorithm = "sha1WithRSA"; } else { throw new IllegalArgumentException("Unknown key algorithm " + keyAlgorithm); } @@ -406,76 +404,15 @@ public final class TestKeyStore extends Assert { KeyPair kp = kpg.generateKeyPair(); privateKey = kp.getPrivate(); PublicKey publicKey = kp.getPublic(); - // 2.) use keys to make certificate - - // note that there doesn't seem to be a standard way to make a - // certificate using java.* or javax.*. The CertificateFactory - // interface assumes you want to read in a stream of bytes a - // factory specific format. So here we use Bouncy Castle's - // X509V3CertificateGenerator and related classes. - X509Principal issuer; - if (caCert == null) { - issuer = subject; - } else { - Principal xp = caCert.getSubjectDN(); - issuer = new X509Principal(new X509Name(xp.getName())); - } - - long millisPerDay = 24 * 60 * 60 * 1000; - long now = System.currentTimeMillis(); - Date start = new Date(now - millisPerDay); - Date end = new Date(now + millisPerDay); - BigInteger serial = BigInteger.valueOf(1); - - X509V3CertificateGenerator x509cg = new X509V3CertificateGenerator(); - x509cg.setSubjectDN(subject); - x509cg.setIssuerDN(issuer); - x509cg.setNotBefore(start); - x509cg.setNotAfter(end); - x509cg.setPublicKey(publicKey); - x509cg.setSignatureAlgorithm(signatureAlgorithm); - x509cg.setSerialNumber(serial); - if (keyUsage != 0) { - x509cg.addExtension(X509Extensions.KeyUsage, - true, - new KeyUsage(keyUsage)); - } - if (ca) { - x509cg.addExtension(X509Extensions.BasicConstraints, - true, - new BasicConstraints(true)); - } - for (GeneralName subjectAltName : subjectAltNames) { - x509cg.addExtension(X509Extensions.SubjectAlternativeName, false, - new GeneralNames(subjectAltName).getEncoded()); - } - if (!permittedNameConstraints.isEmpty() || !excludedNameConstraints.isEmpty()) { - x509cg.addExtension(X509Extensions.NameConstraints, true, - new NameConstraints(permittedNameConstraints, excludedNameConstraints)); - } + // 2.) use keys to make certificate + X500Principal issuer = ((caCert != null) + ? caCert.getSubjectX500Principal() + : subject); PrivateKey signingKey = (caKey == null) ? privateKey : caKey; - if (signingKey instanceof ECPrivateKey) { - /* - * bouncycastle needs its own ECPrivateKey implementation - */ - KeyFactory kf = KeyFactory.getInstance(keyAlgorithm, "BC"); - PKCS8EncodedKeySpec ks = new PKCS8EncodedKeySpec(signingKey.getEncoded()); - signingKey = kf.generatePrivate(ks); - } - x509c = x509cg.generateX509Certificate(signingKey); - if (StandardNames.IS_RI) { - /* - * The RI can't handle the BC EC signature algorithm - * string of "ECDSA", since it expects "...WITHEC...", - * so convert from BC to RI X509Certificate - * implementation via bytes. - */ - CertificateFactory cf = CertificateFactory.getInstance("X.509"); - ByteArrayInputStream bais = new ByteArrayInputStream(x509c.getEncoded()); - Certificate c = cf.generateCertificate(bais); - x509c = (X509Certificate) c; - } + x509c = createCertificate(publicKey, signingKey, subject, issuer, keyUsage, ca, + subjectAltNames, + permittedNameConstraints, excludedNameConstraints); } X509Certificate[] x509cc; @@ -500,20 +437,119 @@ public final class TestKeyStore extends Assert { return keyStore; } - private X509Principal localhost() throws UnknownHostException { - return x509Principal(InetAddress.getLoopbackAddress().getHostName()); + private X500Principal localhost() { + return new X500Principal("CN=" + InetAddress.getLoopbackAddress().getHostName()); } + } - /** - * Create an X509Principal with the given attributes - */ - public static X509Principal x509Principal(String commonName) { - Hashtable attributes = new Hashtable(); - attributes.put(X509Principal.CN, commonName); - return new X509Principal(attributes); + public static X509Certificate createCA(PublicKey publicKey, + PrivateKey privateKey, + String subject) { + try { + X500Principal principal = new X500Principal(subject); + return createCertificate(publicKey, privateKey, + principal, principal, + 0, true, + new Vector<GeneralName>(), + new Vector<GeneralSubtree>(), + new Vector<GeneralSubtree>()); + } catch (Exception e) { + throw new RuntimeException(e); } } + private static X509Certificate createCertificate( + PublicKey publicKey, + PrivateKey privateKey, + X500Principal subject, + X500Principal issuer, + int keyUsage, + boolean ca, + List<GeneralName> subjectAltNames, + Vector<GeneralSubtree> permittedNameConstraints, + Vector<GeneralSubtree> excludedNameConstraints) throws Exception { + // Note that there is no way to programmatically make a + // Certificate using java.* or javax.* APIs. The + // CertificateFactory interface assumes you want to read + // in a stream of bytes, typically the X.509 factory would + // allow ASN.1 DER encoded bytes and optionally some PEM + // formats. Here we use Bouncy Castle's + // X509V3CertificateGenerator and related classes. + + long millisPerDay = 24 * 60 * 60 * 1000; + long now = System.currentTimeMillis(); + Date start = new Date(now - millisPerDay); + Date end = new Date(now + millisPerDay); + BigInteger serial = BigInteger.valueOf(1); + + String keyAlgorithm = privateKey.getAlgorithm(); + String signatureAlgorithm; + if (keyAlgorithm.equals("RSA")) { + signatureAlgorithm = "sha1WithRSA"; + } else if (keyAlgorithm.equals("DSA")) { + signatureAlgorithm = "sha1WithDSA"; + } else if (keyAlgorithm.equals("EC")) { + signatureAlgorithm = "sha1WithECDSA"; + } else if (keyAlgorithm.equals("EC_RSA")) { + signatureAlgorithm = "sha1WithRSA"; + } else { + throw new IllegalArgumentException("Unknown key algorithm " + keyAlgorithm); + } + + X509V3CertificateGenerator x509cg = new X509V3CertificateGenerator(); + x509cg.setSubjectDN(subject); + x509cg.setIssuerDN(issuer); + x509cg.setNotBefore(start); + x509cg.setNotAfter(end); + x509cg.setPublicKey(publicKey); + x509cg.setSignatureAlgorithm(signatureAlgorithm); + x509cg.setSerialNumber(serial); + if (keyUsage != 0) { + x509cg.addExtension(X509Extensions.KeyUsage, + true, + new KeyUsage(keyUsage)); + } + if (ca) { + x509cg.addExtension(X509Extensions.BasicConstraints, + true, + new BasicConstraints(true)); + } + for (GeneralName subjectAltName : subjectAltNames) { + x509cg.addExtension(X509Extensions.SubjectAlternativeName, + false, + new GeneralNames(subjectAltName).getEncoded()); + } + if (!permittedNameConstraints.isEmpty() || !excludedNameConstraints.isEmpty()) { + x509cg.addExtension(X509Extensions.NameConstraints, + true, + new NameConstraints(permittedNameConstraints, + excludedNameConstraints)); + } + + if (privateKey instanceof ECPrivateKey) { + /* + * bouncycastle needs its own ECPrivateKey implementation + */ + KeyFactory kf = KeyFactory.getInstance(keyAlgorithm, "BC"); + PKCS8EncodedKeySpec ks = new PKCS8EncodedKeySpec(privateKey.getEncoded()); + privateKey = kf.generatePrivate(ks); + } + X509Certificate x509c = x509cg.generateX509Certificate(privateKey); + if (StandardNames.IS_RI) { + /* + * The RI can't handle the BC EC signature algorithm + * string of "ECDSA", since it expects "...WITHEC...", + * so convert from BC to RI X509Certificate + * implementation via bytes. + */ + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + ByteArrayInputStream bais = new ByteArrayInputStream(x509c.getEncoded()); + Certificate c = cf.generateCertificate(bais); + x509c = (X509Certificate) c; + } + return x509c; + } + /** * Return the key algorithm for a possible compound algorithm * identifier containing an underscore. If not underscore is @@ -551,10 +587,14 @@ public final class TestKeyStore extends Assert { * keyStorePassword argument, which can be null if a password is * not desired. */ - public static KeyStore createKeyStore() throws Exception { - KeyStore keyStore = KeyStore.getInstance(StandardNames.KEY_STORE_ALGORITHM); - keyStore.load(null, null); - return keyStore; + public static KeyStore createKeyStore() { + try { + KeyStore keyStore = KeyStore.getInstance(StandardNames.KEY_STORE_ALGORITHM); + keyStore.load(null, null); + return keyStore; + } catch (Exception e) { + throw new RuntimeException(e); + } } /** @@ -562,8 +602,7 @@ public final class TestKeyStore extends Assert { * algorithms. Throws IllegalStateException if there are are more * or less than one. */ - public PrivateKeyEntry getPrivateKey(String keyAlgorithm, String signatureAlgorithm) - throws Exception { + public PrivateKeyEntry getPrivateKey(String keyAlgorithm, String signatureAlgorithm) { return privateKey(keyStore, keyPassword, keyAlgorithm, signatureAlgorithm); } @@ -573,36 +612,40 @@ public final class TestKeyStore extends Assert { * or less than one. */ public static PrivateKeyEntry privateKey(KeyStore keyStore, char[] keyPassword, - String keyAlgorithm, String signatureAlgorithm) throws Exception { - PrivateKeyEntry found = null; - PasswordProtection password = new PasswordProtection(keyPassword); - for (String alias : Collections.list(keyStore.aliases())) { - if (!keyStore.entryInstanceOf(alias, PrivateKeyEntry.class)) { - continue; - } - PrivateKeyEntry privateKey = (PrivateKeyEntry) keyStore.getEntry(alias, password); - if (!privateKey.getPrivateKey().getAlgorithm().equals(keyAlgorithm)) { - continue; - } - X509Certificate certificate = (X509Certificate) privateKey.getCertificate(); - if (!certificate.getSigAlgName().contains(signatureAlgorithm)) { - continue; + String keyAlgorithm, String signatureAlgorithm) { + try { + PrivateKeyEntry found = null; + PasswordProtection password = new PasswordProtection(keyPassword); + for (String alias : Collections.list(keyStore.aliases())) { + if (!keyStore.entryInstanceOf(alias, PrivateKeyEntry.class)) { + continue; + } + PrivateKeyEntry privateKey = (PrivateKeyEntry) keyStore.getEntry(alias, password); + if (!privateKey.getPrivateKey().getAlgorithm().equals(keyAlgorithm)) { + continue; + } + X509Certificate certificate = (X509Certificate) privateKey.getCertificate(); + if (!certificate.getSigAlgName().contains(signatureAlgorithm)) { + continue; + } + if (found != null) { + throw new IllegalStateException("KeyStore has more than one private key for " + + " keyAlgorithm: " + keyAlgorithm + + " signatureAlgorithm: " + signatureAlgorithm + + "\nfirst: " + found.getPrivateKey() + + "\nsecond: " + privateKey.getPrivateKey() ); + } + found = privateKey; } - if (found != null) { - throw new IllegalStateException("KeyStore has more than one private key for " + if (found == null) { + throw new IllegalStateException("KeyStore contained no private key for " + " keyAlgorithm: " + keyAlgorithm - + " signatureAlgorithm: " + signatureAlgorithm - + "\nfirst: " + found.getPrivateKey() - + "\nsecond: " + privateKey.getPrivateKey() ); + + " signatureAlgorithm: " + signatureAlgorithm); } - found = privateKey; - } - if (found == null) { - throw new IllegalStateException("KeyStore contained no private key for " - + " keyAlgorithm: " + keyAlgorithm - + " signatureAlgorithm: " + signatureAlgorithm); + return found; + } catch (Exception e) { + throw new RuntimeException(e); } - return found; } /** @@ -660,7 +703,7 @@ public final class TestKeyStore extends Assert { * for the given algorithm. Throws IllegalStateException if there * are are more or less than one. */ - public Certificate getRootCertificate(String algorithm) throws Exception { + public X509Certificate getRootCertificate(String algorithm) { return rootCertificate(keyStore, algorithm); } @@ -669,44 +712,47 @@ public final class TestKeyStore extends Assert { * the given algorithm. Throws IllegalStateException if there are * are more or less than one. */ - public static Certificate rootCertificate(KeyStore keyStore, String algorithm) - throws Exception { - Certificate found = null; - for (String alias : Collections.list(keyStore.aliases())) { - if (!keyStore.entryInstanceOf(alias, TrustedCertificateEntry.class)) { - continue; - } - TrustedCertificateEntry certificateEntry = - (TrustedCertificateEntry) keyStore.getEntry(alias, null); - Certificate certificate = certificateEntry.getTrustedCertificate(); - if (!certificate.getPublicKey().getAlgorithm().equals(algorithm)) { - continue; - } - if (!(certificate instanceof X509Certificate)) { - continue; - } - X509Certificate x = (X509Certificate) certificate; - if (!x.getIssuerDN().equals(x.getSubjectDN())) { - continue; + public static X509Certificate rootCertificate(KeyStore keyStore, String algorithm) { + try { + X509Certificate found = null; + for (String alias : Collections.list(keyStore.aliases())) { + if (!keyStore.entryInstanceOf(alias, TrustedCertificateEntry.class)) { + continue; + } + TrustedCertificateEntry certificateEntry = + (TrustedCertificateEntry) keyStore.getEntry(alias, null); + Certificate certificate = certificateEntry.getTrustedCertificate(); + if (!certificate.getPublicKey().getAlgorithm().equals(algorithm)) { + continue; + } + if (!(certificate instanceof X509Certificate)) { + continue; + } + X509Certificate x = (X509Certificate) certificate; + if (!x.getIssuerDN().equals(x.getSubjectDN())) { + continue; + } + if (found != null) { + throw new IllegalStateException("KeyStore has more than one root CA for " + + algorithm + + "\nfirst: " + found + + "\nsecond: " + certificate ); + } + found = x; } - if (found != null) { - throw new IllegalStateException("KeyStore has more than one root CA for " - + algorithm - + "\nfirst: " + found - + "\nsecond: " + certificate ); + if (found == null) { + throw new IllegalStateException("KeyStore contained no root CA for " + algorithm); } - found = certificate; - } - if (found == null) { - throw new IllegalStateException("KeyStore contained no root CA for " + algorithm); + return found; + } catch (Exception e) { + throw new RuntimeException(e); } - return found; } /** * Create a client key store that only contains self-signed certificates but no private keys */ - public static KeyStore createClient(KeyStore caKeyStore) throws Exception { + public static KeyStore createClient(KeyStore caKeyStore) { KeyStore clientKeyStore = createKeyStore(); copySelfSignedCertificates(clientKeyStore, caKeyStore); return clientKeyStore; @@ -716,20 +762,24 @@ public final class TestKeyStore extends Assert { * Copy self-signed certificates from one key store to another. * Returns true if successful, false if no match found. */ - public static boolean copySelfSignedCertificates(KeyStore dst, KeyStore src) throws Exception { - boolean copied = false; - for (String alias : Collections.list(src.aliases())) { - if (!src.isCertificateEntry(alias)) { - continue; - } - X509Certificate cert = (X509Certificate)src.getCertificate(alias); - if (!cert.getSubjectDN().equals(cert.getIssuerDN())) { - continue; + public static boolean copySelfSignedCertificates(KeyStore dst, KeyStore src) { + try { + boolean copied = false; + for (String alias : Collections.list(src.aliases())) { + if (!src.isCertificateEntry(alias)) { + continue; + } + X509Certificate cert = (X509Certificate)src.getCertificate(alias); + if (!cert.getSubjectDN().equals(cert.getIssuerDN())) { + continue; + } + dst.setCertificateEntry(alias, cert); + copied = true; } - dst.setCertificateEntry(alias, cert); - copied = true; + return copied; + } catch (Exception e) { + throw new RuntimeException(e); } - return copied; } /** |