From 6f4315ed4d70a8fa77be3fefa450cf5d30df1d54 Mon Sep 17 00:00:00 2001 From: Andreas Schildbach Date: Sun, 30 Mar 2014 11:53:25 +0200 Subject: [PATCH] Extract loading of X.509 trust stores to TrustStoreLoader. --- .../protocols/payments/PaymentSession.java | 101 +++------------- .../protocols/payments/TrustStoreLoader.java | 111 ++++++++++++++++++ .../bitcoin/protocols/payments/X509Utils.java | 23 ++++ 3 files changed, 149 insertions(+), 86 deletions(-) create mode 100644 core/src/main/java/com/google/bitcoin/protocols/payments/TrustStoreLoader.java diff --git a/core/src/main/java/com/google/bitcoin/protocols/payments/PaymentSession.java b/core/src/main/java/com/google/bitcoin/protocols/payments/PaymentSession.java index 53121307..ca2105c8 100644 --- a/core/src/main/java/com/google/bitcoin/protocols/payments/PaymentSession.java +++ b/core/src/main/java/com/google/bitcoin/protocols/payments/PaymentSession.java @@ -23,28 +23,20 @@ import com.google.bitcoin.script.ScriptBuilder; import com.google.bitcoin.uri.BitcoinURI; import com.google.bitcoin.utils.Threading; import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Joiner; import com.google.common.collect.Lists; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.protobuf.ByteString; import com.google.protobuf.InvalidProtocolBufferException; import org.bitcoin.protocols.payments.Protos; -import org.spongycastle.asn1.ASN1String; -import org.spongycastle.asn1.x500.AttributeTypeAndValue; -import org.spongycastle.asn1.x500.RDN; -import org.spongycastle.asn1.x500.X500Name; -import org.spongycastle.asn1.x500.style.RFC4519Style; import javax.annotation.Nullable; -import javax.security.auth.x500.X500Principal; import java.io.*; import java.math.BigInteger; import java.net.*; import java.security.*; import java.security.cert.*; import java.util.Date; -import java.util.Iterator; import java.util.List; import java.util.concurrent.Callable; @@ -80,7 +72,7 @@ import java.util.concurrent.Callable; public class PaymentSession { private static ListeningExecutorService executor = Threading.THREAD_POOL; private NetworkParameters params; - private String trustStorePath; + private final TrustStoreLoader trustStoreLoader; private Protos.PaymentRequest paymentRequest; private Protos.PaymentDetails paymentDetails; private BigInteger totalValue = BigInteger.ZERO; @@ -123,16 +115,15 @@ public class PaymentSession { * If verifyPki is specified and the payment request object specifies a PKI method, then the system trust store will * be used to verify the signature provided by the payment request. An exception is thrown by the future if the * signature cannot be verified. - * If trustStorePath is not null, the trust store used for PKI verification will be loaded from the given location - * instead of using the system default trust store location. + * If trustStoreLoader is null, the system default trust store is used. */ - public static ListenableFuture createFromBitcoinUri(final BitcoinURI uri, final boolean verifyPki, @Nullable final String trustStorePath) + public static ListenableFuture createFromBitcoinUri(final BitcoinURI uri, final boolean verifyPki, @Nullable final TrustStoreLoader trustStoreLoader) throws PaymentRequestException { String url = uri.getPaymentRequestUrl(); if (url == null) throw new PaymentRequestException.InvalidPaymentRequestURL("No payment request URL (r= parameter) in BitcoinURI " + uri); try { - return fetchPaymentRequest(new URI(url), verifyPki, trustStorePath); + return fetchPaymentRequest(new URI(url), verifyPki, trustStoreLoader); } catch (URISyntaxException e) { throw new PaymentRequestException.InvalidPaymentRequestURL(e); } @@ -167,21 +158,20 @@ public class PaymentSession { * If the payment request object specifies a PKI method, then the system trust store will * be used to verify the signature provided by the payment request. An exception is thrown by the future if the * signature cannot be verified. - * If trustStorePath is not null, the trust store used for PKI verification will be loaded from the given location - * instead of using the system default trust store location. + * If trustStoreLoader is null, the system default trust store is used. */ - public static ListenableFuture createFromUrl(final String url, final boolean verifyPki, @Nullable final String trustStorePath) + public static ListenableFuture createFromUrl(final String url, final boolean verifyPki, @Nullable final TrustStoreLoader trustStoreLoader) throws PaymentRequestException { if (url == null) throw new PaymentRequestException.InvalidPaymentRequestURL("null paymentRequestUrl"); try { - return fetchPaymentRequest(new URI(url), verifyPki, trustStorePath); + return fetchPaymentRequest(new URI(url), verifyPki, trustStoreLoader); } catch(URISyntaxException e) { throw new PaymentRequestException.InvalidPaymentRequestURL(e); } } - private static ListenableFuture fetchPaymentRequest(final URI uri, final boolean verifyPki, @Nullable final String trustStorePath) { + private static ListenableFuture fetchPaymentRequest(final URI uri, final boolean verifyPki, @Nullable final TrustStoreLoader trustStoreLoader) { return executor.submit(new Callable() { @Override public PaymentSession call() throws Exception { @@ -189,7 +179,7 @@ public class PaymentSession { connection.setRequestProperty("Accept", "application/bitcoin-paymentrequest"); connection.setUseCaches(false); Protos.PaymentRequest paymentRequest = Protos.PaymentRequest.parseFrom(connection.getInputStream()); - return new PaymentSession(paymentRequest, verifyPki, trustStorePath); + return new PaymentSession(paymentRequest, verifyPki, trustStoreLoader); } }); } @@ -199,8 +189,7 @@ public class PaymentSession { * Verifies PKI by default. */ public PaymentSession(Protos.PaymentRequest request) throws PaymentRequestException { - parsePaymentRequest(request); - verifyPki(); + this(request, true, null); } /** @@ -208,19 +197,16 @@ public class PaymentSession { * If verifyPki is true, also validates the signature and throws an exception if it fails. */ public PaymentSession(Protos.PaymentRequest request, boolean verifyPki) throws PaymentRequestException { - parsePaymentRequest(request); - if (verifyPki) - verifyPki(); + this(request, verifyPki, null); } /** * Creates a PaymentSession from the provided {@link Protos.PaymentRequest}. * If verifyPki is true, also validates the signature and throws an exception if it fails. - * If trustStorePath is not null, the trust store used for PKI verification will be loaded from the given location - * instead of using the system default trust store location. + * If trustStoreLoader is null, the system default trust store is used. */ - public PaymentSession(Protos.PaymentRequest request, boolean verifyPki, @Nullable final String trustStorePath) throws PaymentRequestException { - this.trustStorePath = trustStorePath; + public PaymentSession(Protos.PaymentRequest request, boolean verifyPki, @Nullable final TrustStoreLoader trustStoreLoader) throws PaymentRequestException { + this.trustStoreLoader = trustStoreLoader != null ? trustStoreLoader : new TrustStoreLoader.DefaultTrustStoreLoader(); parsePaymentRequest(request); if (verifyPki) verifyPki(); @@ -450,7 +436,7 @@ public class PaymentSession { CertPath path = certificateFactory.generateCertPath(certs); // Retrieves the most-trusted CAs from keystore. - PKIXParameters params = new PKIXParameters(createKeyStore(trustStorePath)); + PKIXParameters params = new PKIXParameters(trustStoreLoader.getKeyStore()); // Revocation not supported in the current version. params.setRevocationEnabled(false); @@ -508,63 +494,6 @@ public class PaymentSession { } } - private KeyStore createKeyStore(@Nullable String path) - throws IOException, KeyStoreException, NoSuchAlgorithmException, CertificateException { - String keyStoreType = KeyStore.getDefaultType(); - char[] defaultPassword = "changeit".toCharArray(); - if (path != null) { - // If the user provided path, only try to load the keystore at that path. - KeyStore keyStore = KeyStore.getInstance(keyStoreType); - FileInputStream is = new FileInputStream(path); - keyStore.load(is, defaultPassword); - return keyStore; - } - try { - // Check if we are on Android. - Class version = Class.forName("android.os.Build$VERSION"); - // Build.VERSION_CODES.ICE_CREAM_SANDWICH is 14. - if (version.getDeclaredField("SDK_INT").getInt(version) >= 14) { - // After ICS, Android provided this nice method for loading the keystore, - // so we don't have to specify the location explicitly. - KeyStore keystore = KeyStore.getInstance("AndroidCAStore"); - keystore.load(null, null); - return keystore; - } else { - keyStoreType = "BKS"; - path = System.getProperty("java.home") + "/etc/security/cacerts.bks".replace('/', File.separatorChar); - } - } catch (ClassNotFoundException e) { - // NOP. android.os.Build is not present, so we are not on Android. Fall through. - } catch (NoSuchFieldException e) { - throw new RuntimeException(e); // Should never happen. - } catch (IllegalAccessException e) { - throw new RuntimeException(e); // Should never happen. - } - if (path == null) { - path = System.getProperty("javax.net.ssl.trustStore"); - } - if (path == null) { - return loadFallbackStore(defaultPassword); - } - try { - KeyStore keyStore = KeyStore.getInstance(keyStoreType); - FileInputStream is = new FileInputStream(path); - keyStore.load(is, defaultPassword); - return keyStore; - } catch (FileNotFoundException e) { - // If we failed to find a system trust store, load our own fallback trust store. This can fail on Android - // but we should never reach it there. - return loadFallbackStore(defaultPassword); - } - } - - private KeyStore loadFallbackStore(char[] defaultPassword) throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException { - KeyStore keyStore = KeyStore.getInstance("JKS"); - InputStream is = getClass().getResourceAsStream("cacerts"); - keyStore.load(is, defaultPassword); - return keyStore; - } - private void parsePaymentRequest(Protos.PaymentRequest request) throws PaymentRequestException { try { if (request == null) diff --git a/core/src/main/java/com/google/bitcoin/protocols/payments/TrustStoreLoader.java b/core/src/main/java/com/google/bitcoin/protocols/payments/TrustStoreLoader.java new file mode 100644 index 00000000..34396a4b --- /dev/null +++ b/core/src/main/java/com/google/bitcoin/protocols/payments/TrustStoreLoader.java @@ -0,0 +1,111 @@ +/** + * Copyright 2014 Andreas Schildbach + * + * 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 com.google.bitcoin.protocols.payments; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.KeyStoreException; + +import javax.annotation.Nonnull; + +public interface TrustStoreLoader { + + KeyStore getKeyStore() throws FileNotFoundException, KeyStoreException; + + static final String DEFAULT_KEYSTORE_TYPE = KeyStore.getDefaultType(); + static final String DEFAULT_KEYSTORE_PASSWORD = "changeit"; + + public class DefaultTrustStoreLoader implements TrustStoreLoader { + + @Override + public KeyStore getKeyStore() throws FileNotFoundException, KeyStoreException { + + String keystorePath = null; + String keystoreType = DEFAULT_KEYSTORE_TYPE; + try { + // Check if we are on Android. + Class version = Class.forName("android.os.Build$VERSION"); + // Build.VERSION_CODES.ICE_CREAM_SANDWICH is 14. + if (version.getDeclaredField("SDK_INT").getInt(version) >= 14) { + return loadIcsKeyStore(); + } else { + keystoreType = "BKS"; + keystorePath = System.getProperty("java.home") + + "/etc/security/cacerts.bks".replace('/', File.separatorChar); + } + } catch (ClassNotFoundException e) { + // NOP. android.os.Build is not present, so we are not on Android. Fall through. + } catch (NoSuchFieldException e) { + throw new RuntimeException(e); // Should never happen. + } catch (IllegalAccessException e) { + throw new RuntimeException(e); // Should never happen. + } + if (keystorePath == null) { + keystorePath = System.getProperty("javax.net.ssl.trustStore"); + } + if (keystorePath == null) { + return loadFallbackStore(); + } + try { + return X509Utils.loadKeyStore(keystoreType, DEFAULT_KEYSTORE_PASSWORD, + new FileInputStream(keystorePath)); + } catch (FileNotFoundException e) { + // If we failed to find a system trust store, load our own fallback trust store. This can fail on + // Android but we should never reach it there. + return loadFallbackStore(); + } + } + + private KeyStore loadIcsKeyStore() throws KeyStoreException { + try { + // After ICS, Android provided this nice method for loading the keystore, + // so we don't have to specify the location explicitly. + KeyStore keystore = KeyStore.getInstance("AndroidCAStore"); + keystore.load(null, null); + return keystore; + } catch (IOException x) { + throw new KeyStoreException(x); + } catch (GeneralSecurityException x) { + throw new KeyStoreException(x); + } + } + + private KeyStore loadFallbackStore() throws FileNotFoundException, KeyStoreException { + return X509Utils.loadKeyStore("JKS", DEFAULT_KEYSTORE_PASSWORD, getClass().getResourceAsStream("cacerts")); + } + } + + public class FileTrustStoreLoader implements TrustStoreLoader { + + private final File path; + + public FileTrustStoreLoader(@Nonnull File path) throws FileNotFoundException { + if (!path.exists()) + throw new FileNotFoundException(path.toString()); + this.path = path; + } + + @Override + public KeyStore getKeyStore() throws FileNotFoundException, KeyStoreException { + return X509Utils.loadKeyStore(DEFAULT_KEYSTORE_TYPE, DEFAULT_KEYSTORE_PASSWORD, new FileInputStream(path)); + } + } +} diff --git a/core/src/main/java/com/google/bitcoin/protocols/payments/X509Utils.java b/core/src/main/java/com/google/bitcoin/protocols/payments/X509Utils.java index 13b86df4..943a409f 100644 --- a/core/src/main/java/com/google/bitcoin/protocols/payments/X509Utils.java +++ b/core/src/main/java/com/google/bitcoin/protocols/payments/X509Utils.java @@ -16,6 +16,11 @@ package com.google.bitcoin.protocols.payments; +import java.io.IOException; +import java.io.InputStream; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.KeyStoreException; import java.security.cert.CertificateParsingException; import java.security.cert.X509Certificate; import java.util.Collection; @@ -64,4 +69,22 @@ public class X509Utils { return altName; } } + + public static @Nonnull KeyStore loadKeyStore(@Nonnull String keystoreType, @Nullable String keystorePassword, @Nonnull InputStream is) + throws KeyStoreException { + try { + KeyStore keystore = KeyStore.getInstance(keystoreType); + keystore.load(is, keystorePassword != null ? keystorePassword.toCharArray() : null); + return keystore; + } catch (IOException x) { + throw new KeyStoreException(x); + } catch (GeneralSecurityException x) { + throw new KeyStoreException(x); + } finally { + try { + is.close(); + } catch (IOException x) { + } + } + } }