NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages
Purchases for Laravel

Signature verification & trust

Every cryptographic primitive comes from crypto-for-laravel; every trust decision stays in this package. Purchases calls no openssl_* function of its own:

Storecrypto-for-laravel doesPurchases keeps
StripeHMAC-SHA256 and constant-time comparisonThe t=/v1= header scheme and the replay-tolerance window (default 300 s)
AppleES256 JWS verify and sign, plus all X.509 maths — parsing, SHA-1 fingerprints, chain linkage, validity datesThe trust decision — the x5c chain pinned by fingerprint to Apple’s WWDR intermediate and Root CA G3, each link proven to have signed the one below, every certificate in date, the leaf and intermediate carrying Apple’s App Store signing (1.2.840.113635.100.6.11.1) and WWDR (1.2.840.113635.100.6.2.1) marker extensions — then the app binding (bundle id, environment, Apple ID)
GoogleRS256 JWS assertionThe OAuth2 JWT-bearer grant, scope and access-token caching

Crypto exceptions are translated into VerificationException at every provider boundary, so one catch covers every store. Apple’s pinned fingerprints prove only that Apple signed a notification — Apple signs every app’s notifications with that chain — so purchases also binds each one to your app: bundle id, environment and, in production, Apple ID (see Apple App Store).

Apple certificate validity

An App Store notification is rejected when any certificate in its x5c chain — leaf, intermediate or root — is outside its notBefore..notAfter window. A clock-skew leeway, applied to both ends of the window, absorbs a host clock that runs slightly fast or slow:

// config/purchases.php
'settings' => [
    'apple' => [
        // Seconds, 0–3600. Default 60. Anything outside that range is a
        // misconfiguration and throws InvalidConfigurationException — a
        // fat-fingered value can never silently switch the check off.
        'certificate_clock_skew' => env('PURCHASES_APPLE_CERTIFICATE_CLOCK_SKEW', 60),
    ],
],

The rejection has its own message, naming the certificate and the instant it lapsed, so an expired chain is never mistaken for a bad signature:

RoundlyConsulting\Purchases\Exceptions\VerificationException:
Apple certificate [Apple Worldwide Developer Relations Certification Authority] expired at
2026-01-01T00:00:00+00:00; the notification's certificate chain is outside its validity period.

Re-verifying archived notifications

Re-verifying an archived Apple notification signed by a since-rotated, now-expired certificate fails — the correct outcome, since an expired certificate proves nothing. Raise certificate_clock_skew only to absorb clock drift; it is not a grace period and is capped at one hour. purchases:replay is unaffected: it rebuilds results from the audit snapshots, which were verified when they arrived.

Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

More ways to support, including crypto

By donating, you agree to our donation terms.

Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.