An Erlang library for verifying and decrypting Apple Pay payment tokens (EC_v1).
Implements the full Apple Pay Payment Token verification and decryption flow:
- PKCS#7 signature extraction and validation
- Leaf / intermediate certificate identification via Apple OIDs
- Certificate chain verification against the Apple Root CA
- CMS signing time and message digest checks (constant-time comparison)
- ECDH shared secret derivation + Apple-specific KDF
- AES-256-GCM decryption of the payment data
- Erlang/OTP 24+ (uses
crypto:crypto_one_time_aead/6) - rebar3
rebar3 compile%% Read your PEM-encoded merchant certificate, private key, and the Apple Root CA
{ok, RawCert} = file:read_file("merchant_id.pem"),
{ok, RawPrivateKey} = file:read_file("private_key.pem"),
{ok, RawAppleRoot} = file:read_file("AppleRootCA-G3.cer"),
%% Prepare a reusable environment (do this once at startup)
Env = erlang_apay_decrypt:prepare_env(RawCert, RawPrivateKey, RawAppleRoot, 180000),
%% Decode the token received from the client
APayMessage = jsx:decode(TokenJson, [return_maps]),
%% Verify and decrypt
case erlang_apay_decrypt:verify_and_decrypt_apay_message(APayMessage, [{env, Env}]) of
{ok, DecryptedJson} ->
%% DecryptedJson contains the payment data
io:format("~s~n", [DecryptedJson]);
{error, Reason} ->
io:format("Failed: ~p~n", [Reason])
end.raw_cert = File.read!("merchant_id.pem")
raw_private_key = File.read!("private_key.pem")
raw_apple_root = File.read!("AppleRootCA-G3.cer")
env = :erlang_apay_decrypt.prepare_env(raw_cert, raw_private_key, raw_apple_root, 180_000)
token = Jason.decode!(token_json)
case :erlang_apay_decrypt.verify_and_decrypt_apay_message(token, env: env) do
{:ok, decrypted} -> IO.puts(decrypted)
{:error, reason} -> IO.inspect(reason)
endSee the examples/apay_decrypt_ex directory for a full Elixir example project.
Prepares a reusable proplist containing the parsed merchant identity. Call once and pass the result via the env option.
| Parameter | Type | Description |
|---|---|---|
RawCert |
binary (PEM) | Your Apple Pay merchant identity certificate |
RawPrivateKey |
binary (PEM) | The EC private key associated with the certificate |
RawAppleRoot |
binary (PEM/DER) | Apple Root CA – G3 certificate |
Threshold |
integer | Maximum allowed age of the signing time in milliseconds (e.g. 180000 for 3 minutes) |
Verifies the signature and decrypts an Apple Pay payment token.
APayMessage is a map with binary keys as decoded from the JSON token (<<"data">>, <<"signature">>, <<"header">>, etc.).
Options (proplist):
| Option | Type | Description |
|---|---|---|
env |
proplist | Pre-built environment from prepare_env/4 |
raw_merch_cert |
binary | Merchant certificate PEM (alternative to env) |
raw_private_key |
binary | Private key PEM (alternative to env) |
raw_apple_root |
binary | Apple Root CA (alternative to env) |
threshold |
integer | Signing time threshold in ms (default 0) |
skip_chain_check |
boolean | Skip certificate chain validation (default false) |
check_time |
{{Y,M,D},{H,Mi,S}} |
Override the current time used for signing time verification |
Returns: decrypted payment data binary on success, or {error, Reason}.
Possible errors:
| Error | Meaning |
|---|---|
{unexpected_num_of_certs, N} |
Signature does not contain exactly 2 certificates |
no_required_certs |
Neither leaf nor intermediate certificate found |
no_leaf_cert |
Leaf certificate missing from signature |
no_intermediate_cert |
Intermediate certificate missing from signature |
bad_signature_issuer |
Signer does not match the leaf certificate |
bad_signing_time |
Signing time outside the allowed threshold |
bad_message_digest |
Computed message digest does not match the signed digest |
- Signature parsing — The base64-encoded PKCS#7 signature is decoded and the
ContentInfo/SignedDatastructure is extracted. - Certificate extraction — Leaf and intermediate certificates are identified by their Apple-specific OID extensions.
- Chain verification — The certificate chain
[Intermediate, Leaf]is validated against the Apple Root CA usingpublic_key:pkix_path_validation/3(can be skipped withskip_chain_check). - Signing time & digest — The CMS signing time is checked against the current time (± threshold), and the message digest over
EphemeralPublicKey || Data || TransactionId || ApplicationDatais verified. - Decryption — An ECDH shared secret is computed from the ephemeral public key and the merchant private key. A symmetric AES-256-GCM key is derived using the Apple-specific KDF, and the encrypted data is decrypted.
MIT — see LICENSE.