org.bitcoinj.protocols.payments.PaymentProtocol Maven / Gradle / Ivy
/*
* 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 org.bitcoinj.protocols.payments;
import org.bitcoinj.core.*;
import org.bitcoinj.crypto.X509Utils;
import org.bitcoinj.script.ScriptBuilder;
import com.google.common.base.MoreObjects;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import org.bitcoin.protocols.payments.Protos;
import javax.annotation.Nullable;
import java.io.Serializable;
import java.security.*;
import java.security.cert.*;
import java.security.cert.Certificate;
import java.util.ArrayList;
import java.util.List;
/**
* Utility methods and constants for working with
* BIP 70 aka the payment protocol. These are low level wrappers around the protocol buffers. If you're implementing
* a wallet app, look at {@link PaymentSession} for a higher level API that should simplify working with the protocol.
*
* BIP 70 defines a binary, protobuf based protocol that runs directly between sender and receiver of funds. Payment
* protocol data does not flow over the Bitcoin P2P network or enter the block chain. It's instead for data that is only
* of interest to the parties involved but isn't otherwise needed for consensus.
*/
public class PaymentProtocol {
// MIME types as defined in BIP71.
public static final String MIMETYPE_PAYMENTREQUEST = "application/bitcoin-paymentrequest";
public static final String MIMETYPE_PAYMENT = "application/bitcoin-payment";
public static final String MIMETYPE_PAYMENTACK = "application/bitcoin-paymentack";
/**
* Create a payment request with one standard pay to address output. You may want to sign the request using
* {@link #signPaymentRequest}. Use {@link org.bitcoin.protocols.payments.Protos.PaymentRequest.Builder#build} to get the actual payment
* request.
*
* @param params network parameters
* @param amount amount of coins to request, or null
* @param toAddress address to request coins to
* @param memo arbitrary, user readable memo, or null if none
* @param paymentUrl URL to send payment message to, or null if none
* @param merchantData arbitrary merchant data, or null if none
* @return created payment request, in its builder form
*/
public static Protos.PaymentRequest.Builder createPaymentRequest(NetworkParameters params,
@Nullable Coin amount, Address toAddress, @Nullable String memo, @Nullable String paymentUrl,
@Nullable byte[] merchantData) {
return createPaymentRequest(params, ImmutableList.of(createPayToAddressOutput(amount, toAddress)), memo,
paymentUrl, merchantData);
}
/**
* Create a payment request. You may want to sign the request using {@link #signPaymentRequest}. Use
* {@link org.bitcoin.protocols.payments.Protos.PaymentRequest.Builder#build} to get the actual payment request.
*
* @param params network parameters
* @param outputs list of outputs to request coins to
* @param memo arbitrary, user readable memo, or null if none
* @param paymentUrl URL to send payment message to, or null if none
* @param merchantData arbitrary merchant data, or null if none
* @return created payment request, in its builder form
*/
public static Protos.PaymentRequest.Builder createPaymentRequest(NetworkParameters params,
List outputs, @Nullable String memo, @Nullable String paymentUrl,
@Nullable byte[] merchantData) {
final Protos.PaymentDetails.Builder paymentDetails = Protos.PaymentDetails.newBuilder();
paymentDetails.setNetwork(params.getPaymentProtocolId());
for (Protos.Output output : outputs)
paymentDetails.addOutputs(output);
if (memo != null)
paymentDetails.setMemo(memo);
if (paymentUrl != null)
paymentDetails.setPaymentUrl(paymentUrl);
if (merchantData != null)
paymentDetails.setMerchantData(ByteString.copyFrom(merchantData));
paymentDetails.setTime(Utils.currentTimeSeconds());
final Protos.PaymentRequest.Builder paymentRequest = Protos.PaymentRequest.newBuilder();
paymentRequest.setSerializedPaymentDetails(paymentDetails.build().toByteString());
return paymentRequest;
}
/**
* Parse a payment request.
*
* @param paymentRequest payment request to parse
* @return instance of {@link PaymentSession}, used as a value object
* @throws PaymentProtocolException
*/
public static PaymentSession parsePaymentRequest(Protos.PaymentRequest paymentRequest)
throws PaymentProtocolException {
return new PaymentSession(paymentRequest, false, null);
}
/**
* Sign the provided payment request.
*
* @param paymentRequest Payment request to sign, in its builder form.
* @param certificateChain Certificate chain to send with the payment request, ordered from client certificate to root
* certificate. The root certificate itself may be omitted.
* @param privateKey The key to sign with. Must match the public key from the first certificate of the certificate chain.
*/
public static void signPaymentRequest(Protos.PaymentRequest.Builder paymentRequest,
X509Certificate[] certificateChain, PrivateKey privateKey) {
try {
final Protos.X509Certificates.Builder certificates = Protos.X509Certificates.newBuilder();
for (final Certificate certificate : certificateChain)
certificates.addCertificate(ByteString.copyFrom(certificate.getEncoded()));
paymentRequest.setPkiType("x509+sha256");
paymentRequest.setPkiData(certificates.build().toByteString());
paymentRequest.setSignature(ByteString.EMPTY);
final Protos.PaymentRequest paymentRequestToSign = paymentRequest.build();
final String algorithm;
if ("RSA".equalsIgnoreCase(privateKey.getAlgorithm()))
algorithm = "SHA256withRSA";
else
throw new IllegalStateException(privateKey.getAlgorithm());
final Signature signature = Signature.getInstance(algorithm);
signature.initSign(privateKey);
signature.update(paymentRequestToSign.toByteArray());
paymentRequest.setSignature(ByteString.copyFrom(signature.sign()));
} catch (final GeneralSecurityException x) {
// Should never happen so don't make users have to think about it.
throw new RuntimeException(x);
}
}
/**
* Uses the provided PKI method to find the corresponding public key and verify the provided signature.
*
* @param paymentRequest Payment request to verify.
* @param trustStore KeyStore of trusted root certificate authorities.
* @return verification data, or null if no PKI method was specified in the {@link org.bitcoin.protocols.payments.Protos.PaymentRequest}.
* @throws PaymentProtocolException if payment request could not be verified.
*/
@Nullable
public static PkiVerificationData verifyPaymentRequestPki(Protos.PaymentRequest paymentRequest, KeyStore trustStore)
throws PaymentProtocolException {
List certs = null;
try {
final String pkiType = paymentRequest.getPkiType();
if ("none".equals(pkiType))
// Nothing to verify. Everything is fine. Move along.
return null;
String algorithm;
if ("x509+sha256".equals(pkiType))
algorithm = "SHA256withRSA";
else if ("x509+sha1".equals(pkiType))
algorithm = "SHA1withRSA";
else
throw new PaymentProtocolException.InvalidPkiType("Unsupported PKI type: " + pkiType);
Protos.X509Certificates protoCerts = Protos.X509Certificates.parseFrom(paymentRequest.getPkiData());
if (protoCerts.getCertificateCount() == 0)
throw new PaymentProtocolException.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 PaymentProtocolException.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 PaymentProtocolException.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 PaymentProtocolException.InvalidPkiData(e);
} catch (CertificateException e) {
// The X.509 certificate data didn't parse correctly.
throw new PaymentProtocolException.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 PaymentProtocolException.PkiVerificationException(e, certs);
} catch (InvalidKeyException e) {
// Shouldn't happen if the certs verified correctly.
throw new PaymentProtocolException.PkiVerificationException(e);
} catch (SignatureException e) {
// Something went wrong during hashing (yes, despite the name, this does not mean the sig was invalid).
throw new PaymentProtocolException.PkiVerificationException(e);
} catch (KeyStoreException e) {
throw new RuntimeException(e);
}
}
/**
* Information about the X.509 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 PaymentProtocolException.PkiVerificationException {
try {
this.displayName = displayName;
this.merchantSigningKey = merchantSigningKey;
this.rootAuthority = rootAuthority;
this.rootAuthorityName = X509Utils.getDisplayNameFromCertificate(rootAuthority.getTrustedCert(), true);
} catch (CertificateParsingException x) {
throw new PaymentProtocolException.PkiVerificationException(x);
}
}
@Override
public String toString() {
return MoreObjects.toStringHelper(this)
.add("displayName", displayName)
.add("rootAuthorityName", rootAuthorityName)
.add("merchantSigningKey", merchantSigningKey)
.add("rootAuthority", rootAuthority)
.toString();
}
}
/**
* Create a payment message with one standard pay to address output.
*
* @param transactions one or more transactions that satisfy the requested outputs.
* @param refundAmount amount of coins to request as a refund, or null if no refund.
* @param refundAddress address to refund coins to
* @param memo arbitrary, user readable memo, or null if none
* @param merchantData arbitrary merchant data, or null if none
* @return created payment message
*/
public static Protos.Payment createPaymentMessage(List transactions,
@Nullable Coin refundAmount, @Nullable Address refundAddress, @Nullable String memo,
@Nullable byte[] merchantData) {
if (refundAddress != null) {
if (refundAmount == null)
throw new IllegalArgumentException("Specify refund amount if refund address is specified.");
return createPaymentMessage(transactions,
ImmutableList.of(createPayToAddressOutput(refundAmount, refundAddress)), memo, merchantData);
} else {
return createPaymentMessage(transactions, null, memo, merchantData);
}
}
/**
* Create a payment message. This wraps up transaction data along with anything else useful for making a payment.
*
* @param transactions transactions to include with the payment message
* @param refundOutputs list of outputs to refund coins to, or null
* @param memo arbitrary, user readable memo, or null if none
* @param merchantData arbitrary merchant data, or null if none
* @return created payment message
*/
public static Protos.Payment createPaymentMessage(List transactions,
@Nullable List refundOutputs, @Nullable String memo, @Nullable byte[] merchantData) {
Protos.Payment.Builder builder = Protos.Payment.newBuilder();
for (Transaction transaction : transactions) {
transaction.verify();
builder.addTransactions(ByteString.copyFrom(transaction.unsafeBitcoinSerialize()));
}
if (refundOutputs != null) {
for (Protos.Output output : refundOutputs)
builder.addRefundTo(output);
}
if (memo != null)
builder.setMemo(memo);
if (merchantData != null)
builder.setMerchantData(ByteString.copyFrom(merchantData));
return builder.build();
}
/**
* Parse transactions from payment message.
*
* @param params network parameters (needed for transaction deserialization)
* @param paymentMessage payment message to parse
* @return list of transactions
*/
public static List parseTransactionsFromPaymentMessage(NetworkParameters params,
Protos.Payment paymentMessage) {
final List transactions = new ArrayList<>(paymentMessage.getTransactionsCount());
for (final ByteString transaction : paymentMessage.getTransactionsList())
transactions.add(params.getDefaultSerializer().makeTransaction(transaction.toByteArray()));
return transactions;
}
/**
* Message returned by the merchant in response to a Payment message.
*/
public static class Ack {
@Nullable private final String memo;
Ack(@Nullable String memo) {
this.memo = memo;
}
/**
* Returns the memo included by the merchant in the payment ack. This message is typically displayed to the user
* as a notification (e.g. "Your payment was received and is being processed"). If none was provided, returns
* null.
*/
@Nullable public String getMemo() {
return memo;
}
}
/**
* Create a payment ack.
*
* @param paymentMessage payment message to send with the ack
* @param memo arbitrary, user readable memo, or null if none
* @return created payment ack
*/
public static Protos.PaymentACK createPaymentAck(Protos.Payment paymentMessage, @Nullable String memo) {
final Protos.PaymentACK.Builder builder = Protos.PaymentACK.newBuilder();
builder.setPayment(paymentMessage);
if (memo != null)
builder.setMemo(memo);
return builder.build();
}
/**
* Parse payment ack into an object.
*/
public static Ack parsePaymentAck(Protos.PaymentACK paymentAck) {
final String memo = paymentAck.hasMemo() ? paymentAck.getMemo() : null;
return new Ack(memo);
}
/**
* Create a standard pay to address output for usage in {@link #createPaymentRequest} and
* {@link #createPaymentMessage}.
*
* @param amount amount to pay, or null
* @param address address to pay to
* @return output
*/
public static Protos.Output createPayToAddressOutput(@Nullable Coin amount, Address address) {
Protos.Output.Builder output = Protos.Output.newBuilder();
if (amount != null) {
final NetworkParameters params = address.getParameters();
if (params.hasMaxMoney() && amount.compareTo(params.getMaxMoney()) > 0)
throw new IllegalArgumentException("Amount too big: " + amount);
output.setAmount(amount.value);
} else {
output.setAmount(0);
}
output.setScript(ByteString.copyFrom(ScriptBuilder.createOutputScript(address).getProgram()));
return output.build();
}
/**
* Value object to hold amount/script pairs.
*/
public static class Output implements Serializable {
@Nullable public final Coin amount;
public final byte[] scriptData;
public Output(@Nullable Coin amount, byte[] scriptData) {
this.amount = amount;
this.scriptData = scriptData;
}
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy