Search code examples
spring-bootjvmcertificatespring-webfluxapplepay

Apple Pay Merchant Validation with Spring Boot (Reactive)


We have a spring boot backend trying to perform merchant validation and retrieve a payment session for Apple Pay. Following these steps from Apple, we were able to generate a merchant validation certificate. When I try to create a Keystore programmatically and add the certificate issued by Apple for use in my web client call, I get the following error:

Caused by: javax.net.ssl.SSLHandshakeException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target

Here is the code snippet:

val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())

val certificateFactory = CertificateFactory.getInstance("X.509")

val keyStore: KeyStore = KeyStore.getInstance(KeyStore.getDefaultType())
keyStore.load(null)

val merchantValidationInputStream = FileInputStream(<PATH_TO_CERT_FROM_APPLE>)
val merchantValidationCert= certificateFactory.generateCertificate(merchantValidationInputStream)
keyStore.setCertificateEntry("applePayMerchantValidationCert", merchantValidationCert)

val sslContext = SslContextBuilder
                            .forClient()
                            .trustManager(trustManagerFactory)
                            .build()

val httpClient =   HttpClient.create()
                .secure { it.sslContext(sslContext) }

val webClient = builder
    .clientConnector(ReactorClientHttpConnector(httpClient))
    .baseUrl(<VALIDATION_URL_PASSED_FROM_WEB>)
    .build()

return webClient.post()
    .bodyValue(<REQUEST_BODY_WITH_MERCHANTID_DISPLAYNAME_INITIATIVE_INITIATIVE_CONTEXT>)
    .retrieve()
    .toBodilessEntity()
// For now I am just trying to get this call working and not doing anything with the response. 
// Using https://developer.apple.com/documentation/apple_pay_on_the_web/apple_pay_js_api/requesting_an_apple_pay_payment_session#3199965 
// as reference for the request body structure

Could I get some help figuring out what might be happening here?

Tools/Framework Versions

  • Spring Boot: 2.7.6
  • Kotlin: 1.7.22
  • Java: 17 (Adoptium)

Things I have tried:

  • Used keytool to import root and intermediary certs from Apple (adding them as trusted certs)
  • Tried converting the .cer file from Apple to a .pem file before adding it to the web client call

Solution

  • After working on this with @janani-subbiah we determined the following was the solution.

    For full instructions please visit the Apple Pay docs but I wanted to give a high level overview of the cert transitions we needed to make as well as the Spring Boot code to use it. Also note, you can do most of the cert changes with OpenSSL but I just used the Mac Keychain tool for simplicity.

    1. Create merchant id in Apple console
    2. Create CSR on Mac Keychain app with ECC and 256 bit key pair
    3. Create payment processing cert in Apple Console using the CSR from previous step (this is sent to our payment processor, not used in our code)
    4. Add all the frontend domains that will use this Apple Pay config in the Apple Console
    5. Download the Apple verification file for each domain and deploy it to the path Apple specifies on your frontend to complete verification
    6. Create CSR on Mac Keychain Access app with default settings
    7. Create the merchant identity cert in the Apple Console (this is the cert that needs to be sent via our backend for mutual TLS)
    8. Download that cert, open it in Keychain Access on the same Mac you used in Step 6 and go to your login keychain, select "My Certificates". You should see the merchant cert there and it can be expanded to see the private key. Select both the cert and private key then right click and select export 2 items. In the export make sure to select p12 format and set a secure password that will be used by the backend.
    9. Finally, in a Spring Boot app you need the following to use the cert.
    data class PaymentSessionRequest(
        val merchantIdentifier: String = "merchant.your merchant identifier",
        val displayName: String = "Any Apple Pay Display Name",
        val initiative: String = "web",
        val initiativeContext: String = "Fully qualified domain of the frontend"
    )
    
    val pass = "Password you set when exporting the p12 cert in step 8" // Should be set as a secret and exposed as an environment variable.
    val inputStream = javaClass.getResourceAsStream("/merchant_id.p12") // Should be mounted as a secret file and loaded from there.
    val keyStore = KeyStore.getInstance("PKCS12")
    keyStore.load(inputStream, pass.toCharArray())
    
    val keyManagerFactory = KeyManagerFactory.getInstance("SunX509")
    keyManagerFactory.init(keyStore, pass.toCharArray())
    
    val context = SslContextBuilder.forClient()
        .keyManager(keyManagerFactory)
        .build()
    
    val client: HttpClient = HttpClient.create().secure { spec -> spec.sslContext(context) }
    val connector: ClientHttpConnector = ReactorClientHttpConnector(client)
    
    val webClient = WebClient.builder()
        .baseUrl("https://apple-pay-gateway.apple.com") // Should use validation URL passed in from the Apple Pay JS SDK and include IP address validation based on Apple's docs
        .clientConnector(connector)
        .build()
    
    webClient.post()
        .uri("/paymentservices/paymentSession")
        .bodyValue(PaymentSessionRequest())
        .retrieve()
        .bodyToMono<String>()
        .doOnNext { logger.info("Apple Pay Session response: $it") }