/** * 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 * 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); } }