diff --git a/core/src/main/java/com/google/bitcoin/protocols/payments/PaymentProtocol.java b/core/src/main/java/com/google/bitcoin/protocols/payments/PaymentProtocol.java new file mode 100644 index 00000000..416b9471 --- /dev/null +++ b/core/src/main/java/com/google/bitcoin/protocols/payments/PaymentProtocol.java @@ -0,0 +1,171 @@ +/** + * Copyright 2013 Google Inc. + * 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.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.Signature; +import java.security.SignatureException; +import java.security.cert.CertPath; +import java.security.cert.CertPathValidator; +import java.security.cert.CertPathValidatorException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.CertificateParsingException; +import java.security.cert.PKIXCertPathValidatorResult; +import java.security.cert.PKIXParameters; +import java.security.cert.TrustAnchor; +import java.security.cert.X509Certificate; +import java.util.List; + +import javax.annotation.Nullable; + +import org.bitcoin.protocols.payments.Protos; + +import com.google.bitcoin.crypto.X509Utils; +import com.google.common.collect.Lists; +import com.google.protobuf.ByteString; +import com.google.protobuf.InvalidProtocolBufferException; + +public class PaymentProtocol { + + /** + * Uses the provided PKI method to find the corresponding public key and verify the provided signature. + * + * @param paymentRequest + * Payment request to verify. + * @param trustStore + * KeyStory of trusted root certificate authorities. + * @return verification data, or null if no PKI method was specified in the {@link Protos.PaymentRequest}. + * @throws PaymentRequestException + * if payment request could not be verified. + */ + public static @Nullable PkiVerificationData verifyPaymentRequestPki(Protos.PaymentRequest paymentRequest, KeyStore trustStore) + throws PaymentRequestException { + List certs = null; + try { + final String pkiType = paymentRequest.getPkiType(); + if (pkiType.equals("none")) + // Nothing to verify. Everything is fine. Move along. + return null; + + String algorithm; + if (pkiType.equals("x509+sha256")) + algorithm = "SHA256withRSA"; + else if (pkiType.equals("x509+sha1")) + algorithm = "SHA1withRSA"; + else + throw new PaymentRequestException.InvalidPkiType("Unsupported PKI type: " + pkiType); + + Protos.X509Certificates protoCerts = Protos.X509Certificates.parseFrom(paymentRequest.getPkiData()); + if (protoCerts.getCertificateCount() == 0) + throw new PaymentRequestException.InvalidPkiData("No certificates provided in message: server config error"); + + // Parse the certs and turn into a certificate chain object. Cert factories can parse both DER and base64. + // The ordering of certificates is defined by the payment protocol spec to be the same as what the Java + // crypto API requires - convenient! + CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); + certs = Lists.newArrayList(); + for (ByteString bytes : protoCerts.getCertificateList()) + certs.add((X509Certificate) certificateFactory.generateCertificate(bytes.newInput())); + CertPath path = certificateFactory.generateCertPath(certs); + + // Retrieves the most-trusted CAs from keystore. + PKIXParameters params = new PKIXParameters(trustStore); + // Revocation not supported in the current version. + params.setRevocationEnabled(false); + + // Now verify the certificate chain is correct and trusted. This let's us get an identity linked pubkey. + CertPathValidator validator = CertPathValidator.getInstance("PKIX"); + PKIXCertPathValidatorResult result = (PKIXCertPathValidatorResult) validator.validate(path, params); + PublicKey publicKey = result.getPublicKey(); + // OK, we got an identity, now check it was used to sign this message. + Signature signature = Signature.getInstance(algorithm); + // Note that we don't use signature.initVerify(certs.get(0)) here despite it being the most obvious + // way to set it up, because we don't care about the constraints specified on the certificates: any + // cert that links a key to a domain name or other identity will do for us. + signature.initVerify(publicKey); + Protos.PaymentRequest.Builder reqToCheck = paymentRequest.toBuilder(); + reqToCheck.setSignature(ByteString.EMPTY); + signature.update(reqToCheck.build().toByteArray()); + if (!signature.verify(paymentRequest.getSignature().toByteArray())) + throw new PaymentRequestException.PkiVerificationException("Invalid signature, this payment request is not valid."); + + // Signature verifies, get the names from the identity we just verified for presentation to the user. + final X509Certificate cert = certs.get(0); + String displayName = X509Utils.getDisplayNameFromCertificate(cert, true); + if (displayName == null) + throw new PaymentRequestException.PkiVerificationException("Could not extract name from certificate"); + // Everything is peachy. Return some useful data to the caller. + return new PkiVerificationData(displayName, publicKey, result.getTrustAnchor()); + } catch (InvalidProtocolBufferException e) { + // Data structures are malformed. + throw new PaymentRequestException.InvalidPkiData(e); + } catch (CertificateException e) { + // The X.509 certificate data didn't parse correctly. + throw new PaymentRequestException.PkiVerificationException(e); + } catch (NoSuchAlgorithmException e) { + // Should never happen so don't make users have to think about it. PKIX is always present. + throw new RuntimeException(e); + } catch (InvalidAlgorithmParameterException e) { + throw new RuntimeException(e); + } catch (CertPathValidatorException e) { + // The certificate chain isn't known or trusted, probably, the server is using an SSL root we don't + // know about and the user needs to upgrade to a new version of the software (or import a root cert). + throw new PaymentRequestException.PkiVerificationException(e, certs); + } catch (InvalidKeyException e) { + // Shouldn't happen if the certs verified correctly. + throw new PaymentRequestException.PkiVerificationException(e); + } catch (SignatureException e) { + // Something went wrong during hashing (yes, despite the name, this does not mean the sig was invalid). + throw new PaymentRequestException.PkiVerificationException(e); + } catch (KeyStoreException e) { + throw new RuntimeException(e); + } + } + + /** + * Information about the X509 signature's issuer and subject. + */ + public static class PkiVerificationData { + /** Display name of the payment requestor, could be a domain name, email address, legal name, etc */ + public final String displayName; + /** SSL public key that was used to sign. */ + public final PublicKey merchantSigningKey; + /** Object representing the CA that verified the merchant's ID */ + public final TrustAnchor rootAuthority; + /** String representing the display name of the CA that verified the merchant's ID */ + public final String rootAuthorityName; + + private PkiVerificationData(@Nullable String displayName, PublicKey merchantSigningKey, + TrustAnchor rootAuthority) throws PaymentRequestException.PkiVerificationException { + try { + this.displayName = displayName; + this.merchantSigningKey = merchantSigningKey; + this.rootAuthority = rootAuthority; + this.rootAuthorityName = X509Utils.getDisplayNameFromCertificate(rootAuthority.getTrustedCert(), true); + } catch (CertificateParsingException x) { + throw new PaymentRequestException.PkiVerificationException(x); + } + } + } +} 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 97c95e69..7270ef48 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 @@ -19,13 +19,12 @@ package com.google.bitcoin.protocols.payments; import com.google.bitcoin.core.*; import com.google.bitcoin.crypto.TrustStoreLoader; -import com.google.bitcoin.crypto.X509Utils; import com.google.bitcoin.params.MainNetParams; +import com.google.bitcoin.protocols.payments.PaymentProtocol.PkiVerificationData; 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.collect.Lists; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.protobuf.ByteString; @@ -36,8 +35,7 @@ import javax.annotation.Nullable; import java.io.*; import java.math.BigInteger; import java.net.*; -import java.security.*; -import java.security.cert.*; +import java.security.KeyStoreException; import java.util.Date; import java.util.List; import java.util.concurrent.Callable; @@ -83,7 +81,7 @@ public class PaymentSession { * Stores the calculated PKI verification data, or null if none is available. * Only valid after the session is created with verifyPki set to true, or verifyPki() is manually called. */ - public PkiVerificationData pkiVerificationData; + public final PkiVerificationData pkiVerificationData; /** * Returns a future that will be notified with a PaymentSession object after it is fetched using the provided uri. @@ -210,8 +208,17 @@ public class PaymentSession { 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(); + if (verifyPki) { + try { + pkiVerificationData = PaymentProtocol.verifyPaymentRequestPki(request, this.trustStoreLoader.getKeyStore()); + } catch (IOException x) { + throw new PaymentRequestException(x); + } catch (KeyStoreException x) { + throw new PaymentRequestException(x); + } + } else { + pkiVerificationData = null; + } } /** @@ -377,125 +384,6 @@ public class PaymentSession { }); } - /** - * Information about the X509 signature's issuer and subject. - */ - public static class PkiVerificationData { - /** Display name of the payment requestor, could be a domain name, email address, legal name, etc */ - public final String displayName; - /** SSL public key that was used to sign. */ - public final PublicKey merchantSigningKey; - /** Object representing the CA that verified the merchant's ID */ - public final TrustAnchor rootAuthority; - /** String representing the display name of the CA that verified the merchant's ID */ - public final String rootAuthorityName; - - private PkiVerificationData(@Nullable String displayName, PublicKey merchantSigningKey, - TrustAnchor rootAuthority) throws PaymentRequestException.PkiVerificationException { - try { - this.displayName = displayName; - this.merchantSigningKey = merchantSigningKey; - this.rootAuthority = rootAuthority; - this.rootAuthorityName = X509Utils.getDisplayNameFromCertificate(rootAuthority.getTrustedCert(), true); - } catch (CertificateParsingException x) { - throw new PaymentRequestException.PkiVerificationException(x); - } - } - } - - /** - * Uses the provided PKI method to find the corresponding public key and verify the provided signature. - * Returns null if no PKI method was specified in the {@link Protos.PaymentRequest}. - */ - public @Nullable PkiVerificationData verifyPki() throws PaymentRequestException { - List certs = null; - try { - if (pkiVerificationData != null) - return pkiVerificationData; - if (paymentRequest.getPkiType().equals("none")) - // Nothing to verify. Everything is fine. Move along. - return null; - - String algorithm; - if (paymentRequest.getPkiType().equals("x509+sha256")) - algorithm = "SHA256withRSA"; - else if (paymentRequest.getPkiType().equals("x509+sha1")) - algorithm = "SHA1withRSA"; - else - throw new PaymentRequestException.InvalidPkiType("Unsupported PKI type: " + paymentRequest.getPkiType()); - - Protos.X509Certificates protoCerts = Protos.X509Certificates.parseFrom(paymentRequest.getPkiData()); - if (protoCerts.getCertificateCount() == 0) - throw new PaymentRequestException.InvalidPkiData("No certificates provided in message: server config error"); - - // Parse the certs and turn into a certificate chain object. Cert factories can parse both DER and base64. - // The ordering of certificates is defined by the payment protocol spec to be the same as what the Java - // crypto API requires - convenient! - CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); - certs = Lists.newArrayList(); - for (ByteString bytes : protoCerts.getCertificateList()) - certs.add((X509Certificate) certificateFactory.generateCertificate(bytes.newInput())); - CertPath path = certificateFactory.generateCertPath(certs); - - // Retrieves the most-trusted CAs from keystore. - PKIXParameters params = new PKIXParameters(trustStoreLoader.getKeyStore()); - // Revocation not supported in the current version. - params.setRevocationEnabled(false); - - // Now verify the certificate chain is correct and trusted. This let's us get an identity linked pubkey. - CertPathValidator validator = CertPathValidator.getInstance("PKIX"); - PKIXCertPathValidatorResult result = (PKIXCertPathValidatorResult) validator.validate(path, params); - PublicKey publicKey = result.getPublicKey(); - // OK, we got an identity, now check it was used to sign this message. - Signature signature = Signature.getInstance(algorithm); - // Note that we don't use signature.initVerify(certs.get(0)) here despite it being the most obvious - // way to set it up, because we don't care about the constraints specified on the certificates: any - // cert that links a key to a domain name or other identity will do for us. - signature.initVerify(publicKey); - Protos.PaymentRequest.Builder reqToCheck = paymentRequest.toBuilder(); - reqToCheck.setSignature(ByteString.EMPTY); - signature.update(reqToCheck.build().toByteArray()); - if (!signature.verify(paymentRequest.getSignature().toByteArray())) - throw new PaymentRequestException.PkiVerificationException("Invalid signature, this payment request is not valid."); - - // Signature verifies, get the names from the identity we just verified for presentation to the user. - final X509Certificate cert = certs.get(0); - String displayName = X509Utils.getDisplayNameFromCertificate(cert, true); - if (displayName == null) - throw new PaymentRequestException.PkiVerificationException("Could not extract name from certificate"); - // Everything is peachy. Return some useful data to the caller. - PkiVerificationData data = new PkiVerificationData(displayName, publicKey, result.getTrustAnchor()); - // Cache the result so we don't have to re-verify if this method is called again. - pkiVerificationData = data; - return data; - } catch (InvalidProtocolBufferException e) { - // Data structures are malformed. - throw new PaymentRequestException.InvalidPkiData(e); - } catch (CertificateException e) { - // The X.509 certificate data didn't parse correctly. - throw new PaymentRequestException.PkiVerificationException(e); - } catch (NoSuchAlgorithmException e) { - // Should never happen so don't make users have to think about it. PKIX is always present. - throw new RuntimeException(e); - } catch (InvalidAlgorithmParameterException e) { - throw new RuntimeException(e); - } catch (CertPathValidatorException e) { - // The certificate chain isn't known or trusted, probably, the server is using an SSL root we don't - // know about and the user needs to upgrade to a new version of the software (or import a root cert). - throw new PaymentRequestException.PkiVerificationException(e, certs); - } catch (InvalidKeyException e) { - // Shouldn't happen if the certs verified correctly. - throw new PaymentRequestException.PkiVerificationException(e); - } catch (SignatureException e) { - // Something went wrong during hashing (yes, despite the name, this does not mean the sig was invalid). - throw new PaymentRequestException.PkiVerificationException(e); - } catch (IOException e) { - throw new PaymentRequestException.PkiVerificationException(e); - } catch (KeyStoreException e) { - throw new RuntimeException(e); - } - } - private void parsePaymentRequest(Protos.PaymentRequest request) throws PaymentRequestException { try { if (request == null) diff --git a/core/src/test/java/com/google/bitcoin/protocols/payments/PaymentSessionTest.java b/core/src/test/java/com/google/bitcoin/protocols/payments/PaymentSessionTest.java index e1b38a85..bebffb28 100644 --- a/core/src/test/java/com/google/bitcoin/protocols/payments/PaymentSessionTest.java +++ b/core/src/test/java/com/google/bitcoin/protocols/payments/PaymentSessionTest.java @@ -18,6 +18,7 @@ package com.google.bitcoin.protocols.payments; import com.google.bitcoin.core.*; +import com.google.bitcoin.crypto.TrustStoreLoader; import com.google.bitcoin.params.TestNet3Params; import com.google.common.util.concurrent.ListenableFuture; import com.google.protobuf.ByteString; @@ -123,8 +124,8 @@ public class PaymentSessionTest { public void testPkiVerification() throws Exception { InputStream in = getClass().getResourceAsStream("pki_test.bitcoinpaymentrequest"); Protos.PaymentRequest paymentRequest = Protos.PaymentRequest.newBuilder().mergeFrom(in).build(); - MockPaymentSession paymentSession = new MockPaymentSession(paymentRequest); - PaymentSession.PkiVerificationData pkiData = paymentSession.verifyPki(); + PaymentProtocol.PkiVerificationData pkiData = PaymentProtocol.verifyPaymentRequestPki(paymentRequest, + new TrustStoreLoader.DefaultTrustStoreLoader().getKeyStore()); assertEquals("www.bitcoincore.org", pkiData.displayName); assertEquals("The USERTRUST Network, Salt Lake City, US", pkiData.rootAuthorityName); } diff --git a/tools/src/main/java/com/google/bitcoin/tools/PaymentProtocolTool.java b/tools/src/main/java/com/google/bitcoin/tools/PaymentProtocolTool.java index ed8e118d..0f806463 100644 --- a/tools/src/main/java/com/google/bitcoin/tools/PaymentProtocolTool.java +++ b/tools/src/main/java/com/google/bitcoin/tools/PaymentProtocolTool.java @@ -16,6 +16,8 @@ package com.google.bitcoin.tools; +import com.google.bitcoin.crypto.TrustStoreLoader; +import com.google.bitcoin.protocols.payments.PaymentProtocol; import com.google.bitcoin.protocols.payments.PaymentRequestException; import com.google.bitcoin.protocols.payments.PaymentSession; import com.google.bitcoin.uri.BitcoinURI; @@ -27,6 +29,7 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; +import java.security.KeyStoreException; import java.security.cert.X509Certificate; import java.util.Date; import java.util.concurrent.ExecutionException; @@ -69,7 +72,8 @@ public class PaymentProtocolTool { final int version = session.getPaymentRequest().getPaymentDetailsVersion(); StringBuilder output = new StringBuilder( format("Bitcoin payment request, version %d%nDate: %s%n", version, session.getDate())); - PaymentSession.PkiVerificationData pki = session.verifyPki(); + PaymentProtocol.PkiVerificationData pki = PaymentProtocol.verifyPaymentRequestPki( + session.getPaymentRequest(), new TrustStoreLoader.DefaultTrustStoreLoader().getKeyStore()); if (pki != null) { output.append(format("Signed by: %s%nIdentity verified by: %s%n", pki.displayName, pki.rootAuthorityName)); } @@ -103,6 +107,8 @@ public class PaymentProtocolTool { System.err.println(e.getMessage()); } catch (IOException e) { e.printStackTrace(); + } catch (KeyStoreException e) { + e.printStackTrace(); } } }