Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 29 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,18 +111,25 @@ verifier:
did:
# identification of the verifier in communication with wallets
clientIdentification:
# identification used by the verifier when requesting authorization. Can be a did, but also methods like x509_san_dns
# identification used by the verifier when requesting authorization, following the OIDC4VP client identifier prefixes(see https://openid.net/specs/openid-4-verifiable-presentations-1_0.html). Can be a did (e.g. "did:web:..."), an x509_san_dns entry ("x509_san_dns:<hostname>") or a redirect_uri entry ("redirect_uri:<the verifier's own callback url>"). redirect_uri only works with the "urlEncoded" request mode, see "Request modes" below - it is the only scheme whose requests must NOT be signed. If left empty, the "urlEncoded" mode defaults it per-request to "redirect_uri:" followed by the verifier's own callback URL, so it always matches.
id:
# path to the signing key(in pem format) for request object. Needs to correspond with the id
# path to the signing key(in pem format) for request object. Needs to correspond with the id. Not used (and not needed) for the "redirect_uri" id scheme, since that scheme is never signed.
keyPath:
# algorithm to be used for signing the request. Needs to match the signing key
# algorithm to be used for signing the request. Needs to match the signing key. Not used for the "redirect_uri" id scheme.
requestKeyAlgorithm:
# depending on the id type, the certificate chain needs to be included in the object(f.e. in case of x509_san_dns)
# depending on the id type, the certificate chain needs to be included in the object(f.e. in case of x509_san_dns). Not used for the "redirect_uri" id scheme.
certificatePath:
# Kid used when key certificate does not include it. If both are missing, id is used
# Kid used when key certificate does not include it. If both are missing, id is used. Not used for the "redirect_uri" id scheme.
kid:
# supported modes for requesting authentication. in case of byReference and byValue, the clientIdentification needs to be properly configured
# supported modes for requesting authentication. in case of byReference and byValue, the clientIdentification needs to be properly configured(signing key required). urlEncoded is the only mode that does not sign the request, and the only one compatible with the "redirect_uri" id scheme above.
supportedModes: ["urlEncoded", "byReference","byValue"]
# request mode used for flows where the caller has no way to request one explicitly - the
# OIDC-bridging authorization endpoint(/api/v1/authorization), and /api/v2/loginQR when no
# request_mode query parameter is given. Must be one of supportedModes above. Defaults to
# "byReference" for backwards compatibility, but only when "byReference" is itself listed in
# supportedModes - otherwise it defaults to the first entry of supportedModes instead, so
# e.g. a supportedModes: ["urlEncoded"]-only config keeps working without setting this.
requestMode: byReference
# address of the (ebsi-compliant) trusted-issuers-registry to be used for verifying the issuer of a received credential
tirAddress:
# Expiry(in seconds) of an authentication session. After that, a new flow needs to be initiated.
Expand Down Expand Up @@ -578,15 +585,28 @@ The mode can be set during the intial requests, by sending the parameter "reques

#### urlEncoded

The only mode where the request is **not** signed — every parameter (including `presentation_definition`/`dcql_query`, when configured) is inlined as a plain query parameter instead of being wrapped in a JWT. Because nothing is fetched separately and nothing is base64-encoded into a compact JWT, the resulting URI/QR-code is larger than with `byValue`/`byReference`.

This is the **only mode compatible with the `redirect_uri` [client identifier prefix](https://openid.net/specs/openid-4-verifiable-presentations-1_0.html)** (as opposed to e.g. `did:...` or `x509_san_dns:...`). Per the OIDC4VP spec, requests using `redirect_uri` as client identifier scheme cannot be signed, since there is no key/certificate for the wallet to verify a signature against — the wallet's only trust check is that the response is sent back to the exact URI embedded in `client_id`.

To use it:
* leave `clientIdentification.id` empty and the verifier defaults it, per request, to `redirect_uri:` followed by its own callback URL (`<host><pathPrefix>/api/v1/authentication_response`) — this is always consistent, since both values are derived from the same incoming request.
* alternatively, set `clientIdentification.id` explicitly to `redirect_uri:` followed by the verifier's own callback URL, exactly as it will be sent as `response_uri`, e.g. `redirect_uri:https://verifier.org/api/v1/authentication_response`. If the host in `id` ever stops matching the host of the incoming request (e.g. after an ingress hostname or `pathPrefix` change), the verifier rejects the request instead of sending a `client_id`/`response_uri` pair the wallet is guaranteed to reject.
* `keyPath`, `requestKeyAlgorithm` and `certificatePath` are **not needed** for this mode (no signing, no `x5c` header) — they only matter if `byValue`/`byReference` are also listed in `supportedModes` for other wallets.
* make sure `"urlEncoded"` is included in `supportedModes`, and either pass `requestMode=urlEncoded` on the initial request, or set it as the default (see below).
* if `"urlEncoded"` is the **only** entry in `supportedModes`, `verifier.requestMode` defaults to it automatically (the built-in `byReference` default only applies when it is itself listed in `supportedModes`) — you only need to set `verifier.requestMode` explicitly if you list several modes and want a non-`byReference` default.

Trade-off: `redirect_uri` gives up the cryptographic proof of the verifier's identity that `did:...`/`x509_san_dns:...` provide — use it only when the wallet you need to support doesn't implement a signed scheme (some do not, see wallet compatibility notes below), or as a fallback for wallets you don't fully control.

Example:
```
openid4vp://?response_type=vp_token&response_mode=direct_post&client_id=did:key:verifier&redirect_uri=https://verifier.org/api/v1/authentication_response&state=randomState&nonce=randomNonce
openid4vp://?response_type=vp_token&response_mode=direct_post&client_id=redirect_uri:https://verifier.org/api/v1/authentication_response&response_uri=https://verifier.org/api/v1/authentication_response&state=randomState&nonce=randomNonce
```

#### byValue
Example:
```
openid4vp://?client_id=did:key:verifier&request=eyJhbGciOiJFUzI1NiIsInR5cCI6Im9hdXRoLWF1dGh6LXJlcStqd3QifQ.eyJjbGllbnRfaWQiOiJkaWQ6a2V5OnZlcmlmaWVyIiwiZXhwIjozMCwiaXNzIjoiZGlkOmtleTp2ZXJpZmllciIsIm5vbmNlIjoicmFuZG9tTm9uY2UiLCJwcmVzZW50YXRpb25fZGVmaW5pdGlvbiI6eyJpZCI6IiIsImlucHV0X2Rlc2NyaXB0b3JzIjpudWxsLCJmb3JtYXQiOm51bGx9LCJyZWRpcmVjdF91cmkiOiJodHRwczovL3ZlcmlmaWVyLm9yZy9hcGkvdjEvYXV0aGVudGljYXRpb25fcmVzcG9uc2UiLCJyZXNwb25zZV90eXBlIjoidnBfdG9rZW4iLCJzY29wZSI6Im9wZW5pZCIsInN0YXRlIjoicmFuZG9tU3RhdGUifQ.Z0xv_E9vvhRN2nBeKQ49LgH8lkjkX-weR7R5eCmX9ebGr1aE8_6usa2PO9nJ4LRv8oWMg0q9fsQ2x5DTYbvLdA
openid4vp://?client_id=did:key:verifier&request=eyJhbGciOiJFUzI1NiIsInR5cCI6Im9hdXRoLWF1dGh6LXJlcStqd3QifQ.eyJjbGllbnRfaWQiOiJkaWQ6a2V5OnZlcmlmaWVyIiwiZXhwIjozMCwiaXNzIjoiZGlkOmtleTp2ZXJpZmllciIsIm5vbmNlIjoicmFuZG9tTm9uY2UiLCJwcmVzZW50YXRpb25fZGVmaW5pdGlvbiI6eyJpZCI6IiIsImlucHV0X2Rlc2NyaXB0b3JzIjpudWxsLCJmb3JtYXQiOm51bGx9LCJyZXNwb25zZV91cmkiOiJodHRwczovL3ZlcmlmaWVyLm9yZy9hcGkvdjEvYXV0aGVudGljYXRpb25fcmVzcG9uc2UiLCJyZXNwb25zZV90eXBlIjoidnBfdG9rZW4iLCJzY29wZSI6Im9wZW5pZCIsInN0YXRlIjoicmFuZG9tU3RhdGUifQ.Z0xv_E9vvhRN2nBeKQ49LgH8lkjkX-weR7R5eCmX9ebGr1aE8_6usa2PO9nJ4LRv8oWMg0q9fsQ2x5DTYbvLdA
```
Decoded:
```json
Expand All @@ -604,7 +624,7 @@ Decoded:
"input_descriptors": null,
"format": null
},
"redirect_uri": "https://verifier.org/api/v1/authbyValentication_response",
"response_uri": "https://verifier.org/api/v1/authentication_response",
"response_type": "vp_token",
"scope": "openid",
"state": "randomState"
Expand Down
5 changes: 5 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,11 @@ type Verifier struct {
ClientIdentification ClientIdentification `mapstructure:"clientIdentification"`
// supported request modes - currently 'urlEncoded', 'byValue' and 'byReference' are available. In case of byValue, the keyPath has to be set.
SupportedModes []string `mapstructure:"supportedModes" default:"urlEncoded"`
// request mode to use for flows where the caller has no way to request one explicitly
// (e.g. the OIDC-bridging /api/v1/authorization endpoint, or /api/v2/loginQR when no
// request_mode query parameter is given). Must be one of supportedModes when set. Defaults
// to "byReference" to preserve the pre-existing, hardcoded behaviour.
RequestMode string `mapstructure:"requestMode" default:"byReference"`
// address of the (ebsi-compatible) trusted-issuers-registry for verifying the issuer
TirAddress string `mapstructure:"tirAddress"`
// expiry of the tir-cache entries
Expand Down
4 changes: 2 additions & 2 deletions config/configClient.go
Original file line number Diff line number Diff line change
Expand Up @@ -518,7 +518,7 @@ type DCQL struct {
// A non-empty array of Credential Queries that specify the requested Credentials.
Credentials []CredentialQuery `json:"credentials" mapstructure:"credentials"`
// A non-empty array of Credential Set Queries that specifies additional constraints on which of the requested Credentials to return.
CredentialSets []CredentialSetQuery `json:"credential_sets" mapstructure:"credential_sets"`
CredentialSets []CredentialSetQuery `json:"credential_sets,omitempty" mapstructure:"credential_sets,omitempty"`
}

// CredentialQuery is an object representing a request for a presentation of one or more matching Credentials
Expand All @@ -538,7 +538,7 @@ type CredentialQuery struct {
// A non-empty array containing arrays of identifiers for elements in claims that specifies which combinations of claims for the Credential are requested.
ClaimSets [][]string `json:"claim_sets,omitempty" mapstructure:"claim_sets,omitempty"`
// A non-empty array of objects that specifies expected authorities or trust frameworks that certify Issuers, that the Verifier will accept. Every Credential returned by the Wallet SHOULD match at least one of the conditions present in the corresponding trusted_authorities array if present.
TrustedAuthorities []TrustedAuthorityQuery `json:"trusted_authorities" mapstructure:"trusted_authorities"`
TrustedAuthorities []TrustedAuthorityQuery `json:"trusted_authorities,omitempty" mapstructure:"trusted_authorities,omitempty"`
}

// RequiresCryptographicHolderBinding returns true when the field is nil (absent) or explicitly true.
Expand Down
19 changes: 19 additions & 0 deletions config/configClient_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -485,3 +485,22 @@ func TestTrustedIssuersListsDecodeHook(t *testing.T) {
assert.Equal(t, input, result)
})
}

func Test_DCQL_MarshalJSON_OmitsUnsetOptionalArrays(t *testing.T) {
dcql := DCQL{Credentials: []CredentialQuery{{Id: "my-cred"}}}

out, err := json.Marshal(dcql)
assert.NoError(t, err)
assert.NotContains(t, string(out), "credential_sets")
assert.NotContains(t, string(out), "trusted_authorities")

var roundTripped map[string]interface{}
assert.NoError(t, json.Unmarshal(out, &roundTripped))
_, hasCredentialSets := roundTripped["credential_sets"]
assert.False(t, hasCredentialSets)

credentials := roundTripped["credentials"].([]interface{})
credential := credentials[0].(map[string]interface{})
_, hasTrustedAuthorities := credential["trusted_authorities"]
assert.False(t, hasTrustedAuthorities)
}
3 changes: 3 additions & 0 deletions config/provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ func Test_ReadConfig(t *testing.T) {
KeyAlgorithm: "RS256",
GenerateKey: true,
SupportedModes: []string{"urlEncoded"},
RequestMode: "byReference",
JwtExpiration: 30,
StatusListCacheExpiry: DefaultStatusCacheExpirySeconds,
StatusListHttpTimeout: DefaultStatusHttpTimeoutSeconds,
Expand Down Expand Up @@ -145,6 +146,7 @@ func Test_ReadConfig(t *testing.T) {
KeyAlgorithm: "RS256",
GenerateKey: true,
SupportedModes: []string{"urlEncoded"},
RequestMode: "byReference",
JwtExpiration: 30,
StatusListCacheExpiry: DefaultStatusCacheExpirySeconds,
StatusListHttpTimeout: DefaultStatusHttpTimeoutSeconds,
Expand Down Expand Up @@ -198,6 +200,7 @@ func Test_ReadConfig(t *testing.T) {
KeyAlgorithm: "RS256",
GenerateKey: true,
SupportedModes: []string{"urlEncoded"},
RequestMode: "byReference",
JwtExpiration: 30,
StatusListCacheExpiry: DefaultStatusCacheExpirySeconds,
StatusListHttpTimeout: DefaultStatusHttpTimeoutSeconds,
Expand Down
Loading
Loading