Learn · Understand · Build

Verifying the PII Signature — Rotated Keys and the Inactive JWKS

CategorySecurityRead6 minUpdated10 Sep 2026

Verifying the TPP's signature on payment PII is optional. If you do it, this article is about the one thing that will break it: the signature lives as long as the consent, but the key that made it does not. A TPP may rotate its signing key at any time, and a rotated key leaves the active JWKS. Verify at the wrong moment against the wrong key set and you will reject a signature that is perfectly valid.

PIIJWSTrust Framework
01 The problem

One signature, two clocks

This article assumes you have chosen to verify

The PersonalIdentifiableInformation field arrives inside a request that the API Hub has already verified was signed by the authenticated TPP, so verifying the inner JWS is not required — it is a defence-in-depth measure. See Verifying the TPP JWS Signature for when an LFI might choose to. Nothing here makes it mandatory.

The PII on a payment consent is a JWE that encapsulates a JWS. The TPP builds the PII object, signs it with its signing key, encrypts it to your Enc1 public key, and submits it at PAR. That signature is made exactly once and never changes.

The consent, however, can be long-lived. A multi-payment consent stays usable until its ExpirationDateTime, and each payment made under it carries the same PII — the same bytes, signed at the same original moment.

Meanwhile the key that made the signature is on its own, entirely independent schedule:

  • Every Trust Framework signing certificate expires after 13 months.
  • A TPP may rotate whenever it likes — after an incident, on internal policy, or as routine hygiene. It does not need a reason and it does not tell you.
  • Rotation issues a new certificate with a new kid. It does not re-sign anything.
  • Once the old certificate is retired, its kid leaves the active JWKS.

So the two clocks drift apart. The signature stays fixed at the moment of consent creation while the TPP's published key set moves on without it. A consent authorised in March and paid against in October may have been signed with a key that stopped being active in June.

This is not a sandbox concern. Checked against the production keystore, ADCB already holds four active application keys and four retired ones — real rotations, on live organisations, while long-lived consents signed under the older keys remain in force.

02 Where you verify

The point of verification decides which keys you need

The PII reaches your Ozone Connect implementation at more than one point in the consent lifecycle, and the age of the signature is different at each. This is the whole decision.

Where you verifyAge of the signatureKey set you need
POST /consent/action/validateSecondsActive JWKS only
POST /consent/event/postSecondsActive JWKS only
The consent authorisation journeyMinutes to hoursActive and inactive
POST /consent/event/patchMinutes to hoursActive and inactive
POST /paymentsUp to the consent lifetimeActive and inactive

The one safe moment: consent validate

/consent/action/validate is called by the API Hub before the consent is created — synchronously, inside the TPP's PAR request. The signature is at most seconds old. A key cannot have been rotated out between the TPP signing the PII and the Hub calling you about it.

If you verify here, the active JWKS is sufficient. This is the conventional fresh-signature case, and the straightforward createRemoteJWKSet pattern works correctly. The same holds for the post consent event, which fires at the same moment in the lifecycle.

Everywhere else is “later”

The authorisation journey is already later. It is tempting to treat consent authorisation as still “at creation” — it is usually the same session, after all. It is not. Between the TPP creating the consent and the customer actually authenticating on your platform there can be a redirect the customer does not follow immediately, an app switch, a login they abandon and resume, a re-authentication, a device handover.

Payment execution is much later. A single instant payment follows authorisation closely, but a multi-payment consent may execute against the same PII for as long as the consent lives. That is the case with no upper bound short of ExpirationDateTime.

Do not assume the redirect was prompt

The most common way to get this wrong is to reason “the customer authorises seconds after the TPP creates the consent, so the key must still be active”. Usually true; not reliably true. If you verify anywhere other than the creation-time hooks, you MUST be prepared for a key that is no longer active.

03 Active & inactive JWKS

Retired keys are published at a parallel path

Retired keys are not lost. The Trust Framework keystore publishes them at a parallel inactive/ path alongside the active set. Every identifier resolves to a pair of endpoints.

The URL format

Productiontext
Active     https://keystore.directory.openfinance.ae/{id}/application.jwks
Inactive   https://keystore.directory.openfinance.ae/{id}/inactive/application.jwks

Sandbox uses the same shape on its own host:

Sandboxtext
Active     https://keystore.sandbox.directory.openfinance.ae/{id}/application.jwks
Inactive   https://keystore.sandbox.directory.openfinance.ae/{id}/inactive/application.jwks

The inactive/ segment sits between the identifier and the filename — not appended to the filename, and not prefixed to the path.

For the PII signature, {id} is the TPP's software statement ID, which arrives on every relevant call in the o3-caller-software-statement-id header — including /consent/action/validate and POST /payments. You do not need to look it up.

The same inactive/ convention applies to transport.jwks, though that is not relevant to signature verification.

What is in the inactive set

The response is an ordinary JWKS — the same format as the active set, with retired keys instead of current ones. Sampling a production organisation:

EndpointKeys
application.jwks4
inactive/application.jwks4

Every certificate sampled from an inactive/ set had a notAfter in the past, consistent with keys retired by expiry or rotation.

Membership of inactive/ does not tell you why a key was retired

The set does not distinguish a certificate that expired normally, one superseded by a routine rotation, and one revoked for key compromise. If a revocation reason is material to your risk decision, obtain it from the certificate record in the Trust Framework — it cannot be inferred from which JWKS the key appeared in.

Pin the algorithm yourself

Published JWKS entries carry kty, use, kid and x5c — and no alg. Your verifier must therefore pin the algorithm itself: PS256, the only algorithm in the UAE Open Finance FAPI profile. Do not accept whatever the JWS header proposes.

04 Verifying later

Resolve the kid across both sets

If you verify anywhere other than the creation-time hooks, resolve the kid across both sets:

  1. Read the kid from the JWS header of the decrypted PII.
  2. Look it up in the TPP's active JWKS.
  3. If it is not there, look it up in the TPP's inactive JWKS.
  4. Verify the signature against whichever key matched, pinning PS256.

A kid found in the inactive set is the expected outcome for an older consent. It means the TPP rotated its signing key at some point after creating the consent, which is routine and entirely permitted. It is a key-lifecycle event, not a signature defect, and on its own it says nothing adverse about the signature or the TPP.

Order the lookup active-first

Most consents you verify will be recent, so the active set will almost always hit. Treat the inactive lookup as the fallback, and cache both sets as you would any JWKS rather than fetching per request.

The straightforward pattern shown in the API guide resolves the active JWKS only:

Active keys onlytypescript
const jwks = createRemoteJWKSet(new URL(jwksUri))   // active keys only
const { payload } = await jwtVerify(jwsString, jwks)

That is correct at /consent/action/validate. Used at payment execution it will work for months and then begin failing on long-lived consents — with no change at either end, and in a way that presents as a signature error when the signature is fine.

05 Timing claims

The same default catches exp and nbf

Key resolution is the subject of this article, but the same libraries carry a second default that fails for the same underlying reason, and it is worth knowing about while you are here.

jwtVerify and its equivalents enforce exp and nbfagainst the current time by default. On the PII JWS those claims bound the PAR submission window — the moment the TPP created the consent — not the moment you happen to be verifying. At payment execution on a long-lived consent they will normally have lapsed.

If you verify the PII signature at a later point, evaluate its timing claims against the consent's CreationDateTime rather than against now, or do not evaluate them at all. Whether the consent is still usable today is answered by the API Hub's consent validation on every request, not by a claim inside the PII.