class PaymentSession {

    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  PaymentSession(Protos.PaymentRequest request);
    public  PaymentSession(Protos.PaymentRequest request, boolean verifyPki);
    public  PaymentSession(Protos.PaymentRequest request, boolean verifyPki, @Nullable final String trustStorePath);

}