What PingFederate does when it issues and verifies verifiable credentials, and which keys and certificates it holds, signs with, publishes and trusts. Drawn from the idp-pf-vcs platform as deployed on production.
Left: what PF signs. Centre: the key material PF holds. Right: what PF verifies. Select a key to see where it is used.
pf-oid4vcicnf.pf-oid4vcipf-oid4vptyp=oauth-authz-req+jwt, ES256, leaf-only x5c, client_id = x509_hash:…, DCQL query.pf-dtsstatuslist+jwt, ES256, LSB-first zlib bit array. sub is the list URI.PF nativejwtatm manager, carrying openid_credential authorization_details.pf-oid4vpclient_metadata. Lives minutes, in session.services/pingfederate · TerraformKeyAccessor.getDsigKeypair(id)pingfederate_keypairs_signing_key. Private key crosses the wire once, at import. Rotation is a terraform apply./pf/JWKS · JwksEndpointKeyAccessorTrustedCAAccessor · pingfederate_certificate_caPF volume · vc-data/c_nonces, VP sessions, offers, status-list bits and next index.pf-oid4vci /credentialcredential_configuration_id must be in authorization_details.pf-oid4vci /credentialc_nonce. The proven key becomes cnf or the mdoc device key.pf-oid4vp /responsesdirect_post.jwt JWE, verify the issuer JWS via jwt-vc-issuer, disclosures against _sd, KB-JWT over aud, nonce, sd_hash.pf-oid4vp /responsespf-oid4vp · walletskid against PF's JWKS. A kid absent from the set is refused by name.outside PFclient_credentials token; SCIM verifies ES256 against /pf/JWKS and allowlists the client id.Each extension is a Maven module under pf-ext/, baked into the PF image. WARs mount at their file name; plugins are discovered through a PF-INF marker. Shared crypto lives in vc-core, which imports no PingFederate type.
Pre-authorized code and authorization code, both formats, claims sourced from the SCIM identity store.
pf-oid4vci.warLive/pf-oid4vci/*/.well-known/openid-credential-issuerNames PF's /as/authorization.oauth2 for auth-code and the WAR's own mini-AS for pre-authorized_code./.well-known/jwt-vc-issuerPublishes the SD-JWT issuer key verifiers resolve./nonceIssues the c_nonce the holder's key proof must sign./tokenPre-authorized code only. Auth-code tokens come from PF itself./credentialThe issuing step. See below./admin/offersMints a pre-authorized offer. Bearer-protected; the connector calls it./admin/healthNames which keys resolved from PF and which are still in the environment.PF_OID4VCI_CLIENTS) and enforces the RAR: a token authorising pid-sd-jwt that asks for mdl-mdoc gets 403.c_nonce.client_credentials token. An mDL needs a driver-licence document on file; missing evidence is insufficient_evidence, never invented.pf.plugins.pf-vc-rar.jarLiveAuthorizationDetailProcessorvcrarProcessor instance registered for the openid_credential authorization-detail type.vci-svcPF client allowed to request that RAR type (authorization_detail_types).openid_credential entries at authorization time, so the grant carries typed authorization_details the credential endpoint reads off the token.Signed request out, encrypted response in, and a wallet presentation as a first-class PF login.
pf-oid4vp.warLive/pf-oid4vp/*POST /sessionsCreates a verification session. ?credential=mdl asks for an mDL; default is the PID SD-JWT VC.GET /request/{id}The signed request object the wallet fetches and checks before showing consent.POST /responsesReceives the wallet's direct_post.jwt JWE.GET /sessions/{id}Poll for verified and the disclosed claims.x5c (the anchor is never shipped), x509_hash client id, a fresh encryption key, a DCQL query for PID or mDL.kid.aud, nonce, sd_hash.pf.plugins.pf-vp-adapter.jarLiveIdpAuthenticationAdapterV2vpWalletLoginAdapter instance, credential pid. Root of the SignInWithWallet policy tree.contractgiven_name, family_name, birthdate, document_number, credential_format.pf-oid4vp session over HTTP at PF's own base URL (plugin and WAR are separate classloaders).The material both sides pin. Served by PF, signed with PF's own published key.
pf-dts.warLive/pf-dts/*/statuslists/1Token Status List JWT, ES256, verifiable at /pf/JWKS. Allocate an index at issuance; flip a bit to revoke./pki/iaca.pemThe IACA anchor issued mdocs chain to./pki/reader-root.pemThe anchor wallets use to trust a signed request./healthBaked git commit and status_list_signer: pinned or unpinned-first-p256.PingFederate nativeBuilt inOAuth AS/as/authorization.oauth2Auth-code issuance and the wallet-login policy./as/token.oauth2Access tokens for wallet, issuer, verifier, portal and console clients./pf/JWKSThe published signing set everything above verifies against.jwtatmJWT access token manager. Carries client_id and authorization_details; sets no aud, hence the client-id gate.Every piece of key material PF signs with, publishes or trusts. "Resolves from" is the preferred path; the environment variables remain as the legacy path until the migration is verified at /pf-oid4vci/admin/health.
| Key / certificate | Type | Resolves from | Signs | Verified against by | Minted where | Rotation |
|---|---|---|---|---|---|---|
SD-JWT issuer keysdjwt-issuer-1 |
EC P-256, ES256 | PF keystorePF_SDJWT_KEYPAIR_IDlegacy ISSUER_SIGNING_JWK |
SD-JWT VC issuer JWS | Wallets and verifiers via /pf-oid4vci/.well-known/jwt-vc-issuer |
Offline, wrapped as PKCS12, imported by Terraform | terraform apply with new file data; id changes, WAR restarts |
mdoc Document Signermdoc-ds-1 |
P-256 leaf certificate, signed by the IACA (18013-5 Annex B) | PF keystorePF_MDOC_DS_KEYPAIR_IDlegacy MDOC_DS_JWK + MDOC_DS_PEM |
MSO / IssuerAuth (COSE_Sign1) | Verifiers chain DS to the IACA; the wallet's trusted_authorities filter matches by AKI |
Offline ceremony; the IACA signs the leaf. PF never self-signs it | Re-sign offline, re-import |
IACA rootmdoc-iaca-1 |
X.509 CA, offline | PF trusted CApingfederate_certificate_calegacy MDOC_IACA_PEM for the verifier |
Nothing at runtime (offline) | pf-oid4vp and the iOS reader trust mdocs whose DS chains here; published at /pf-dts/pki/iaca.pem |
Offline (scripts/pki.mjs); carries SKI/AKI |
New root means re-issue; old URIs stay proxied until re-issue |
Reader authentication leafreader-leaf-1 |
P-256 leaf certificate, signed by the reader root | PF keystorePF_READER_KEYPAIR_IDlegacy READER_LEAF_JWK + READER_LEAF_PEM |
OID4VP request object (JAR), leaf-only x5c. Its SHA-256 is the x509_hash client id |
Wallets, against the pinned reader root | Offline; same identity as the standalone verifier so one pin covers both | Changes the verifier's client id; wallets re-pin nothing (root unchanged) |
Reader rootreader-root.pem |
X.509 CA, offline | env READER_ROOT_PEM, republished by pf-dts |
Nothing at runtime | Wallets pin it from /pf-dts/pki/reader-root.pem |
Offline | Wallet re-pin |
Status-list signerPF_STATUS_LIST_KID |
P-256 kid in PF's OAuth JWKS | PF JWKSJwksEndpointKeyAccessor; unpinned falls to first P-256 and says so at /pf-dts/health |
statuslist+jwt at /pf-dts/statuslists/1 |
Wallets and both verifier ports, by kid; a kid outside the set is refused |
PF's own key management | PF rotation. G-31 open no purpose separation from the access-token signer |
Access-token signerjwtatm |
ES256, PF OAuth JWKS | PF JWKS Access Token Manager jwtatm |
JWT access tokens, incl. openid_credential authorization_details |
pf-oid4vci in-process (no fetch); SCIM over /pf/JWKS with the Go stdlib |
PF native | PF native |
Response encryption keyephemeral |
P-256, ECDH-ES + A128GCM / A256GCM | PF session per request, resolved by kid to session |
Nothing; it decrypts the wallet's direct_post.jwt |
The wallet encrypts to it from client_metadata |
In the WAR, per session | Every request |
Holder keycnf / deviceKey |
P-256, the wallet's | not PF's proven at /credential |
Key proof over c_nonce; KB-JWT or device signature at presentation |
pf-oid4vci binds it into the credential; pf-oid4vp verifies it at /responses |
Wallet (Secure Enclave or software) | Wallet's concern |
Service client secretsvci-svc · issuer-svc · verifier-svc · portal-svc |
Client secret (SECRET auth) |
PF OAuth clients Terraform variables, sensitive |
Nothing; they obtain client_credentials tokens |
PF at /as/token.oauth2; SCIM allowlists the resulting client_id |
Terraform | Rotate the variable, apply |
PF_SDJWT_KEYPAIR_ID stops the WAR rather than signing with something nobody chose.x5c (HAIP §5); wallets already hold the root./health serves a coarse token (key_sources: mixed); the full picture sits behind the bearer at /admin/health, because naming a straggler key is target selection./as/authorization.oauth2. The SignInWithWallet policy tree starts at vpWalletLogin.pf-oid4vp session and commits an HTML page with the QR or same-device link. PF parks the transaction until the browser returns to the resume path.verified.WalletIdentity policy contract. PF issues a real authorization code; the ID token carries the credential's claims.Proven end to end on production on 2 Aug 2026 with an issuer-signed PID SD-JWT VC. The connector demo uses this same path as its "Connector AS".
id-kp-oauthStatusSigning EKU (OID still TBD) or realigning PF's two signing paths.GAP-REGISTER G-31/pf-oid4vci/admin/health as the truth; key_sources: mixed means environment stragglers remain.docs/KEY-MIGRATION.mdpf-oid4vci, but it stays behind PF_PROVISIONING until the verifier trusts both IACAs. PF-issued mdocs would fail verification there today.idp-dcapi-demo site/src/index.tsGET /pf-dts/healthBaked commit matches the build; status_list_signer reads pinned.GET /pf-oid4vp/healthBaked commit; reader identity loaded.GET /pf-oid4vci/admin/healthBearer-protected. Names every key that resolved from PF and every straggler still in the environment, with a scope line.GET /pf-dts/statuslists/1Content type statuslist+jwt, verifies against /pf/JWKS./pf-admin-api/v1/idp/adaptersau.idp.pf.vpadapter.VpAdapter present in the descriptors./pf-admin-api/v1/oauth/authorizationDetailProcessorsau.idp.pf.rar.RarProcessor present; instance vcrar.startup loga=AVAILABLE for each WAR context. The platform can silently serve last-good.conformance/run.sh479 spec-cited assertions. PASSED WITH GAPS is not a pass; CI makes a skipped toolchain fatal.In the ConnectID demo the request-signing authority sits with the Trust Controller stand-in, which signs the DCQL request with the relying party's federation key. PF as Connector AS never holds the key that names the RP. That is by design, not a gap.