mirror of
https://github.com/Qortal/altcoinj.git
synced 2025-02-14 11:15:51 +00:00
Move verification of payment requests into new PaymentProtocol class.
This commit is contained in:
parent
dfc5104d31
commit
4502c40e59
@ -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<X509Certificate> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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<X509Certificate> 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)
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user