All articles
// developers

What Actually Goes Into a .pkpass File, and Why Wallet Rejects Yours

A .pkpass is a signed zip. The manifest hashes every file, the signature covers the manifest, and one wrong byte fails the whole thing silently.

Updated
What Actually Goes Into a .pkpass File, and Why Wallet Rejects Yours
Julio Song
10 min read

A developer we'll call Marcus has the pass generation code working. pass.json has the right fields, the barcode encodes correctly, the icon renders at the right size. He zips the folder, serves it from an endpoint, taps the link on his phone. Wallet shows "The pass cannot be installed to Passbook at this time" and nothing else. No line number, no field name, no hint about which of the dozen moving parts failed. He checks pass.json again. It's fine. The bug isn't in the pass at all. It's in the two files sitting next to it that most tutorials mention in one paragraph and move past: manifest.json and signature.

Those two files are what turn a folder of JSON and PNGs into something Wallet will actually trust. Get the pass content right and skip the manifest or signature step and you still get a rejected pass, with an error message that gives you nothing to work from. Here's what's actually in that zip, what the manifest and signature are checking, and the specific ways this breaks.

What's actually inside the zip

A .pkpass file is a zip archive with a different extension, and the structure inside it is flatter than most people expect. At the top level sit pass.json, whatever images the pass uses (icon.png at minimum, plus logo.png, strip.png, and their @2x and @3x variants depending on the pass style), an optional .lproj folder per language for localized strings, manifest.json, and signature. Nothing is nested inside a wrapping folder. Zip the directory itself rather than its contents and you get a pass where every path in manifest.json is off by one level, which fails validation with the same generic error as everything else on this list.

Two more details trip up generated passes specifically. Metadata files that macOS or a zip library adds automatically, .DS_Store chief among them, have to be left out. And every file that ends up in the archive has to be accounted for in manifest.json, so an image you reference in pass.json but forgot to include, or a stray file your build tool left behind, both produce a mismatch between what the manifest promises and what the archive actually contains.

The one image you can't skip

icon.png is, in practice, the only image Wallet actually requires, and it's easy to underrate because it's the smallest asset in the bundle. It's what shows up on the lock screen and in Mail when someone forwards a pass, and Wallet refuses to install a pass that's missing it, independent of whether the manifest and signature are otherwise flawless. Apple's own guide specifies it at 29x29 points, with icon@2x.png at 58x58 and icon@3x.png at 87x87 to cover the resolutions your pass might render at. If your pass style uses logo.png or strip.png, the same doubling and tripling applies to those too, and each variant needs its own entry in manifest.json like every other file in the bundle. Skip a required @2x or @3x variant and you won't see an error about the specific missing file, just the same generic rejection as everything else here, because from Wallet's side a missing image and a tampered manifest look identical.

The manifest is a hash of everything else

manifest.json is not configuration. It's a fingerprint of the rest of the bundle. Building it means walking every file in the pass folder except the manifest and signature themselves, taking the SHA-1 hash of each file's raw contents, and writing those hashes into a JSON dictionary keyed by the file's path relative to the bundle root:

{
  "icon.png": "3b2e8f1c9a7d4e6f0b1a2c3d4e5f60718293a4b5",
  "logo.png": "9a1b2c3d4e5f60718293a4b5c6d7e8f901234567",
  "pass.json": "7d4e6f0b1a2c3d4e5f60718293a4b53b2e8f1c9a"
}

That's it. No signature, no certificate involved yet, just a hash of every byte in the bundle. SHA-1 is what Apple's own pass-building guide documents, and it's still what the format uses today, even though SHA-1 is a weak choice for anything security-critical on its own. The reason it's fine here is that the manifest is never trusted by itself. It's only ever trusted once something else vouches for it, which is what the next file does.

The signature covers the manifest, not the pass

signature is a detached PKCS#7 signature over manifest.json, built with your Pass Type ID certificate's private key, with Apple's WWDR intermediate certificate attached so the whole chain travels with the pass. Detached means the signature file contains no copy of the manifest's contents, only cryptographic proof that whoever holds the matching certificate signed exactly those bytes. The signing step also embeds an S/MIME signing-time attribute, so the timestamp on the signature comes from the moment you signed rather than a client clock that could say anything.

Notice what the signature does and doesn't cover. It proves the manifest wasn't tampered with, and it proves whoever signed it holds a certificate Apple issued. It says nothing about the pass content itself, because it doesn't need to. The manifest already covers that, one layer down. Chaining trust this way is why you have to build the pieces in a fixed order: manifest first, from the finished set of files, then the signature over that finished manifest. Sign before every file is in its final form and you'll produce a signature that verifies against a manifest that no longer matches what's actually in the zip.

If you're writing this by hand rather than through a library, the practical implication is that signing has to be the very last step in your build, after every image is in its final compressed form and every field in pass.json is final. Most PassKit libraries handle the ordering for you, which is exactly why the failures below tend to show up in code that works around a library rather than in the library itself.

How Wallet checks it, in order

When a phone opens a .pkpass, the checks run in a specific sequence, and that order is why some mistakes fail loudly while others fail with no explanation at all. Wallet first recomputes the SHA-1 hash of every file in the archive and compares each one against what manifest.json claims. A single byte changed anywhere, an image re-compressed after signing, a pass.json hand-edited to fix a typo, fails right here, before any certificate is ever examined. Only once every hash matches does Wallet move on to the signature itself, verifying that the PKCS#7 blob actually covers this exact manifest and walking the attached WWDR certificate back to a root Apple trusts.

That ordering explains a failure pattern that confuses a lot of people building this for the first time. Editing a pass after it's signed, even a change as small as fixing a misspelled word in a field, breaks the pass with no certificate error at all, because the hash check catches it before the signature is ever inspected. If you're chasing a rejected pass and you're sure the certificate is fine, this is the first place to look. The fix isn't touching the certificate. It's re-running the whole build: regenerate the manifest against the new files, then sign that new manifest from scratch. There's no partial update path here. Any change to the bundle, even one you're confident is harmless, means redoing both steps.

This is also why an update to a pass someone already has installed isn't a patch applied to the existing file. It's a brand new .pkpass, built and signed from scratch against the changed data, then pushed to the device through Apple's web service protocol. Nothing about the manifest or signature format allows a partial or incremental update, because the whole point of hashing every file is to make a partial change detectable.

The four ways this breaks in practice

Most rejected passes trace back to one of a small number of mistakes, and none of them show up as a distinct error on the device:

  • The pass gets modified after signing, without re-signing. This is the single most common bug in homegrown pass generators, usually from a library that builds pass.json, signs it, and then a later step in the pipeline touches the file again, maybe to inject a tracking parameter or fix a formatting issue. The manifest hash for that file no longer matches, and the pass fails before the certificate is ever checked.
  • The signature file is empty or truncated. One developer working from the open-source node-passbook library hit exactly this. The pass loaded a signing key that looked correct, ran the signing step without throwing an error, and still produced a signature file with nothing in it. Wallet's response was "Manifest signature did not verify successfully", which describes the symptom accurately and gives no hint that the file itself was blank.
  • The zip wraps a folder instead of the folder's contents. manifest.json records paths relative to the bundle root. Zip the parent directory and every one of those paths is now off by a segment, so every hash lookup fails even though the files themselves are byte-for-byte correct.
  • A file is referenced in pass.json but missing from the manifest, or vice versa. Both directions are treated as tampering. An image you removed from the pass but forgot to delete from disk, or one you added but forgot to reference, produces the same generic rejection as a corrupted signature.

Hedge on frequency more than root cause here. Nobody publishes numbers on which of these four is most common across the whole ecosystem, but the first one, sign-then-edit, shows up often enough in library issue trackers to be worth checking before anything else. If you inherited a pass generator someone else wrote, trace the exact order it does things in before you assume the certificate is the problem. Look for any step that runs after the signing call, a logging line that happens to serialize pass.json again, a CDN that recompresses images on upload, a build script that strips whitespace from JSON files as a formatting pass. Any of those, running after signing instead of before it, produces exactly this failure.

Debugging a pass Wallet won't install

Wallet's on-device error is close to useless by design, so the actual debugging happens somewhere else. On a Mac, Console.app captures the underlying reason at the moment you attempt to add a pass, and it's specific in a way the phone's own error message never is: a hash mismatch on a named file, a certificate that doesn't chain, a team identifier that doesn't match. Apple also ships a command-line tool called signpass in its sample code for exactly this, built to sign and verify a pass bundle outside of Wallet entirely, which is a faster way to catch a manifest or signature problem than shipping a pass to a phone and waiting for the generic failure.

Failing that, you can check the basics by hand. Unzip the .pkpass, confirm every file pass.json references actually exists in the archive, and confirm the set of files matches what manifest.json lists exactly, no extras and nothing missing. That single comparison catches most of the four failure modes above before you ever get to the signature itself.

Handling this correctly means treating manifest generation and signing as one atomic step that runs after every other file in the bundle is finalized, never before, and never partially. Passmint's pass generation builds the manifest and signature as part of the same call that produces the pass, so there's no window where a later step in your pipeline can touch a file after it's already been signed.

Primary sources

Common questions

SHA-1. Apple's own pass-building guide documents SHA-1 for hashing every file in the bundle, and the format still uses it today even though SHA-1 is a weak choice for anything security-critical on its own.
The manifest holds a SHA-1 hash of every file taken at signing time. Any later edit changes that file's hash, so Wallet's hash check fails before the PKCS#7 signature is ever examined.
Yes, in practice. It is the only image Wallet consistently requires across every pass style, shown on the lock screen and in Mail, and a pass missing it gets rejected even with a valid manifest and signature.
No. Any change to the bundle means rebuilding manifest.json and re-signing from scratch, since the manifest hashes every file and catching a partial change is exactly what it is designed to do.
Julio Song

Technical content writer, Passmint

Julio is a technical content writer at Passmint. He writes about Apple PassKit, the Google Wallet API, and what breaks when wallet passes meet production traffic.

More from Julio Song

Related reading

Share this article