OID4VCgo implements OpenID4VCI + OpenID4VP under the HAIP 1.0 profile — protocol code intended to run in high-assurance, security-critical deployments (digital identity wallets, credential issuers and verifiers). If you believe you've found a vulnerability (a protocol violation, a cryptographic weakness, an authentication/authorization bypass, or anything else with security impact), please report it privately rather than opening a public issue.
Use GitHub's private vulnerability reporting: open the Security tab on this repository and select Report a vulnerability. This creates a private advisory visible only to maintainers until a fix is ready, rather than a public issue.
If private reporting isn't available for any reason, open a regular issue asking a maintainer to open a private channel — please don't include vulnerability details in a public issue.
OID4VCgo is under early active development and pre-1.0 (APIs may still
change) — see the latest release
for the current tag. Reports against main or the latest tag are the
ones we can act on.
A verified X.509 chain says nothing about a certificate its CA has since
revoked. The verifier's issuer trust checks revocation:
verifier.X5CIssuerKeyResolver and verifier.X5ChainIssuerKeyResolver
require a Revocation (a certrevocation.Checker) and accept a
credential only through a verified path from its issuer's certificate
to a trust anchor on which no certificate is revoked. Supply your CAs'
CRLs with certrevocation.CRLs{Lists: ...}: fetch each CA's CRL from
the distribution point its certificates name (ISO/IEC 18013-5 Annex B
makes one mandatory for an IACA's), keep the parsed lists in memory and
refresh them before their nextUpdate. certrevocation.Refresher does
this for you: give it the distribution point URLs and the CA
certificates that sign their CRLs, run go r.Run(ctx), and pass
r.Checker(). It fetches only from public addresses by default, keeps
the last good list when a fetch fails, and refuses a list that isn't
signed by one of those CAs or is older than the newest it has accepted
from that CA, so an unreachable or tampered-with distribution point
(CRLs are often served over plain http) can't evict or roll back a good
list. Bind each URL to its CA with URLIssuers, and persist what
OnAccept hands over to pass back as Floors on restart. A missing or
stale CRL fails closed. A trust anchor that isn't self-signed, a
pinned leaf or an intermediate CA among the roots, is checked against
its CA's CRL too, so that CA's certificate must be in CRLs.Issuers
(r.Checker() sets the Refresher's own Issuers there), or the
anchor is refused. Where revocation is checked, put only self-signed
roots in the trust anchors, not a CA's whole chain: an intermediate
there needs its own CA's CRL. The Refresher's rollback floor is kept
per CA, by name and key, so one CA can't raise another's by using its
name; NewRefresher refuses two Issuers by one name with different
keys unless AllowSameNameIssuers is set, as for a CA that re-keyed
under its name.
certrevocation.NoCheck{} declines the check explicitly, for
short-lived certificates or a trust framework that removes compromised
issuers from the anchor set itself.
Every other X.509 trust decision can check revocation too, through a
certrevocation.Checker beside the roots it applies to. Each role is
configured on its own, since CRLs fails closed for a CA it has no
current list from and an ecosystem may publish CRLs for some roles and
not others. A wallet must choose for each role it uses: outside
Development, walletflow.New refuses a role whose roots are set
without a Checker (certrevocation.NoCheck{} to decline), as
wallet.New does under wallet.AssuranceProduction for issuer metadata
and an X5CVerifierRoots, and the mobile NewWallet refuses a role
named in neither revocation nor no_revocation_check. Elsewhere — a
development wallet, or the standalone functions and reader-side options
below — a nil Checker checks nothing, and the certificate is trusted
from its chain and validity period alone:
- a wallet's issuer trust on receipt
(
wallet.VerifyIssuedCredentialParams.IssuerRevocation,walletflow.Config.IssuerRevocation); - a Verifier's request-signing certificate
(
wallet.X5CVerifierRoots.Revocation); - signed issuer metadata (
wallet.Config.IssuerMetadataRevocation,walletflow.Config.IssuerMetadataRevocation); - a registrar's certificate (
registration.Registrars.Revocation,walletflow.Config.RegistrarRevocation); - an mdoc reader's certificate (
proximity.ReaderTrust.Revocation,mdocdcapi.ReaderTrust.Revocation,walletflow.Config.MdocReaderRevocation); - a proximity reader's document signer
(
proximity.WithIssuerRevocation, required at every assurance level); - a status list signer (
statuslist.VerifyOptions.Revocation,walletflow.Config.StatusRevocation;verifier.StatusPolicypasses it on from itsstatuslist.Checker, and requires it underverifier.AssuranceProduction); - a key attestation's or jwt proof's certificate, at the issuer
(
issuer.X5CAttestationVerifier.Revocation,issuer.X5CProofBindingKeyResolver.Revocation), whichissuer.AssuranceProductionrequires, ascertrevocation.NoCheck{}to decline.
The mobile library exposes them as the wallet's and reader's crls and
revocation configuration, and the wallet's no_revocation_check,
with SetCRLs to replace the lists before they expire
(mobile/ABI.md). dcql.AKITrustedAuthoritiesChecker checks none of
its own: the verifier hands it the path its issuer key resolver already
checked. A Wallet Attestation's signer is verified by
FAPIgo's server (server.X5CAttesterChain), whose Revocation
(server.ClientCertificateCRLs, or
server.NoClientCertificateRevocationCheck{} to decline) is required
since FAPIgo v0.62.0; the attestation's own status claim is checked by
server.Dependencies.ClientAttestationStatus, which production
requires (server.NoClientAttestationStatusCheck{} to decline). Since
FAPIgo v0.66.0 it also refuses a signer certificate issued for another
role (server.DefaultAttesterLeafPolicy) and an x5c carrying the
trust anchor.
These are risks the library documents rather than closes: most because closing them is up to a party it doesn't control, the rest because they're not yet closed in the library.
- Bearer access tokens. Under
walletflow.ProfileOpenID4VCIa wallet takes a Bearer access token where the Authorization Server issues one (OpenID4VCI §13.2). It asks for the token for the offer's Credential Issuer (RFC 8707resource), but only the Authorization Server can restrict the token to that issuer. If it doesn't, an issuer sharing the Authorization Server with another can redeem the token it receives at the other, for a credential about the End-User.walletflow.ProfileHAIPtakes only DPoP-bound tokens. - Relayed key attestations. A key attestation names no audience,
and the
c_nonceit carries comes from an unauthenticated Nonce Endpoint (OpenID4VCI 1.0 Appendix D, §7). An issuer can hand a wallet ac_nonceit fetched from another issuer, and redeem the key attestation the wallet returns there as theattestationproof type, for a credential about whoever its own access token names, bound to the wallet's keys. That works whether the wallet sent the key attestation as that proof type or in a jwt proof'skey_attestationheader, which carries the same JWT, and against any issuer that takes theattestationproof type. Two things limit it:- an
issuerunder production assurance refuses theattestationproof type unlessConfig.AllowAttestationProofTypetakes it, so by default only jwt proofs, whoseaudnames the issuer, are redeemed; - walletflow refuses an SD-JWT VC whose
issisn't the offer's (unlessConfig.AllowForeignIss).issuernames itself inisswhenPreparesets none, but SD-JWT VC doesn't requireiss, and an mdoc names no issuer: for those, nothing on the wallet's side stops the relay but the issuers it trusts.
- an
- The first CRL fetched over plain http.
certrevocation.Refresherrefuses a list older than one it has accepted from the same CA, or than itsFloors, but nothing bounds the first list it ever accepts from a CA. Over plain http, an attacker on the path can answer that fetch with any list the CA signed that is still inside its validity, one predating a revocation, for as long as the CA's CRL cadence allows (ISO/IEC 18013-5 Annex B allows long ones for an IACA). SetRequireHTTPSwhere the distribution points serve https; otherwise seedFloorswith a list obtained over a trusted channel. - DCQL integers above 2^53. A DCQL query decoded from JSON holds its
numbers as float64, so a
valuesfilter on an integer larger than 2^53 in magnitude is rounded: it can match a claim one off it, and miss its own. This applies to the wallet's filter and the verifier's check alike. - Relayed cross-device requests. In the cross-device flow nothing
ties the device that shows a QR code to the one that started the
Verifier's session (OpenID4VP 1.0 §14.2). An attacker can start a
session at a genuine Verifier, show its QR code on a page of their
own, and receive the result when the holder presents to it: the
request is genuinely the Verifier's, so the wallet's checks pass.
verifier.Transactionscloses this for the same-device flow, where the result is released only to the browser that began the request, through a single-useresponse_code(HAIP 1.0 §5.1), and for the Digital Credentials API, whose response is bound to the requesting page's origin (TransactionsConfig.Origins). A cross-device presentation can only be judged by the holder, from the Verifier the wallet names on its consent screen. Where the Verifier's session must be bound to the person presenting, such as a login, use the same-device flow or the Digital Credentials API, not a QR code. - Response endpoint timing.
verifier.Transactionsanswers every refused or unknown answer to its response endpoint with the same reply, so whoever holds a request's QR code can't learn from it whether the holder has presented. The time taken still differs: a refused answer to a pending request is recorded on the transaction (LastError), a store write, and one to an answered or expired request isn't. Over a quiet network, the timing can tell that party whether and when the holder presented. - Key storage evidence on iOS. A Wallet Provider attests the
wallet's keys to issuers (OpenID4VCI 1.0 Appendix D, HAIP 1.0
§4.5.1), and its
key_storageclaim is only as good as its evidence. The Android library hands the provider each key's Android Key Attestation chain, over the provider's challenge, when the keys are made byAndroidKeystoreKeyStore. iOS has no attestation of Secure Enclave keys a provider can verify: App Attest attests the app, not where its keys live. An iOS provider'skey_storagerests on the app keeping keys in the Secure Enclave, asKeychainKeyStoredoes, not on hardware evidence. The mobile libraries' "Going to production" guides say what a provider checks on each platform. - One mdoc device key may both MAC and sign. With
walletflow.ProximityDeviceMACPreferred, a proximity presentation MACs with the credential's device key where it can and signs where it can't, and OpenID4VP and the DC API always sign. Nothing records which mechanism a key has used, though ISO/IEC 18013-5 §12.4.4 has each key use one. The deniability a MAC gives is then lost for that key; no attack on the key is known. - Linkability across credentials.
walletflowwarns before a presentation would hand one Verifier a copy another has seen (Presentation.Linkable), judging copies of one credential against each other. It doesn't compare values an issuer reuses across credentials (a status list index,suborjtishared by the SD-JWT VC and the mdoc of one issuance, or kept by a refresh): two Verifiers given those can link the holder with no warning. HAIP 1.0 §6.1 makes avoiding such values the issuer's job. - A credential matched only through
aka_vcts. The wallet's DCQL matching treats an SD-JWT VC'saka_vctsas types it is of, andwallet.DefaultSelectiontakes the first matching credential, with no preference for one whose ownvctwas asked for. A credential that claims another type throughaka_vctscan be offered, and pre-selected, for a request for that type. A Verifier decides whether its issuer may issue that type: authorize each ofIssuerAuthorization.MatchedTypes, not onlyType.
- The affected package/file and, if possible, a minimal reproduction.
- The specific OID4VCI / OID4VP / HAIP / FAPI 2.0 requirement or security property you believe is violated (a spec section reference helps — see SPECIFICATIONS.md for exact versions).
- Whether the issue is exploitable against a default configuration or requires a specific, non-default setup.
We'll acknowledge reports and work with you on a fix and coordinated disclosure timeline before any public advisory is published.