/** * 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); } }
private void parsePaymentRequest(Protos.PaymentRequest request) throws PaymentRequestException { try { if (request == null) throw new PaymentRequestException("request cannot be null"); if (request.getPaymentDetailsVersion() != 1) throw new PaymentRequestException.InvalidVersion( "Version 1 required. Received version " + request.getPaymentDetailsVersion()); paymentRequest = request; if (!request.hasSerializedPaymentDetails()) throw new PaymentRequestException("No PaymentDetails"); paymentDetails = Protos.PaymentDetails.newBuilder() .mergeFrom(request.getSerializedPaymentDetails()) .build(); if (paymentDetails == null) throw new PaymentRequestException("Invalid PaymentDetails"); if (!paymentDetails.hasNetwork()) params = MainNetParams.get(); else params = NetworkParameters.fromPmtProtocolID(paymentDetails.getNetwork()); if (params == null) throw new PaymentRequestException.InvalidNetwork( "Invalid network " + paymentDetails.getNetwork()); if (paymentDetails.getOutputsCount() < 1) throw new PaymentRequestException.InvalidOutputs("No outputs"); for (Protos.Output output : paymentDetails.getOutputsList()) { if (output.hasAmount()) totalValue = totalValue.add(BigInteger.valueOf(output.getAmount())); } // This won't ever happen in practice. It would only happen if the user provided outputs // that are obviously invalid. Still, we don't want to silently overflow. if (totalValue.compareTo(NetworkParameters.MAX_MONEY) > 0) throw new PaymentRequestException.InvalidOutputs("The outputs are way too big."); } catch (InvalidProtocolBufferException e) { throw new PaymentRequestException(e); } }
private Protos.PaymentRequest minimalPaymentRequest() { Protos.PaymentDetails.Builder paymentDetails = Protos.PaymentDetails.newBuilder(); paymentDetails.setTime(System.currentTimeMillis()); Protos.PaymentRequest.Builder paymentRequest = Protos.PaymentRequest.newBuilder(); paymentRequest.setSerializedPaymentDetails(paymentDetails.build().toByteString()); return paymentRequest.build(); }
private static void sendPaymentRequest(String location, boolean verifyPki) { if (location.startsWith("http") || location.startsWith("defcoin")) { try { ListenableFuture<PaymentSession> future; if (location.startsWith("http")) { future = PaymentSession.createFromUrl(location, verifyPki); } else { BitcoinURI paymentRequestURI = new BitcoinURI(location); future = PaymentSession.createFromBitcoinUri(paymentRequestURI, verifyPki); } PaymentSession session = future.get(); if (session != null) { send(session); } else { System.err.println("Server returned null session"); System.exit(1); } } catch (PaymentRequestException e) { System.err.println("Error creating payment session " + e.getMessage()); System.exit(1); } catch (BitcoinURIParseException e) { System.err.println("Invalid defcoin uri: " + e.getMessage()); System.exit(1); } catch (InterruptedException e) { // Ignore. } catch (ExecutionException e) { throw new RuntimeException(e); } } else { // Try to open the payment request as a file. FileInputStream stream = null; try { File paymentRequestFile = new File(location); stream = new FileInputStream(paymentRequestFile); } catch (Exception e) { System.err.println("Failed to open file: " + e.getMessage()); System.exit(1); } try { paymentRequest = org.bitcoin.protocols.payments.Protos.PaymentRequest.newBuilder() .mergeFrom(stream) .build(); } catch (IOException e) { System.err.println("Failed to parse payment request from file " + e.getMessage()); System.exit(1); } PaymentSession session = null; try { session = new PaymentSession(paymentRequest, verifyPki); } catch (PaymentRequestException e) { System.err.println("Error creating payment session " + e.getMessage()); System.exit(1); } send(session); } }
@Test 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(); assertEquals("www.bitcoincore.org", pkiData.name); assertEquals("The USERTRUST Network, Salt Lake City, US", pkiData.rootAuthorityName); }
@Test public void testDefaults() throws Exception { Protos.Output.Builder outputBuilder = Protos.Output.newBuilder().setScript(ByteString.copyFrom(outputToMe.getScriptBytes())); Protos.PaymentDetails paymentDetails = Protos.PaymentDetails.newBuilder().setTime(time).addOutputs(outputBuilder).build(); Protos.PaymentRequest paymentRequest = Protos.PaymentRequest.newBuilder() .setSerializedPaymentDetails(paymentDetails.toByteString()) .build(); MockPaymentSession paymentSession = new MockPaymentSession(paymentRequest); assertEquals(BigInteger.ZERO, paymentSession.getValue()); assertNull(paymentSession.getPaymentUrl()); assertNull(paymentSession.getMemo()); }
/** * Create a payment request. You may want to sign the request using {@link #signPaymentRequest}. * Use {@link 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<Protos.Output> 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; }
private Protos.PaymentRequest newSimplePaymentRequest() { Protos.Output.Builder outputBuilder = Protos.Output.newBuilder() .setAmount(nanoCoins.longValue()) .setScript(ByteString.copyFrom(outputToMe.getScriptBytes())); Protos.PaymentDetails paymentDetails = Protos.PaymentDetails.newBuilder() .setNetwork("test") .setTime(time) .setPaymentUrl(simplePaymentUrl) .addOutputs(outputBuilder) .setMemo(paymentRequestMemo) .setMerchantData(merchantData) .build(); Protos.PaymentRequest paymentRequest = Protos.PaymentRequest.newBuilder() .setPaymentDetailsVersion(1) .setPkiType("none") .setSerializedPaymentDetails(paymentDetails.toByteString()) .build(); return paymentRequest; }
/** * 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 * Protos.PaymentRequest}. * @throws PaymentProtocolException if payment request could not be verified. */ @Nullable public static PkiVerificationData verifyPaymentRequestPki( Protos.PaymentRequest paymentRequest, KeyStore trustStore) throws PaymentProtocolException { List<X509Certificate> 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); } }
/** * 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 { 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"); List<X509Certificate> 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(createKeyStore(trustStorePath)); // 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. X500Principal principal = certs.get(0).getSubjectX500Principal(); // At this point the Java crypto API falls flat on its face and dies - there's no clean way to // get the // different parts of the certificate name except for parsing the string. That's hard because // of various // custom escaping rules and the usual crap. So, use Bouncy Castle to re-parse the string into // binary form // again and then look for the names we want. Fail! org.spongycastle.asn1.x500.X500Name name = new X500Name(principal.getName()); String entityName = null, orgName = null; for (RDN rdn : name.getRDNs()) { AttributeTypeAndValue pair = rdn.getFirst(); if (pair.getType().equals(RFC4519Style.cn)) entityName = ((ASN1String) pair.getValue()).getString(); else if (pair.getType().equals(RFC4519Style.o)) orgName = ((ASN1String) pair.getValue()).getString(); } if (entityName == null && orgName == null) throw new PaymentRequestException.PkiVerificationException( "Invalid certificate, no CN or O fields"); // Everything is peachy. Return some useful data to the caller. PkiVerificationData data = new PkiVerificationData(entityName, orgName, 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); } 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); } }
public static PaymentIntent parsePaymentRequest(@Nonnull final byte[] serializedPaymentRequest) throws PaymentProtocolException { try { if (serializedPaymentRequest.length > 50000) throw new PaymentProtocolException( "payment request too big: " + serializedPaymentRequest.length); final Protos.PaymentRequest paymentRequest = Protos.PaymentRequest.parseFrom(serializedPaymentRequest); final String pkiName; final String pkiCaName; if (!"none".equals(paymentRequest.getPkiType())) { final KeyStore keystore = new TrustStoreLoader.DefaultTrustStoreLoader().getKeyStore(); final PkiVerificationData verificationData = PaymentProtocol.verifyPaymentRequestPki(paymentRequest, keystore); pkiName = verificationData.displayName; pkiCaName = verificationData.rootAuthorityName; } else { pkiName = null; pkiCaName = null; } final PaymentSession paymentSession = PaymentProtocol.parsePaymentRequest(paymentRequest); if (paymentSession.isExpired()) throw new PaymentProtocolException.Expired( "payment details expired: current time " + new Date() + " after expiry time " + paymentSession.getExpires()); if (!paymentSession.getNetworkParameters().equals(Constants.NETWORK_PARAMETERS)) throw new PaymentProtocolException.InvalidNetwork( "cannot handle payment request network: " + paymentSession.getNetworkParameters()); final ArrayList<PaymentIntent.Output> outputs = new ArrayList<PaymentIntent.Output>(1); for (final PaymentProtocol.Output output : paymentSession.getOutputs()) outputs.add(PaymentIntent.Output.valueOf(output)); final String memo = paymentSession.getMemo(); final String paymentUrl = paymentSession.getPaymentUrl(); final byte[] merchantData = paymentSession.getMerchantData(); final byte[] paymentRequestHash = Hashing.sha256().hashBytes(serializedPaymentRequest).asBytes(); final PaymentIntent paymentIntent = new PaymentIntent( PaymentIntent.Standard.BIP70, pkiName, pkiCaName, outputs.toArray(new PaymentIntent.Output[0]), memo, paymentUrl, merchantData, null, paymentRequestHash); if (paymentIntent.hasPaymentUrl() && !paymentIntent.isSupportedPaymentUrl()) throw new PaymentProtocolException.InvalidPaymentURL( "cannot handle payment url: " + paymentIntent.paymentUrl); return paymentIntent; } catch (final InvalidProtocolBufferException x) { throw new PaymentProtocolException(x); } catch (final UninitializedMessageException x) { throw new PaymentProtocolException(x); } catch (final FileNotFoundException x) { throw new RuntimeException(x); } catch (final KeyStoreException x) { throw new RuntimeException(x); } }