Validate StoreKit2 in-app purchase jwsRepresentation in backend (node ideally, but anything works)

How can I validate an in-app purchase JWS Representation from StoreKit2 on my backend in Node?

Its easy enough to decode the payload, but I can't find public keys that Apple uses to sign these JWS/JWTs anywhere. Any other time I've worked with JWTs, you simply used the node jsonwebtoken library and passed in the signers public key or shared secret key, either configured or fetched from a JWK.

I do see the docs about validating the certificate chain in the x5c field but am at a bit of a loss on how to verify that its from Apple. Anyone can create a JWT and sign one.

Thank you!

  • I can easily decode the JWS using node-jose j.JWS.createVerify().verify(jwsString, {allowEmbeddedKey: true}).then(r => obj = r) which gives me an object like:

    { protected: [ 'alg', 'x5c' ], header: { alg: 'ES256', x5c: [ 'MIIEMDueU3...', 'MII..., 'MIICQzCCAcmgAwIBAgIILcX8iNLFS5UwCgYIKoZIzj0EAwMwZzEbMBkGA1UEAwwSQXBwbGUgUm9vdCBDQSAtIEczMSYwJAYDVQQLDB1BcHBsZSBDZXJ0aWZpY2F0...' ] }, payload: <Buffer 7b 22 74 72 61 6e 73 61 63 74 69 6f 6e 49 64 22 3a 22 31 30 30 30 30 30 30 38 38 36 39 31 32 38 39 30 22 2c 22 6f 72 69 67 69 6e 61 6c 54 72 61 6e 73 ... 420 more bytes>, signature: <Buffer f8 85 65 79 a1 dc 74 dd 90 80 0a a4 08 85 30 e7 22 80 4c 20 66 09 0b 84 fc f4 e5 57 53 da d5 6f 13 c6 8f 56 e8 29 67 5c 95 a6 27 33 47 1e fe e9 6e 41 ... 14 more bytes>, key: JWKBaseKeyObject { keystore: JWKStore {}, length: 256, kty: 'EC', kid: 'Prod ECC Mac App Store and iTunes Store Receipt Signing', use: '', alg: '' } }

    And its easy to JSON.parse the payload and get the data I want. But, how can i verify that its authentic using the certificate chain in the x5c field

  • Ok, turns out until apple publishes there public keys, which evidently they have not yet done, there is no way to verify these JWS tokens.

Add a Comment

Accepted Reply

Hi, so the JWS contains a chain of certificates (x509) in its x5c header. The first certificate contains the public key used to verify the signature of the JWS. What we need is a way to verify that the certificates are also trustworthy (i.e. signed by Apple and/or a trusted CA). You can find these certificates on their site https://www.apple.com/certificateauthority/.

All you need to do now is download them and, whenever you receive a JWS, validate them against the ones found in the x5c header. The site has both the intermediate and the root certificates. Good luck!

  • Here's a Ruby solution using ruby-jwt (https://github.com/jwt/ruby-jwt). Hope it helps someone, as it was a challenge piecing this together from all of the clues. Or, if you see holes in this implementation please comment:

    `def good_signature?(jws_token) raw = File.read "/AppleRootCA-G3.cer" apple_root_cert = OpenSSL::X509::Certificate.new(raw)

    parts = jws_token.split(".") decoded_parts = parts.map { |part| Base64.decode64(part) } header = JSON.parse(decoded_parts[0])

    cert_chain = header["x5c"].map { |part| OpenSSL::X509::Certificate.new(Base64.decode64(part))} return false unless cert_chain.last == apple_root_cert

    for n in 0..(cert_chain.count - 2) return false unless cert_chain[n].verify(cert_chain[n+1].public_key) end

    begin decoded_token = JWT.decode(jws_token, cert_chain[0].public_key, true, { algorithms: ['ES256'] }) !decoded_token.nil? rescue JWT::JWKError false rescue JWT::DecodeError false end end``

Add a Comment

Replies

Hi, so the JWS contains a chain of certificates (x509) in its x5c header. The first certificate contains the public key used to verify the signature of the JWS. What we need is a way to verify that the certificates are also trustworthy (i.e. signed by Apple and/or a trusted CA). You can find these certificates on their site https://www.apple.com/certificateauthority/.

All you need to do now is download them and, whenever you receive a JWS, validate them against the ones found in the x5c header. The site has both the intermediate and the root certificates. Good luck!

  • Here's a Ruby solution using ruby-jwt (https://github.com/jwt/ruby-jwt). Hope it helps someone, as it was a challenge piecing this together from all of the clues. Or, if you see holes in this implementation please comment:

    `def good_signature?(jws_token) raw = File.read "/AppleRootCA-G3.cer" apple_root_cert = OpenSSL::X509::Certificate.new(raw)

    parts = jws_token.split(".") decoded_parts = parts.map { |part| Base64.decode64(part) } header = JSON.parse(decoded_parts[0])

    cert_chain = header["x5c"].map { |part| OpenSSL::X509::Certificate.new(Base64.decode64(part))} return false unless cert_chain.last == apple_root_cert

    for n in 0..(cert_chain.count - 2) return false unless cert_chain[n].verify(cert_chain[n+1].public_key) end

    begin decoded_token = JWT.decode(jws_token, cert_chain[0].public_key, true, { algorithms: ['ES256'] }) !decoded_token.nil? rescue JWT::JWKError false rescue JWT::DecodeError false end end``

Add a Comment
`def good_signature?(jws_token)
  raw = File.read "/Users/steve1/downloads/AppleRootCA-G3.cer"
  apple_root_cert = OpenSSL::X509::Certificate.new(raw)

  parts = jws_token.split(".")
  decoded_parts = parts.map { |part| Base64.decode64(part) }
  header = JSON.parse(decoded_parts[0])

  cert_chain =  header["x5c"].map { |part| OpenSSL::X509::Certificate.new(Base64.decode64(part))}
  return false unless cert_chain.last == apple_root_cert

  for n in 0..(cert_chain.count - 2)
    return false unless cert_chain[n].verify(cert_chain[n+1].public_key)
  end

  begin
    decoded_token = JWT.decode(jws_token, cert_chain[0].public_key, true, { algorithms: ['ES256'] })
    !decoded_token.nil?
  rescue JWT::JWKError
    false
  rescue JWT::DecodeError
    false
  end
end`