Loyalty app development question: Apple Wallet / Google Wallet pass generation — signing workflow keeps failing #202798
Replies: 3 comments
|
💬 Your Product Feedback Has Been Submitted 🎉 Thank you for taking the time to share your insights with us! Your feedback is invaluable as we build a better GitHub experience for all our users. Here's what you can expect moving forward ⏩
Where to look to see what's shipping 👀
What you can do in the meantime 💻
As a member of the GitHub community, your participation is essential. While we can't promise that every suggestion will be implemented, we want to emphasize that your feedback is instrumental in guiding our decisions and priorities. Thank you once again for your contribution to making GitHub even better! We're grateful for your ongoing support and collaboration in shaping the future of our platform. ⭐ |
|
Thanks for posting in the GitHub Community, @Snehashri52! You are more likely to get a useful response if you are posting your question in the applicable category, the Discussions category is solely related to conversations around the GitHub product Discussions. This question should be in the "Programming Help" category. I've gone ahead and moved it for you. Good luck! |
|
There are two separate problems here, and both can be diagnosed without regenerating everything blindly. Apple WalletFor Pass Type ID certificates, use the current WWDR G4 intermediate. G6 is for other certificate types. A correct signing chain is:
First confirm that the private key actually matches the leaf certificate: openssl x509 -in pass-cert.pem -pubkey -noout | openssl pkey -pubin -outform DER | openssl sha256
openssl pkey -in pass-key.pem -pubout -outform DER | openssl sha256The two hashes must be identical. Then inspect and verify the chain: openssl x509 -in pass-cert.pem -noout -issuer -subject -text | grep -A2 "Authority Key Identifier"
openssl verify -CAfile AppleRootCA-G3.pem -untrusted AppleWWDRCAG4.pem pass-cert.pemAlso unzip the generated unzip -p pass.pkpass signature > signature.der
openssl pkcs7 -inform DER -in signature.der -print_certs -nooutCommon causes are a missing G4 intermediate, a private-key mismatch, a Google WalletThe JWT shown is missing required fields. For a server-generated Add to Google Wallet link, include {
"iss": "SERVICE_ACCOUNT_EMAIL",
"aud": "google",
"typ": "savetowallet",
"iat": 1710000000,
"origins": [],
"payload": {
"loyaltyObjects": [
{ "id": "ISSUER_ID.OBJECT_ID" }
]
}
}Hyphens are allowed in the object ID; the documented character set is alphanumeric plus Finally, open the save URL in an external browser or Custom Tab. An embedded WebView can fail because the Google page is protected by frame policies. Official references: |
Uh oh!
There was an error while loading. Please reload this page.
Discussion Type
Product Feedback
Discussion Content
Hi everyone,
I'm working on the digital membership card module of a loyalty app (Node.js/Express backend, React Native mobile app), and I'm stuck on wallet pass generation for both platforms. The passes are the core of the loyalty program UX — members should add their card to Apple Wallet or Google Wallet and see points balance updates pushed to it. Two separate signing problems, posting both since they may be related to how I'm handling certificates.
Environment
Node.js 20.11 (backend), Express 4.19
passkit-generator v3.2 for Apple Wallet (.pkpass)
google-auth-library v9.x + Google Wallet REST API for Google passes
Certificates generated via Apple Developer portal (Pass Type ID certificate) and exported from Keychain on macOS Sonoma
Server: Ubuntu 22.04 (production), also failing locally
Problem 1 — Apple Wallet: pass builds but iPhone rejects it
The .pkpass file generates without errors, but when opened on-device, iOS shows the pass preview and then silently fails to add it. Console logs on the device (via Xcode) show:
Invalid data error reading pass pass.com.example.loyalty/8f3a...
Signature validation failed
What I've done:
Exported the Pass Type ID certificate + private key from Keychain as .p12, then split it:
bash
openssl pkcs12 -in Certificates.p12 -clcerts -nokeys -out signerCert.pem -legacy
openssl pkcs12 -in Certificates.p12 -nocerts -out signerKey.pem -legacy
Downloaded Apple's WWDR G4 intermediate certificate and converted it:
bash
openssl x509 -inform der -in AppleWWDRCAG4.cer -out wwdr.pem
Pass generation code:
js
const { PKPass } = require("passkit-generator");
const pass = await PKPass.from({
model: "./models/loyalty.pass",
certificates: {
wwdr: fs.readFileSync("./certs/wwdr.pem"),
signerCert: fs.readFileSync("./certs/signerCert.pem"),
signerKey: fs.readFileSync("./certs/signerKey.pem"),
signerKeyPassphrase: process.env.PASS_KEY_PASSPHRASE,
},
}, {
serialNumber: member.id,
// storeCard type with points balance in secondaryFields
});
Verified passTypeIdentifier in pass.json exactly matches the certificate's Pass Type ID, and teamIdentifier matches our team ID.
Suspicion: is this a WWDR G4 vs G6 mismatch? My Pass Type ID cert was issued in June 2025. openssl x509 -noout -issuer -in signerCert.pem shows the issuer CN as "Apple Worldwide Developer Relations Certification Authority" but I can't tell which generation it chains to. Does anyone know a reliable way to determine which WWDR intermediate a Pass Type ID cert needs, and whether a mismatch produces exactly this silent "Signature validation failed" behavior?
Problem 2 — Google Wallet: INVALID_ARGUMENT on JWT for Save link
For Google Wallet I'm creating a LoyaltyObject and generating an "Add to Google Wallet" JWT link:
js
const jwt = require("jsonwebtoken");
const claims = {
iss: serviceAccount.client_email,
aud: "google",
typ: "savetowallet",
payload: {
loyaltyObjects: [{ id:
${ISSUER_ID}.${member.id}, classId:${ISSUER_ID}.loyalty_class_v1}],},
};
const token = jwt.sign(claims, serviceAccount.private_key, { algorithm: "RS256" });
const saveUrl =
https://pay.google.com/gp/v/save/${token};The class exists (created via REST API, state UNDER_REVIEW → approved). But the save link returns:
Something went wrong. [DEVELOPER_ERROR: INVALID_ARGUMENT]
If I create the loyalty object via REST first and then reference only its ID in the JWT, same error. The service account has the Wallet API enabled and is added as a user on the issuer account with Developer access. One thing I'm unsure about: the docs mention origins in the JWT for web buttons — is origins mandatory even when the link is opened from a mobile app WebView rather than a website? And does the member ID portion after ISSUER_ID. have character restrictions (mine are UUIDs with hyphens)?
What I've ruled out
Certificate expiry (both valid into 2027)
JSON validity of pass.json (validated, no trailing commas / BOM)
File ordering in the manifest — passkit-generator handles manifest + signature internally
Clock skew on the server (NTP synced)
If anyone has shipped loyalty passes on both platforms — especially anything around the WWDR intermediate generations or Google Wallet JWT origins behavior in app WebViews — I'd really appreciate a pointer. Happy to share the full pass.json model or the loyalty class definition if useful.
Thanks!
All reactions