[java-idp-plugin-vci] branch main updated: Yet another approach to write Wiki to README
Codeberg
noreply at shibboleth.net
Thu Sep 24 17:47:45 UTC 2026
This is an automated email from the git hooks/post-receive script.
codeberg pushed a commit to branch main
in repository java-idp-plugin-vci.
View the commit online:
https://codeberg.org/Shibboleth/java-idp-plugin-vci/commit/781ae2d2fc5c3676647074642f05ce41a629877f
The following commit(s) were added to refs/heads/main by this push:
new 781ae2d Yet another approach to write Wiki to README
781ae2d is described below
commit 781ae2d2fc5c3676647074642f05ce41a629877f
Author: Janne Lauros <janne.lauros at csc.fi>
AuthorDate: Thu Sep 24 20:47:08 2026 +0300
Yet another approach to write Wiki to README
---
README.md | 2525 +++++++++++++++++++++++++++++--------------------------------
1 file changed, 1216 insertions(+), 1309 deletions(-)
diff --git a/README.md b/README.md
index 62ac2be..9705907 100644
--- a/README.md
+++ b/README.md
@@ -11,12 +11,11 @@ for Shibboleth Identity Provider. It makes the IdP a Credential Issuer. A wallet
token from the IdP and exchanges it for a Verifiable Credential built of IdP attributes.
We target first the [Decentralized Identity Interop Profile 5](https://fidescommunity.github.io/DIIP/).
-Many of the requirements for trust establishment and for the credential formats come from
-there. We have implemented also bits of
-[OpenID4VC High Assurance Interoperability Profile 1.0](https://openid.net/specs/openid4vc-high-assurance-interoperability-profile-1_0-final.html),
-DPoP for instance, but that profile has not been our focus.
+Lately we have worked also on
+[OpenID4VC High Assurance Interoperability Profile 1.0](https://openid.net/specs/openid4vc-high-assurance-interoperability-profile-1_0-final.html).
+What is supported and what not is listed per specification in [Capabilities](#capabilities).
-Both issuance flows of the specification are implemented:
+Both issuance flows are implemented:
- the [Pre-Authorized Code Flow](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-pre-authorized-code-flow),
where something in your deployment creates a Credential Offer for a user that has already
@@ -31,9 +30,99 @@ enveloped in SD-JWT). Both do selective disclosure. Revocation is published as
Things you need to know about this add-on:
-- It is not certified against any conformance suite. Do not deploy it in production.
+- It is not certified. Do not deploy it in production.
- Credentials are signed with EC keys only. The signing actions do not support anything else.
+## Capabilities
+
+Issuer side only. **Yes** is built and tested with some wallet, **Partial** is built with
+limitations, **No** is not built and **No, by design** is left out on purpose.
+
+### [OpenID for Verifiable Credential Issuance 1.0](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html)
+
+| Feature | Status |
+|---|---|
+| Pre-Authorized Code Flow | Yes |
+| Transaction Code, `tx_code` | Yes |
+| Authorization Code Flow | Yes |
+| Credential Offer by value | Yes |
+| Credential Offer by reference, `credential_offer_uri` | No |
+| `issuer_state` | No, by design |
+| Separate Authorization Server | No, by design |
+| Authorization request with `scope` | Yes |
+| Authorization request with `authorization_details` | Partial |
+| PAR and PKCE | Yes |
+| Token endpoint, both grants | Yes |
+| Refresh tokens | No |
+| Client authentication | Yes |
+| DPoP | Yes |
+| Nonce endpoint | Yes |
+| Credential request by `credential_configuration_id` or `credential_identifier` | Yes |
+| Proof type `jwt` | Yes |
+| Key attestation in a `jwt` proof | Yes |
+| Proof types `di_vp` and `attestation` | No |
+| Batch issuance | Yes |
+| Credential Response encryption | No |
+| Credential Request encryption | No |
+| Deferred Credential Endpoint | No |
+| Notification Endpoint | No |
+| Credential and token error responses | Yes |
+| Credential Issuer metadata | Yes |
+| Signed Credential Issuer metadata | Yes |
+| Localised metadata by `Accept-Language` | No |
+| Wallet Attestation, `attest_jwt_client_auth` | Yes |
+| Format `dc+sd-jwt` | Yes |
+| Format `vc+sd-jwt` | Yes |
+| Formats `jwt_vc_json`, `jwt_vc_json-ld`, `ldp_vc`, `mso_mdoc` | No |
+| Holder binding by `did:jwk` and `jwk` | Yes |
+| Holder binding by `did:web` | No |
+
+### [Decentralized Identity Interop Profile 5](https://fidescommunity.github.io/DIIP/)
+
+| Requirement | Status |
+|---|---|
+| SD-JWT VC | Yes |
+| W3C VCDM 2.0 secured with SD-JWT | Partial |
+| ES256 | Yes |
+| Issuer identifier `did:jwk` | Yes |
+| Issuer identifier `did:web` | Yes |
+| Holder identifier `did:jwk` | Yes |
+| Holder identifier `did:web` | No |
+| Pre-Authorized Code Flow and Authorization Code Flow | Yes |
+| `tx_code` | Yes |
+| PKCE `S256` and PAR | Yes |
+| Issuer initiated, same device and cross device | Yes |
+| Immediate issuance | Yes |
+| `scope` | Yes |
+| `authorization_details` | Partial |
+| Proof type `jwt` | Yes |
+| `cnf` as DID URL | Yes |
+| Validity, `nbf` `exp` `validFrom` `validUntil` | Yes |
+| Token Status List | Yes |
+| OpenID Federation, Entity Configuration, `fed` and `termsOfUse` | Yes |
+
+### [OpenID4VC High Assurance Interoperability Profile 1.0](https://openid.net/specs/openid4vc-high-assurance-interoperability-profile-1_0-final.html)
+
+| Feature | Status |
+|---|---|
+| `dc+sd-jwt` | Yes |
+| Issuer key as `x5c` chain in the credential | Yes |
+| `x5c` in Status List Token | Yes |
+| `x5c` in signed Credential Issuer metadata | Yes |
+| ES256 | Yes |
+| Authorization Code Flow with PAR and PKCE `S256` | Yes |
+| `iss` in authorization response, RFC 9207 | Yes |
+| DPoP | Yes |
+| Wallet Attestation at PAR and token endpoint | Yes |
+| Key attestation, `key_attestations_required` | Yes |
+| Proof type `attestation` | No |
+| Holder key in `cnf` `jwk` member | Partial |
+| Pre-Authorized Code Flow | Yes |
+| Same device and cross device offers | Partial |
+| Batch issuance advertised by `batch_credential_issuance` | Yes |
+| Unique, unpredictable status list index | Yes |
+| Refresh tokens, RECOMMENDED | No |
+
## Plugin Installation
> **Info**
@@ -64,11 +153,9 @@ Requires IdP **5.2.3** or later.
> of the Shibboleth wiki. It has the snapshot locations and the commands for all five. Follow
> that page and you have everything this plugin needs.
>
-> The two OpenID Federation plugins of the list are needed only if you import
-> *conf/openid-vci-oidfed.xml* into *conf/global.xml* to state this deployment as a Credential
-> Issuer in an Entity Configuration. Install them before that import, without them the IdP does
-> not start.
-
+> The two OpenID Federation plugins of the list are needed only for
+> [Step 4](#step-4-make-the-url-credential-an-openid-federation-credential), where
+> *conf/openid-vci-oidfed.xml* is imported. Without them the IdP does not start with that import.
### Installation example
@@ -112,7 +199,7 @@ Installing the plugin activates module `OpenID-VCI` and creates six files:
| File | Description |
|---|---|
| *conf/openid-vci.properties* | Every deployer-facing property.|
-| *conf/openid-vci-credentials.xml* | Which key signs credentials, Status List Tokens and issuer metadata. Inactive by default. |
+| *conf/openid-vci-credentials.xml* | Which key signs credentials, Status List Tokens and issuer metadata. Not imported by default. |
| *conf/openid-vci-oidfed.xml* | States this deployment as a Credential Issuer in OpenID Federation. Not imported by default. |
| *static/openid-credential-issuer.json* | Credential Issuer metadata, as a Velocity template. |
| *static/jwt-vc-issuer.json* | Keys that sign the credentials, for a verifier. As a Velocity template. |
@@ -123,27 +210,41 @@ Configurations. There is no template for it. See [Credential Configurations](#cr
## First installation, by example
-This chapter goes through one working (not guaranteed) deployment from start to end. First a credential is
-issued to a wallet with the pre-authorized code flow, then the same credential with the
-authorization code flow and finally the credential is withdrawn with the Status List. The
-examples are from a running test deployment so they fit together.
+This chapter builds one deployment in steps. Every step adds something to the previous one and
+tells only what is new. The examples are from a running test deployment so they fit together,
+but it is not guaranteed that they work for you as such.
+
+1. A did:jwk credential with both flows and the Status List. Signed with the key of the OP.
+2. A second credential, did:web.
+3. A third credential, the issuer is the plain url.
+4. The third credential is made an OpenID Federation credential. Own signing key is taken into use.
+5. A fourth credential, for a HAIP wallet. Signing key gets a certificate chain. The wallet is
+ the OpenID Foundation conformance suite.
+6. Rolling the signing key to the next one.
+
+For steps 1 to 4 the wallet is [Paradym](https://paradym.id). At the end the installation
+releases did:jwk, did:web, plain url, OpenID Federation and HAIP credentials.
`https://issuer.example.org` is your IdP's OIDC issuer everywhere below.
-### Credential Configurations
+### Step 1: did:jwk credential, both flows
+
+First credential is named by `did:jwk`, which is the default. The issuer identifier is the key
+itself and it travels in the credential, so there is nothing to publish for a verifier. The
+credential is signed with the EC key of the OP, *credentials/idp-signing-es.jwk* of a normal
+installation, and nothing about keys needs to be configured yet.
+
+#### Credential Configurations
**File(s):** *metadata/verifiable-credentials.json*
**Format:** JSON
-This one file has two jobs. It is published as `credential_configurations_supported` in your
-Credential Issuer metadata and it is also the instruction on how each credential is built. What
-you advertise is exactly what you are able to issue.
+The file is published as `credential_configurations_supported` of your Credential Issuer
+metadata, and the plugin builds the credentials by it.
The content of the file is defined in
[Credential Issuer Metadata](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-credential-issuer-metadata)
-of the specification, not by this plugin, and it is defined per credential type. Which members
-the two types of this plugin need is in the table below. What follows does not repeat the
-specification, only the members that need a word of their own are mentioned.
+of the specification, per credential format. Below only the members that need explaining.
<details>
<summary>Example contents of metadata/verifiable-credentials.json</summary>
@@ -222,14 +323,13 @@ specification, only the members that need a word of their own are mentioned.
</details>
-The configuration id, `GeantIncubatorDiploma_SDJWT` here, is the most important string of the whole
-setup. A wallet names it in a credential request, the `scope` value matches it and the claim
-values in the access token are keyed with it. Pick it once and use it exactly the same
-everywhere.
+The configuration id, `GeantIncubatorDiploma_SDJWT` here, is used everywhere. A wallet names it
+in a credential request, it is the `scope` value and the claim values in the access token are
+keyed with it. Use exactly the same string in all places.
The example has the same credential in both formats, the one that is `dc+sd-jwt` and the one
-that is `vc+sd-jwt`. You do not need both, the rest of this document keeps them both around to
-show where the formats differ.
+that is `vc+sd-jwt`. You do not need both, this step keeps them both around to show where the
+formats differ. The later steps add `dc+sd-jwt` credentials only.
The `@context` of a `vc+sd-jwt` credential has to resolve. The first entry is the W3C one and
the second one is yours, you publish it yourself as a static file. The terms of your claims are
@@ -253,19 +353,13 @@ in it:
</details>
-You publish it in the url the `@context` names. For this example deployment it is a static file of Jetty
-and included jetty rewrite example in in [Discovery documents](#discovery-documents).
-
-If this feels like a lot of trouble, it is not needed at all when you do not issue the
-`vc+sd-jwt` credential. Leave the configuration of that credential out and the file goes with
-it.
+You publish it in the url the `@context` names, see [Metadata endpoints](#metadata-endpoints)
+below. Without `vc+sd-jwt` credentials the file is not needed.
#### Settings of your own, per credential
-Everything above is published exactly as you write it. The `_issuance_configuration` member is
-not. It states how long a credential is valid, what it says issued it and whether it goes on a
-Status List, and it is dropped from the published `credential_configurations_supported`, so a
-wallet never sees it.
+Member `_issuance_configuration` is not published. It states how long a credential is valid,
+what it says issued it and whether it goes on a Status List.
It goes inside the credential's own configuration, next to `format` and `vct`:
@@ -278,8 +372,7 @@ It goes inside the credential's own configuration, next to `format` and `vct`:
"_issuance_configuration": {
"lifetime": "P365D",
"not_before": "2026-08-01T06:00Z",
- "status_list": true,
- "issuer": "did:web"
+ "status_list": true
},
"credential_metadata": { "...": "..." }
}
@@ -292,17 +385,15 @@ It goes inside the credential's own configuration, next to `format` and `vct`:
| `lifetime` | Duration | `credentialLifetime` of the profile configuration, `P180D` | Length of the validity window. |
| `not_before` | Duration, moment or date | The instant of issuance | Where the validity window opens. |
| `status_list` | Boolean | `openidvci.status-list.includeByDefault`, `true` | Whether the credential takes a Status List slot. |
-| `oidfed` | Boolean | `true` | Whether the credential states the Entity Identifier of this deployment in OpenID Federation. |
-| `issuer` | String | `openidvci.issuer` | What the credential says issued it, overriding the deployment. `did:jwk`, `did:web` and `url` are the three identifiers of [Credential Issuer identifiers](#credential-issuer-identifiers). Stating nothing takes whatever `openidvci.issuer` says. |
+| `issuer` | String | `openidvci.issuer` | What the credential says issued it. `did:jwk`, `did:web` or `url`. See [Step 2](#step-2-add-a-didweb-credential) and [Step 3](#step-3-add-a-plain-url-credential). |
+| `oidfed` | Boolean | `true` | Whether the credential states the Entity Identifier of this deployment, once the deployment is in OpenID Federation. See [Step 4](#step-4-make-the-url-credential-an-openid-federation-credential). |
-All three are optional and so is the member itself. A credential that states nothing behaves
-exactly as it did before you knew this member existed.
+All of them are optional and so is the member itself.
##### The validity window
-**The window is `not_before` until `not_before` plus `lifetime`.** The lifetime is the length of
-the window, not a distance from the moment of issuance. With no `not_before` the window opens at
-issuance.
+The validity window is from `not_before` to `not_before` plus `lifetime`. With no `not_before`
+the window opens at issuance.
Write `not_before` as one of these:
@@ -334,39 +425,10 @@ Rounding moves the window earlier, never later, so the expiration still chooses
shortest `lifetime` you issue. A longer one closes the window before the credential is issued and
nothing is issued.
-##### Whether a credential states this deployment in OpenID Federation
-
-Importing *conf/openid-vci-oidfed.xml* into *conf/global.xml* is what turns this on. That one
-import states this deployment as a Credential Issuer in its Entity Configuration and makes its
-credentials name the same identifier.
-
-
-```json
-{
- "fed": "https://issuer.example.org"
-}
-```
-
-```json
-{
- "termsOfUse": {
- "type": "OpenIDFederation",
- "policyId": "https://issuer.example.org"
- }
-}
-```
-`"oidfed": false` in Credential Configuration can be used to exclude those claims
-from particulrt credential i.e. leave it out from federation.
-
-The federation module decides the `iss` and `sub` of your Entity Configuration, from
-`idp.oidfed.entityID`, and nothing joins that up with `credential_issuer`. It is your
-responsibility to define matching values.
-
##### Whether a credential goes on a Status List
-`"status_list": false` keeps the credential off the list. It carries no `status` claim, it takes
-no slot, and there is nothing about it to revoke later. A credential too short lived to bother
-revoking is the usual reason to do this.
+`"status_list": false` keeps the credential off the list. It carries no `status` claim and it
+cannot be revoked. Use it for short lived credentials.
A credential that states nothing goes on the list. Set
`openidvci.status-list.includeByDefault` to `false` in *conf/openid-vci.properties* to turn that
@@ -378,108 +440,105 @@ round and make the Status List something each credential opts into with `"status
`credentialLifetime` you set on one wallet in *conf/relying-party.xml*. Leave `lifetime` out of
a credential whose validity you want to keep steering per wallet.
-### Signing keys
-
-**File(s):** *conf/openid-vci-credentials.xml*, *conf/openid-vci.properties*
-**Format:** Native Spring, Spring Properties
-
-Nothing here is active by default. Credentials are signed with the OP's own response signing
-credentials and for a first installation you may leave it as it is. That works for as long as
-the OP has an EC key, `credentials/idp-signing-es.jwk` of a normal installation. The signing of
-this plugin does ES256, ES384 and ES512 only, so the RSA key of the OP is of no use here.
-
-If you do take a key of your own into use, defining **openidvci.SigningCredentials**
-*replaces* the OP's credentials instead of adding to them. Nothing else changes while the
-Credential Issuer identifier is `did:jwk`, which is the default. The key travels inside the
-credential itself and there is nothing to publish.
-
-TODO: the numerous options how to manage keys and which identifier type expects what.
-
+#### Metadata endpoints
-### Credential Issuer identifiers
-
-**File(s):** *conf/openid-vci.properties*, *conf/relying-party.xml*
-**Format:** Spring Properties, Native Spring
+**File(s):** *conf/relying-party.xml*, *etc/jetty-rewrite-rules.xml*, *static/oauth-authorization-server.json*
+**Format:** Native Spring, Jetty XML, JSON
-One property decides what an issued credential says issued it. `openidvci.issuer` takes three
-values, and each one decides both the `iss` of the credential and what this deployment has to
-publish for a verifier to check the signature. The same value decides the `iss` of a Status List
-Token, so the two always agree.
+A wallet reads the Credential Issuer metadata and the authorization server metadata, and asks
+for a nonce. A verifier reads the Status List. None of them is a registered client, so these
+endpoints go to the anonymous relying party. Both flows below need it:
-| `openidvci.issuer` | What a credential says issued it | What you publish |
-|---|---|---|
-| empty or `did:jwk`, the default | the did:jwk of the signing key | nothing |
-| `did:web` | the did:web of this deployment | the did:web document, at `/.well-known/did.json` |
-| `url` | the issuer of the OP | JWT VC issuer metadata, at `/.well-known/jwt-vc-issuer` |
+<details>
+<summary>Example snippet of conf/relying-party.xml</summary>
+```xml
+...
+ <bean id="shibboleth.UnverifiedRelyingParty" parent="RelyingParty">
+ <property name="profileConfigurations">
+ <list>
+ <!-- The wallet reads the Credential Issuer metadata -->
+ <ref bean="OpenID.VCI.IssuerMetadata" />
+ <!-- The wallet finds the token endpoint through this one -->
+ <ref bean="OAuth.AuthorizationServer" />
+ <!-- The wallet asks for the nonce its key proof has to carry -->
+ <ref bean="OpenID.VCI.Nonce" />
+ <!-- A verifier reads the Status List -->
+ <ref bean="OAuth.StatusList" />
+ <!-- Of the OP plugin. Publishes the keys of the OP as a keyset -->
+ <ref bean="OIDC.Keyset" />
+ <!-- Of the OP plugin. Serves /.well-known/openid-configuration -->
+ <ref bean="OIDC.Configuration" />
+ <!-- Of the OP plugin. Needed for the audience values of the wallet -->
+ <ref bean="OAUTH2.TokenAudience" />
+ </list>
+ </property>
+ </bean>
+...
+```
-A Credential Configuration is able to state an `issuer` of its own, with the same three values,
-and it overrides the deployment. See
-[Settings of your own, per credential](#settings-of-your-own-per-credential).
+</details>
-**DID.Document** has to be in *conf/relying-party.xml* for the did:web document to be served,
-**OAuth.JWTVCIssuer** for the JWT VC issuer metadata. See
-[Relying party configuration](#relying-party-configuration).
+The templates of *static/* name your host already, they are evaluated against the issuer of the
+OP. One thing you need to add is the credential scopes in `scopes_supported` of
+*static/oauth-authorization-server.json*, a wallet of the code flow asks a credential by scope:
-#### Where a verifier finds the key
+```json
+ "scopes_supported":[
+ "openid",
+ "GeantIncubatorDiploma_SDJWT",
+ "GeantIncubatorDiploma_W3C"
+ ],
+```
-| `openidvci.issuer` | Where the key is |
-|---|---|
-| `did:jwk` | inside the credential, nothing is published |
-| `did:web` | `/.well-known/did.json`, formed from **openidvci.SigningCredentials** and **openidvci.status-list.SigningCredentials** as it is read |
-| `url` | `/.well-known/jwt-vc-issuer` and the `jwks_uri` it names, a template in *static/*, keys from **shibboleth.oidc.SigningCredentialsToPublish** |
+The `/.well-known/` locations are not IdP paths, you wire them in Jetty. For this step you need
+these rules in *etc/jetty-rewrite-rules.xml*. The later steps add one rule each and the whole
+file is in [Discovery documents](#discovery-documents).
<details>
-<summary>Example of the document at /.well-known/did.json</summary>
+<summary>Example snippet of etc/jetty-rewrite-rules.xml</summary>
-```json
-{
- "@context": [
- "https://www.w3.org/ns/did/v1",
- "https://w3id.org/security/suites/jws-2020/v1"
- ],
- "id": "did:web:issuer.example.org",
- "verificationMethod": [
- {
- "id": "did:web:issuer.example.org#signingvci",
- "type": "JsonWebKey2020",
- "controller": "did:web:issuer.example.org",
- "publicKeyJwk": {
- "kty": "EC",
- "crv": "P-256",
- "kid": "signingvci",
- "x": "qQ9ESeIrQ36JijWM-8xdcjXwY46RW3p9YDtP0MVaLnE",
- "y": "5yVZRqwp1tde_CnKC662wW-XFQhEOrGAPi0LTG4OpI8"
- }
- }
- ],
- "assertionMethod": [
- "did:web:issuer.example.org#signingvci"
- ]
-}
+```xml
+ <Call name="addRule">
+ <Arg>
+ <New class="org.eclipse.jetty.rewrite.handler.RewritePatternRule">
+ <Set name="pattern">/.well-known/openid-credential-issuer</Set>
+ <Set name="replacement">/idp/profile/openid/vci/issuer-metadata</Set>
+ </New>
+ </Arg>
+ </Call>
+ <Call name="addRule">
+ <Arg>
+ <New class="org.eclipse.jetty.rewrite.handler.RewritePatternRule">
+ <Set name="pattern">/.well-known/oauth-authorization-server</Set>
+ <Set name="replacement">/idp/profile/oauth/authorization-server</Set>
+ </New>
+ </Arg>
+ </Call>
+ <Call name="addRule">
+ <Arg>
+ <New class="org.eclipse.jetty.rewrite.handler.RewritePatternRule">
+ <Set name="pattern">/.well-known/openid-configuration</Set>
+ <Set name="replacement">/idp/profile/oidc/configuration</Set>
+ </New>
+ </Arg>
+ </Call>
+ <Call name="addRule">
+ <Arg>
+ <New class="org.eclipse.jetty.rewrite.handler.RewritePatternRule">
+ <Set name="pattern">/credentials/GeantIncubatorDiploma_W3C</Set>
+ <Set name="replacement">/credentials/GeantIncubatorDiploma_W3C.json</Set>
+ </New>
+ </Arg>
+ </Call>
```
-Both signing credential lists are stated, a key that appears in both only once.
-
</details>
-#### Status Lists
-
-TODO: Explain the relation between Status Lists, Identifiers and Signing keys.
+#### Pre-authorized code flow
-#### OpenID Federation
-
-`openidvci.issuer` does not affect the federation identity. A credential carries the Entity
-Identifier in `fed`, or in `termsOfUse` when it is a `vc+sd-jwt` one, whatever the identifier is.
-
-`credential_issuer` of your Credential Issuer metadata is an https url in every case, and the
-federation profile requires it to equal your Entity Identifier. See
-[Whether a credential names this deployment in a federation](#whether-a-credential-names-this-deployment-in-a-federation).
-
-### Pre-authorized code flow
-
-**File(s):** *metadata/oidc-client.json*, *conf/relying-party.xml*, *conf/openid-vci.properties*
-**Format:** JSON, Native Spring, Spring Properties
+**File(s):** *metadata/oidc-client.json*, *conf/relying-party.xml*
+**Format:** JSON, Native Spring
In this flow something in your deployment, a portal page, a registration system or a script,
authenticates the user and calls the credential-offer endpoint for the user. The offer is then
@@ -509,40 +568,18 @@ different identity. You register both:
</details>
-The `/wallet` suffix is not a convention you may choose. A wallet that redeems a pre-authorized
+The `/wallet` suffix is fixed. A wallet that redeems a pre-authorized
code does not authenticate, so the plugin resolves its client id by taking the client id of the
creator of the offer and appending `/wallet` to it.
-*conf/relying-party.xml* then needs three relying parties. Unverified endpoints for mostly issuer
-related metadata publications, one for the party that creates the content for credential and one
-for the wallet that redeems the code:
+Then one relying party for the party that creates the content for credential and one for the
+wallet that redeems the code:
<details>
<summary>Example snippet of conf/relying-party.xml</summary>
```xml
...
- <bean id="shibboleth.UnverifiedRelyingParty" parent="RelyingParty">
- <property name="profileConfigurations">
- <list>
- <!-- The wallet reads the Credential Issuer metadata -->
- <ref bean="OpenID.VCI.IssuerMetadata" />
- <!-- The wallet finds the token endpoint through this one -->
- <ref bean="OAuth.AuthorizationServer" />
- <!-- The wallet asks for the nonce its key proof has to carry -->
- <ref bean="OpenID.VCI.Nonce" />
- <!-- A verifier reads the Status List -->
- <ref bean="OAuth.StatusList" />
- <!-- Of the OP plugin. Publishes the keys of the OP as a keyset -->
- <ref bean="OIDC.Keyset" />
- <!-- Of the OP plugin. Serves /.well-known/openid-configuration -->
- <ref bean="OIDC.Configuration" />
- <!-- Of the OP plugin. Needed for the audience values of the wallet -->
- <ref bean="OAUTH2.TokenAudience" />
- </list>
- </property>
- </bean>
-
<bean parent="RelyingPartyByName" c:relyingPartyIds="offer-caller">
<property name="profileConfigurations">
<list>
@@ -571,12 +608,6 @@ for the wallet that redeems the code:
</details>
-The last three beans of the anonymous relying party come with the OP plugin. Without
-`OIDC.Keyset` nothing serves the `jwks_uri` that *static/jwt-vc-issuer.json* names, without
-`OIDC.Configuration` there is no `/.well-known/openid-configuration` for a wallet to read and
-without `OAUTH2.TokenAudience` the `audience` values of a wallet client are not honoured. The
-same relying party is needed by the authorization code flow.
-
The offer request is a `POST` of `application/json` to
`/idp/profile/openid/vci/credential-offer` authenticated as `offer-caller`. Claim values are
given literally and keyed with the credential configuration id:
@@ -626,7 +657,7 @@ openid-credential-offer://?credential_offer=%7B%22credential_issuer%22:%22https:
</details>
-The offer in example asks for both credentials at once and the wallet then may pick one or the other.
+The offer in example asks for both credentials at once and the wallet then may pick one or the other.
Do not really do that. A wallet that is offered two credentials in one offer most likely fails miserably
with it, so keep to one credential per offer. The two are here only to remind us it should be possible.
@@ -637,8 +668,7 @@ Every configuration id of the body must exist in *metadata/verifiable-credential
offer is rejected. There is no attribute resolution when the wallet later redeems the code. The
values above are what is issued.
-
-### Authorization code flow
+#### Authorization code flow
**File(s):** *metadata/oidc-client.json*, *conf/relying-party.xml*, *conf/attribute-resolver.xml*, *conf/attribute-filter.xml*, *conf/oidc.properties*
**Format:** JSON, Native Spring, Property files
@@ -649,29 +679,25 @@ a normal authorization request, the user authenticates at the IdP as usual and t
content comes from an IdP attribute that is resolved for the user that just logged in.
Nothing in the offer is per user here, so you write it by hand. The credential offer endpoint
-does not produce this one: with no claim values to seal and no state to carry, the response
-would be the same bytes for every call. The same url serves everybody and you may print it
-once as a QR code:
+does not produce this one. The same url serves everybody and you may print it once as a QR
+code:
```
openid-credential-offer://?credential_offer=%7B%22credential_issuer%22:%22https://issuer.example.org%22,%22credential_configuration_ids%22:%5B%22GeantIncubatorDiploma_SDJWT%22%5D,%22grants%22:%7B%22authorization_code%22:%7B%7D%7D%7D
```
-The `authorization_code` grant is what tells the wallet to run this flow. It is empty;
-`issuer_state` is not supported. What is issued comes from the attribute below, not from the
-offer.
-
-The `credential_issuer` value has to be the Credential Issuer Identifier this deployment
-publishes, the same string as in *openid-credential-issuer.json*.
+The `authorization_code` grant is what tells the wallet to run this flow. It is empty,
+`issuer_state` is not supported. The `credential_issuer` value has to be the Credential Issuer
+Identifier this deployment publishes, the same string as in *openid-credential-issuer.json*.
The wallet is a normal OIDC client of one identity, there is no second one as in the
-pre-authorized code flow. You register it with the client metadata resolver of the OP plugin:
+pre-authorized code flow. You register it with the client metadata resolver of the OP plugin,
+in the same *metadata/oidc-client.json* as the clients above:
<details>
-<summary>Example contents of metadata/oidc-client.json</summary>
+<summary>Example snippet of metadata/oidc-client.json</summary>
```json
-[
{
"client_id": "id.animo.paradym",
"client_name": "Paradym Wallet",
@@ -688,49 +714,22 @@ pre-authorized code flow. You register it with the client metadata resolver of t
"urn:ietf:params:oauth:grant-type:pre-authorized_code"
]
}
-]
```
</details>
-*metadata/oidc-client.json* is one array of clients, so a deployment that does both flows has
-the clients of both in the same file.
-
-Note the `scope` value. A wallet of the code flow asks for a credential by naming the
-credential configuration id as a scope, so every configuration id you want to be requestable
-that way needs to be here. `grant_types` has the pre-authorized code grant as well, as the same
-wallet is usually also the one that redeems an offer.
+Note the `scope` value. Every configuration id you want to be requestable by scope needs to be
+here. `grant_types` has the pre-authorized code grant as well, as the same wallet is usually
+also the one that redeems an offer.
The relying party override needs four profiles, authorize, token, credential and pushed
-authorization request. The anonymous relying party is the same one as in the pre-authorized
-code flow:
+authorization request:
<details>
<summary>Example snippet of conf/relying-party.xml</summary>
```xml
...
- <bean id="shibboleth.UnverifiedRelyingParty" parent="RelyingParty">
- <property name="profileConfigurations">
- <list>
- <!-- The wallet reads the Credential Issuer metadata -->
- <ref bean="OpenID.VCI.IssuerMetadata" />
- <!-- The wallet finds the token endpoint through this one -->
- <ref bean="OAuth.AuthorizationServer" />
- <!-- The wallet asks for the nonce its key proof has to carry -->
- <ref bean="OpenID.VCI.Nonce" />
- <!-- A verifier reads the Status List -->
- <ref bean="OAuth.StatusList" />
- <!-- Of the OP plugin. Publishes the keys of the OP as a keyset -->
- <ref bean="OIDC.Keyset" />
- <!-- Of the OP plugin. Serves /.well-known/openid-configuration -->
- <ref bean="OIDC.Configuration" />
- <!-- Of the OP plugin. Needed for the audience values of the wallet -->
- <ref bean="OAUTH2.TokenAudience" />
- </list>
- </property>
- </bean>
-
<util:list id="shibboleth.RelyingPartyOverrides">
<bean parent="RelyingPartyByName"
@@ -790,10 +789,8 @@ read the real attributes of the user:
One attribute serves both credentials. Every `AttributeEncoder` of the definition becomes a
claim of its own name, so the same claim list is found with either configuration id and the
-script is written once. The definition itself is of neither type, so it is named
-`GeantIncubatorDiploma` without a type. Mind which name goes where: the encoders name the
-credentials and everything else, `idp.oidc.encodedAttributes` and the filter policy, names the
-attribute.
+script is written once. Mind which name goes where: the encoders name the credentials and
+everything else, `idp.oidc.encodedAttributes` and the filter policy, names the attribute.
Then you tell the OP plugin to embed the attribute into the access token in
*conf/oidc.properties*:
@@ -821,8 +818,7 @@ And you release it to the wallet in *conf/attribute-filter.xml*. This flow reads
</details>
-
-### Status List
+#### Status List
**File(s):** *conf/relying-party.xml*, *conf/openid-vci.properties*
**Format:** Native Spring, Spring Properties
@@ -830,10 +826,7 @@ And you release it to the wallet in *conf/attribute-filter.xml*. This flow reads
Credentials this plugin issues carry a `status` claim that names one slot of a Status List, as
in [draft-ietf-oauth-status-list](https://datatracker.ietf.org/doc/draft-ietf-oauth-status-list/).
The credential endpoint allocates a slot for every credential it signs, unless the credential
-is told not to take one. The Status List is part of a working installation rather than an extra
-you opt into. Turning it off for one credential, or making it something each credential opts
-into, is in
-[Whether a credential goes on a Status List](#whether-a-credential-goes-on-a-status-list).
+is told not to take one.
<details>
<summary>Example of the status claim of an issued credential</summary>
@@ -843,7 +836,7 @@ into, is in
"status": {
"status_list": {
"idx": 41337,
- "uri": "https://issuer.example.org/idp/profile/oauth/status-list/list?id=2027-03"
+ "uri": "https://issuer.example.org/idp/profile/oauth/status-list/list?id=2027-03~did-jwk:NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs"
}
}
}
@@ -857,11 +850,16 @@ default `credentialLifetime` of `P180D` a credential that is issued in September
the list of March 2027. As we need to keep track of credentials and revocation we place them
to buckets per expiration.
+The part after `~` tells under which issuer identifier the credential was issued. A Status List
+Token has to name the same issuer as the credential. For `did:jwk` the part is the thumbprint
+of the signing key, as the identifier is the key. For `did:web` it is `did-web` and for the plain
+url `url`. It is still one list per month, whatever the identifier.
+
The expiration is the one the credential actually gets, so a credential with a `lifetime` or a
`not_before` of its own lands in the list its own window chooses. A `not_before` fixed to a date
puts every holder of that credential in one list.
-Three endpoints do the work and they have three different audiences:
+The Status List endpoints:
| Endpoint | Audience | Authentication | Request |
|---|---|---|---|
@@ -869,10 +867,10 @@ Three endpoints do the work and they have three different audiences:
| `/idp/profile/oauth/status-list/assignments` | your own systems | client | `POST` of JSON |
| `/idp/profile/oauth/status-list/revoke` | your own systems | client | `POST` of JSON |
-You enable them as in [Reference](#reference). See **OAuth.StatusList**, **OAuth.StatusList.Revoke**
-and **OAuth.StatusList.Assignments**.
+All three are already enabled in the relying parties above, the first one in the anonymous one
+and the other two for `offer-caller`.
-One property you need to change before you issue anything:
+One property you need to change before you issue anything, in *conf/openid-vci.properties*:
```properties
# Not the shipped value, and not changed afterwards
@@ -884,11 +882,7 @@ were issued cannot be read from their `idx`. The allocator does not initialise w
and it warns for as long as the shipped placeholder is in place. If you change the key later,
slots that are already used are handed out again.
-Status List Tokens are signed with the OP's own response signing credentials until you define
-**openidvci.status-list.SigningCredentials** in *conf/openid-vci-credentials.xml*. Same choice
-and same consequences as in [Signing keys](#signing-keys) above.
-
-#### Finding the slot of a credential
+##### Finding the slot of a credential
A Status List has bits and nothing else in it, so the mapping from a credential to its slot is
in the assignment records. The credential endpoint writes one record per issuance that took
@@ -945,7 +939,12 @@ end, so `next == high_water` means you are up to date. Gaps in `seq` are normal.
the expiration of the credential that took the slots, which is also when revoking them stops
mattering.
-#### Revoking a credential
+Assignment records expire `openidvci.status-list.assignment.retention` (`P7D`) after they are
+**written**, not after the credential expires. They are the only record of which slot a
+credential got, and without it the credential cannot be revoked. Collect the records regularly,
+more often than the retention.
+
+##### Revoking a credential
<details>
<summary>Example of revoking one slot</summary>
@@ -964,15 +963,15 @@ Cache-Control: no-store
</details>
-The `id` is the one in the credential's `status_list` url. When credentials of more than one
-Credential Issuer identifier are issued, that url carries which identifier the credential was
-issued under and the id you copy from it carries the same. Either form is accepted here, a slot
+The `id` is the month. You may give it also with the `~` part copied from the credential, a slot
belongs to the month whatever the credential says issued it.
Revoking is idempotent and it goes one way only. Nothing in the plugin turns a slot back to
-valid.
+valid. The revoke endpoint does **not** check that the caller issued the credential that holds
+the slot. Any client that has **OAuth.StatusList.Revoke** enabled may revoke any slot of any
+list, so enable it in the relying party that owns the issuance and nowhere else.
-#### Reading the list back
+##### Reading the list back
The published list is a signed JWT of media type `application/statuslist+jwt`. You fetch it
with the `uri` of the credential and it needs no authentication:
@@ -981,7 +980,7 @@ with the `uri` of the credential and it needs no authentication:
<summary>Example of fetching a Status List Token</summary>
```bash
-curl -s 'https://issuer.example.org/idp/profile/oauth/status-list/list?id=2027-03'
+curl -s 'https://issuer.example.org/idp/profile/oauth/status-list/list?id=2027-03~did-jwk:NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs'
```
Its claims, `lst` being the compressed list itself:
@@ -989,7 +988,7 @@ Its claims, `lst` being the compressed list itself:
```json
{
"iss": "did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2Iiwi...",
- "sub": "https://issuer.example.org/idp/profile/oauth/status-list/list?id=2027-03",
+ "sub": "https://issuer.example.org/idp/profile/oauth/status-list/list?id=2027-03~did-jwk:NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs",
"iat": 1788772320,
"exp": 1806537600,
"ttl": 3600,
@@ -1002,1127 +1001,1152 @@ Its claims, `lst` being the compressed list itself:
</details>
-#### Notes
-
-The revoke endpoint checks that the caller authenticated and that the slot is inside a list. It
-does **not** check that the caller issued the credential that holds the slot. Any client that
-has **OAuth.StatusList.Revoke** enabled may revoke any slot of any list, so enable it in the
-relying party that owns the issuance and nowhere else.
-
-Assignment records expire `openidvci.status-list.assignment.retention` (`P7D`) after they are
-**written**, not after the credential expires. They are the only record of which slot a
-credential got. A record you did not collect within that week is a credential you are not able
-to revoke anymore. Poll the endpoint on a schedule that is shorter than the retention, not when
-you happen to need a record.
-
A list is published until its month is over. By then every credential in it has expired. Asking
for a month that has ended is `410 retired_status_list` and asking for something that is not a
month at all is `404 unknown_status_list`, so a verifier is able to tell a withdrawn list from
a wrong URL.
-## Two more installations
+### Step 2: Add a did:web credential
-The installation above is a DIIP deployment, issuing `GeantIncubatorDiploma_SDJWT` and
-`GeantIncubatorDiploma_W3C` with both flows. These two carry it further, and state only what
-changes.
+Next we add a credential that is named by the `did:web` of this deployment,
+`did:web:issuer.example.org`. The identifier is derived from the issuer of the OP. Unlike
+did:jwk it stays the same when the signing key changes, which matters in
+[Step 6](#step-6-rolling-the-signing-key).
-### The deployment in an OpenID Federation
+What is new:
-The federation itself is configured in the plugins of [Dependencies](#dependencies), see
-[OIDFEDCommon](https://shibboleth.atlassian.net/wiki/spaces/IDPPLUGINS/pages/5601296385/OIDFEDCommon)
-of the Shibboleth wiki.
+- the credential states `"issuer": "did:web"` in its `_issuance_configuration`
+- the did:web document is published at `/.well-known/did.json`
+- the credential header `kid` is a DID URL, `did:web:issuer.example.org#<kid of the key>`
+- the Status List url of the credential ends with `~did-web`
-To state this deployment as a Credential Issuer of the federation, import the file in
-*conf/global.xml*:
+The credential goes next to the two of Step 1 in *metadata/verifiable-credentials.json*:
-```xml
-<import resource="openid-vci-oidfed.xml"/>
-```
+<details>
+<summary>Example snippet of metadata/verifiable-credentials.json</summary>
-- **openidvci.SigningCredentials** has to be defined, see [Signing keys](#signing-keys).
-- `idp.oidfed.entityID` and `credential_issuer` have to name the same deployment. A mismatch is
- logged and published.
+```json
+ "GeantIncubatorDiploma_DIDWEB": {
+ "format": "dc+sd-jwt",
+ "vct": "GeantIncubatorDiploma_DIDWEB",
+ "scope": "GeantIncubatorDiploma_DIDWEB",
+ "cryptographic_binding_methods_supported": ["did:jwk"],
+ "proof_types_supported": {
+ "jwt": {
+ "proof_signing_alg_values_supported": ["ES256"]
+ }
+ },
+ "_issuance_configuration": {
+ "issuer": "did:web"
+ },
+ "credential_metadata": {
+ "display": [
+ { "name": "Incubator Diploma, did:web", "locale": "en" }
+ ],
+ "claims": [
+ { "path": ["diploma"], "display": [{ "name": "Diploma", "locale": "en" }] },
+ { "path": ["mail"], "display": [{ "name": "Email", "locale": "en" }] },
+ { "path": ["eppn"], "display": [{ "name": "Principal name", "locale": "en" }] },
+ { "path": ["givenName"], "display": [{ "name": "Given names", "locale": "en" }] },
+ { "path": ["familyName"], "display": [{ "name": "Family name", "locale": "en" }] },
+ { "path": ["affiliation"], "display": [{ "name": "Affiliation", "locale": "en" }] }
+ ]
+ }
+ }
+```
-Credentials then carry `fed` and `termsOfUse`, per credential `"oidfed": false` leaves them out,
-see
-[Whether a credential names this deployment in a federation](#whether-a-credential-names-this-deployment-in-a-federation).
+</details>
-### The deployment under HAIP
+Setting `openidvci.issuer = did:web` in *conf/openid-vci.properties* would do the same for every
+credential that states no `issuer` of its own. Here we keep the deployment default as did:jwk
+and state it per credential.
-To publish the certificate chain in credentials, Status List Tokens and signed metadata, name
-the issuer with a url in *conf/openid-vci.properties*:
+Add **DID.Document** to the anonymous relying party in *conf/relying-party.xml*:
-```properties
-openidvci.issuer = https://issuer.example.org
+```xml
+ <!-- A verifier resolves the did:web identifier from this -->
+ <ref bean="DID.Document" />
```
-To sign with a certificate rather than with a bare key, define the credential from the key and
-the certificates in *conf/openid-vci-credentials.xml*, the certificate of this deployment first
-and the certificate of the trust anchor left out:
+and wire it in *etc/jetty-rewrite-rules.xml*:
```xml
-<bean id="openidvci.DefaultESSigningCredential" parent="shibboleth.BasicX509CredentialFactoryBean"
- p:privateKey="%{idp.home}/credentials/vci-signing.key">
- <property name="certificates">
- <list>
- <value>%{idp.home}/credentials/vci-signing.crt</value>
- <value>%{idp.home}/credentials/issuing-ca.crt</value>
- </list>
- </property>
-</bean>
+ <Call name="addRule">
+ <Arg>
+ <New class="org.eclipse.jetty.rewrite.handler.RewritePatternRule">
+ <Set name="pattern">/.well-known/did.json</Set>
+ <Set name="replacement">/idp/profile/did/document</Set>
+ </New>
+ </Arg>
+ </Call>
```
-and list it in **openidvci.SigningCredentials** and in
-**openidvci.issuerMetadata.SigningCredentials**.
+The document has no file behind it. It is formed from the keys that sign credentials and Status
+List Tokens. We still sign with the keys of the OP, so it lists those, also the RSA key.
-To require a key attestation of the wallet, add the requirement to the credential in
-*metadata/verifiable-credentials.json*:
-
-```json
-"proof_types_supported": {
- "jwt": {
- "proof_signing_alg_values_supported": [ "ES256" ],
- "key_attestations_required": {
- "key_storage": [ "iso_18045_moderate" ],
- "user_authentication": [ "iso_18045_moderate" ]
- }
- }
-}
-```
-
-To accept key attestations and Wallet Attestations, name the certificates they are signed under
-in *conf/global.xml*:
-
-```xml
-<util:list id="openidvci.KeyAttestationTrustAnchors">
- <bean parent="shibboleth.BasicX509CredentialFactoryBean"
- p:entity="%{idp.home}/credentials/attester-ca.crt"
- p:certificates="#{{'%{idp.home}/credentials/attester-ca.crt'}}" />
-</util:list>
-
-<util:list id="openidvci.ClientAttestationTrustAnchors">
- <bean parent="shibboleth.BasicX509CredentialFactoryBean"
- p:entity="%{idp.home}/credentials/attester-ca.crt"
- p:certificates="#{{'%{idp.home}/credentials/attester-ca.crt'}}" />
-</util:list>
-```
-
-To have the wallet authenticate with a Wallet Attestation rather than as a public client,
-register the method in *metadata/oidc-client.json*:
+<details>
+<summary>Example of the document at /.well-known/did.json</summary>
```json
{
- "client_id": "https://wallet.example.org",
- "client_name": "Example Wallet",
- "redirect_uris": ["https://wallet.example.org/callback"],
- "scope": "openid GeantIncubatorDiploma_SDJWT GeantIncubatorDiploma_W3C",
- "audience": "credentials",
- "token_endpoint_auth_method": "attest_jwt_client_auth",
- "response_types": ["code"],
- "grant_types": ["authorization_code"]
+ "@context": [
+ "https://www.w3.org/ns/did/v1",
+ "https://w3id.org/security/suites/jws-2020/v1"
+ ],
+ "id": "did:web:issuer.example.org",
+ "verificationMethod": [
+ {
+ "id": "did:web:issuer.example.org#defaultEC",
+ "type": "JsonWebKey2020",
+ "controller": "did:web:issuer.example.org",
+ "publicKeyJwk": {
+ "kty": "EC",
+ "crv": "P-256",
+ "kid": "defaultEC",
+ "x": "qQ9ESeIrQ36JijWM-8xdcjXwY46RW3p9YDtP0MVaLnE",
+ "y": "5yVZRqwp1tde_CnKC662wW-XFQhEOrGAPi0LTG4OpI8"
+ }
+ }
+ ],
+ "assertionMethod": [
+ "did:web:issuer.example.org#defaultEC"
+ ]
}
```
-and enable the method on the profiles of that wallet in *conf/relying-party.xml*, the
-`client_id` above being the `sub` of its Wallet Attestation:
+</details>
-```xml
-<bean parent="RelyingPartyByName" c:relyingPartyIds="#{{'https://wallet.example.org'}}">
- <property name="profileConfigurations">
- <list>
- <bean parent="OIDC.SSO"
- p:authorizationCodeClaimsSetManipulationStrategy-ref="openidvci.TokenManipulationStrategy"
- p:forcePKCE="true" p:requirePushedAuthorizationRequest="true"
- p:includeIssuerInResponse="true" />
- <bean parent="OpenID.VCI.Token"
- p:accessTokenClaimsSetManipulationStrategy-ref="openidvci.TokenManipulationStrategy"
- p:tokenEndpointAuthMethods="#{{'attest_jwt_client_auth'}}" />
- <bean parent="OpenID.VCI.Credentials" />
- <bean parent="OAUTH2.PAR" p:tokenEndpointAuthMethods="#{{'attest_jwt_client_auth'}}" />
- </list>
- </property>
-</bean>
-```
+A key needs a name to be listed, the `kid` of a JWK key. The keys of the OP have one.
-and add the validator to the login flow in
-*conf/authn/oauth2client-authn-config.xml*, before the validator that authenticates a public
-client:
+To make the credential requestable, the same small additions for both flows:
-```xml
-<util:list id="shibboleth.authn.OAuth2Client.Validators">
- <ref bean="shibboleth.OIDCClientInfoValidator" />
- <ref bean="shibboleth.JWTValidator" />
- <ref bean="openidvci.ClientAttestationValidator" />
- <ref bean="openidvci.PublicClientValidator" />
-</util:list>
-```
-
-The PAR endpoint is the OP's, and it refuses a client registered with a method its own login
-flow does not know. Give the OAuth endpoints of the OP the login flow of this plugin, in
-*conf/oidc.properties*:
+- `GeantIncubatorDiploma_DIDWEB` to `scope` of `id.animo.paradym` in *metadata/oidc-client.json*
+- the same to `scopes_supported` of *static/oauth-authorization-server.json*
+- `<AttributeEncoder xsi:type="oidc:OIDCString" name="GeantIncubatorDiploma_DIDWEB" />` to the
+ attribute definition of Step 1. The claims are the same, so the script does not change.
-```properties
-idp.oauth2.authn.flows = OpenIDVCIClient
-```
+And the offer of the pre-authorized code flow names the new id:
-To advertise all of it, add the members in *static/oauth-authorization-server.json*:
-
-```json
-"token_endpoint_auth_methods_supported":[ "attest_jwt_client_auth" ],
-"client_attestation_signing_alg_values_supported":[ "ES256" ],
-"client_attestation_pop_signing_alg_values_supported":[ "ES256" ],
-"authorization_response_iss_parameter_supported":true
+```bash
+curl -u 'offer-caller:CHANGE-ME' \
+ -H 'Content-Type: application/json' \
+ https://issuer.example.org/idp/profile/openid/vci/credential-offer \
+ -d '{
+ "preAuthorizedCredentials": {
+ "GeantIncubatorDiploma_DIDWEB": [
+ { "path": ["diploma"], "value": "Scrum Master" },
+ { "path": ["mail"], "value": "gemma.erasmus at example.org" },
+ { "path": ["eppn"], "value": "gemmaer at example.org" },
+ { "path": ["givenName"], "value": "Gemma Gemina" },
+ { "path": ["familyName"], "value": "Erasmus" },
+ { "path": ["affiliation"], "value": "member" }
+ ]
+ }
+}'
```
-## Reference
+### Step 3: Add a plain url credential
-**File(s):** *conf/relying-party.xml*
-**Format:** Native Spring
+Third credential is named by the issuer of the OP as such, `https://issuer.example.org`. A
+verifier finds the key from JWT VC issuer metadata, `/.well-known/jwt-vc-issuer`, which points to
+the keyset of the OP.
-You enable an endpoint by adding its profile configuration bean to a relying party. Nothing is
-enabled by default.
+What is new:
-| Endpoint | Profile configuration bean | Profile ID |
-|---|---|---|
-| `/idp/profile/openid/vci/issuer-metadata` | **OpenID.VCI.IssuerMetadata** | `http://geant.org/ns/profiles/openid/vci/issuer-metadata` |
-| `/idp/profile/openid/vci/credential-offer` | **OpenID.VCI.CredentialOffer** | `http://geant.org/ns/profiles/openid/vci/credential-offer` |
-| `/idp/profile/openid/vci/token` | **OpenID.VCI.Token** | `http://geant.org/ns/profiles/openid/vci/token` |
-| `/idp/profile/openid/vci/credentials` | **OpenID.VCI.Credentials** | `http://geant.org/ns/profiles/openid/vci/credentials` |
-| `/idp/profile/openid/vci/nonce` | **OpenID.VCI.Nonce** | `http://geant.org/ns/profiles/openid/vci/nonce` |
-| `/idp/profile/oauth/jwt-vc-issuer` | **OAuth.JWTVCIssuer** | `http://geant.org/ns/profiles/oauth/jwt-vc-issuer` |
-| `/idp/profile/oauth/authorization-server` | **OAuth.AuthorizationServer** | `http://geant.org/ns/profiles/oauth/authorization-server` |
-| `/idp/profile/did/document` | **DID.Document** | `http://geant.org/ns/profiles/did/document` |
-| `/idp/profile/oauth/status-list/list` | **OAuth.StatusList** | `http://geant.org/ns/profiles/oauth/status-list` |
-| `/idp/profile/oauth/status-list/revoke` | **OAuth.StatusList.Revoke** | `http://geant.org/ns/profiles/oauth/status-list/revoke` |
-| `/idp/profile/oauth/status-list/assignments` | **OAuth.StatusList.Assignments** | `http://geant.org/ns/profiles/oauth/status-list/assignments` |
-
-Some of these endpoints are reached without any client registration, so they belong to
-**shibboleth.UnverifiedRelyingParty**. **OpenID.VCI.IssuerMetadata**, **OpenID.VCI.Nonce**,
-**OAuth.JWTVCIssuer** and **OAuth.AuthorizationServer** serve wallets, **OAuth.StatusList** and
-**DID.Document** serve verifiers. **OpenID.VCI.Token** and
-**OpenID.VCI.Credentials** belong there too if you accept unregistered wallets.
+- the credential states `"issuer": "url"`
+- JWT VC issuer metadata is published at `/.well-known/jwt-vc-issuer`
+- the credential header `kid` is the plain name of the key, as in the keyset of the OP
+- the Status List url of the credential ends with `~url`
-<details>
-<summary>Example snippet of conf/relying-party.xml</summary>
+The credential in *metadata/verifiable-credentials.json* is like the one of Step 2, with id
+`GeantIncubatorDiploma_URL` and:
-```xml
-<bean id="shibboleth.UnverifiedRelyingParty" parent="RelyingParty">
- <property name="profileConfigurations">
- <list>
- <!-- Anyone may read the Credential Issuer metadata -->
- <ref bean="OpenID.VCI.IssuerMetadata" />
- <!-- Anyone may read the keys that signed a credential -->
- <ref bean="OAuth.JWTVCIssuer" />
- <!-- Anyone may read the authorization server metadata -->
- <ref bean="OAuth.AuthorizationServer" />
- <!-- Anyone may read the did:web document, needed when openidvci.issuer is did:web -->
- <ref bean="DID.Document" />
- <!-- Anyone may ask for a nonce -->
- <ref bean="OpenID.VCI.Nonce" />
- <!-- Anyone may read a Status List -->
- <ref bean="OAuth.StatusList" />
- </list>
- </property>
-</bean>
+```json
+ "_issuance_configuration": {
+ "issuer": "url"
+ },
```
-</details>
-
-The rest of the profiles go to a relying party of their own. Every profile of this chapter has
-an example of its own below, and [Relying party configuration](#relying-party-configuration)
-has them all as one file.
-
-### Switches
-
-Everything this module lets you decide, grouped by what you are deciding rather than by the
-file it lives in. Names are here for orientation, their types and defaults are in
-[Properties](#properties), [Profile configuration settings](#profile-configuration-settings)
-and [Beans](#beans).
-
-#### Which credentials this deployment issues
-
-*metadata/verifiable-credentials.json* is the whole answer. A credential exists because it has
-a configuration in that file, and its id is the string a wallet asks for, the one `scope`
-matches and the one the access token carries. See
-[Credential Configurations](#credential-configurations).
-
-| Switch | Why you would touch it |
-|---|---|
-| `openidvci.credentialConfigurations.resource` | The file is somewhere else than *metadata/*. |
-| `openidvci.credentialConfigurations.minRefreshDelay`, `…maxRefreshDelay` | How soon an edit of that file takes effect. It is re-read while the IdP runs, so adding a credential needs no restart. |
-
-#### How long an issued credential is valid
-
-The window is `not_before` until `not_before` plus `lifetime`. A credential states its own in
-[Settings of your own, per credential](#settings-of-your-own-per-credential), and what it does
-not state comes from the profile configuration.
-
-| Switch | Why you would touch it |
-|---|---|
-| `openidvci.credentialTimestampPrecision` | The full hour is too coarse or too fine for the `iat` and `exp` your credentials carry. |
-| `lifetime` of a credential | One credential outlives another. A `credentialLifetime` for everything is the alternative. |
-| `not_before` of a credential | The window opens on a date you decide instead of at issuance. A cohort of one school year is the case for it. |
-| `credentialLifetime` | The validity of everything this deployment issues, and of anything that states no `lifetime` of its own. |
-
-#### Whether an issued credential can be withdrawn
-
-A credential is withdrawable when it took a Status List slot, and it took one unless something
-said otherwise. See [Status List](#status-list).
-
-| Switch | Why you would touch it |
-|---|---|
-| `status_list` of a credential | This credential is too short lived to be worth revoking. |
-| `openidvci.status-list.includeByDefault` | Turn the Status List into something each credential opts into instead. |
-| `openidvci.status-list.index.permutationKey` | **You must.** It decides the order slots are handed out in, and with the shipped value an observer is able to tell when a credential was issued. |
-| `openidvci.status-list.token.defaultLifetime` | How long a verifier is allowed to cache a list it read. |
-| `openidvci.status-list.assignment.retention` | How long you have to collect the record of a slot before it is dropped. Your own systems read those records, nothing in the IdP does. |
-
-#### Whether a credential names this deployment in a federation
-
-Off by default, because a credential has no business naming a federation this deployment does
-not publish itself in. The value it names is not yours to write, it is the `credential_issuer`
-of your Credential Issuer metadata. See
-[Whether a credential states this deployment in OpenID Federation](#whether-a-credential-states-this-deployment-in-openid-federation).
-
-| Switch | Why you would touch it |
-|---|---|
-| The import of *conf/openid-vci-oidfed.xml* | This is the switch. It states this deployment as a Credential Issuer in its Entity Configuration and makes credentials name the same identifier. |
-| `oidfed` of a credential | One credential stays out of it while the rest do not. |
-| `openidvci.oidfed.decorateFederationEntity` | State the federation entity in the federation module's own file instead of filling it from Credential Issuer metadata. |
-
-#### Which key signs what
-
-Nothing here is active to begin with. Credentials, Status List Tokens and Credential Issuer
-metadata are all signed with the OP's own signing credentials until you say otherwise, and that
-works for as long as the OP has an EC key. Signing does ES256, ES384 and ES512 only. See
-[Signing keys](#signing-keys).
-
-| Switch | Why you would touch it |
-|---|---|
-| **openidvci.SigningCredentials** | Credentials are signed with a key of their own. Defining this list *replaces* the OP's credentials, it does not add to them. |
-| **openidvci.status-list.SigningCredentials** | Status List Tokens are signed with a key of their own. |
-| **openidvci.issuerMetadata.SigningCredentials** | Publish the Credential Issuer metadata signed. Undefined means unsigned. |
-| `openidvci.signing.es.key`, `openidvci.signing.status-list.es.key`, `openidvci.signing.issuerMetadata.es.key` | Those keys are somewhere else than *credentials/*. |
-| `credentialSignatureSigningConfiguration`, `statusListSignatureSigningConfiguration`, `issuerMetadataSignatureSigningConfiguration` | Narrow the algorithms a deployment allows at all. The algorithm is narrowed again by the `credential_signing_alg_values_supported` of the credential in issue. |
-
-#### What a credential says issued it
-
-One property, three kinds of value, and the same value lands in a Status List Token. What each
-one publishes and how a verifier reaches the key is in
-[Credential Issuer identifiers](#credential-issuer-identifiers).
-
-| Switch | Why you would touch it |
-|---|---|
-| `openidvci.issuer` empty | The default. The `iss` is the `did:jwk` of the signing key, which carries the key inside the credential and needs nothing published. The identifier changes when the key does, so a deployment that rotates keys and revokes credentials wants one of the two below. |
-| `openidvci.issuer` = `did:web` | The `iss` is the `did:web` of this deployment, derived from the issuer of the OP. A verifier reads the keys from `/.well-known/did.json`, which needs the **DID.Document** profile enabled. |
-| `openidvci.issuer` = `url` | The `iss` is the issuer of the OP. A verifier reads the keys through `/.well-known/jwt-vc-issuer`, which needs the **OAuth.JWTVCIssuer** profile enabled and the key in **shibboleth.oidc.SigningCredentialsToPublish**. |
-
-#### What the discovery documents say
-
-Three documents are published, each from a Velocity template of yours in *static/*. One member
-is filled in rather than written by hand, the `credential_configurations_supported` of the
-first. See [Discovery documents](#discovery-documents).
-
-| Switch | Why you would touch it |
-|---|---|
-| `openidvci.issuerMetadata.template` | The document is somewhere else than *static/*. |
-| `openidvci.issuerMetadata.templated` | Your document contains a literal `$` or `#` and must not be read as Velocity. |
-| **openidvci.issuerMetadata.TemplateContext** | Add variables of your own, or replace `$issuer`, `$baseUrl` and `$host`. |
-| `openidvci.issuerMetadata.resolver`, `…resolver.values` | Resolve the document, or single members of it, some other way. |
-| **openidvci.CredentialIssuerMetadata** | Replace the document wholesale with a resource of your own. |
-| `openidvci.issuerMetadata.signedLifetime` | Signed metadata carries an `exp`. Unset publishes it without one. |
-| `openidvci.issuerMetadata.minRefreshDelay`, `…maxRefreshDelay` | How soon an edit of the document takes effect. |
-| `openidvci.jwtVcIssuer.*`, `openidvci.authorizationServer.*` | The same settings for the other two documents, each with its own template, resolver and refresh bounds. |
-
-#### What a pre-authorized code looks like
-
-The length decides which of two quite different codes you get, and it is the one setting here
-to understand before you pick a value. See
-[Pre-authorized code flow](#pre-authorized-code-flow).
-
-| Switch | Why you would touch it |
-|---|---|
-| `openidvci.preAuthorizedCode.defaultLength` | `0` seals the whole offer inside the code, which needs no storage and makes a dense QR code. `10` or more is a short code and a stored offer. |
-| `openidvci.preAuthorizedCode.defaultLifetime` | How long the holder of an offer has to redeem it. |
-| `preAuthorizedCodeLength`, `preAuthorizedCodeLifetime` | The same two per relying party rather than per deployment. |
-| `batchSize` | How many key proofs a given wallet may send in one Credential Request. Has no effect while the Credential Issuer metadata advertises `batch_credential_issuance`, which applies to every wallet alike. |
-
-#### Who is allowed to call an endpoint
-
-Nothing is enabled by default. An endpoint answers because its profile configuration bean is in
-a relying party, and the bean says how the caller authenticates. See
-[Relying party configuration](#relying-party-configuration).
-
-| Switch | Why you would touch it |
-|---|---|
-| The **OpenID.VCI.\*** and **OAuth.StatusList\*** beans | Enable an endpoint, for everyone in **shibboleth.UnverifiedRelyingParty** or for one client by name. |
-| `tokenEndpointAuthMethods` | An unregistered wallet needs `none`, your own caller of the offer and revoke endpoints needs a real method. |
-| `openidvci.authn.flows` | The login flows a VCI endpoint accepts. |
-| **openidvci.TokenManipulationStrategy** | Carry the authorization details and the claim values into the code and the access token. **OIDC.SSO** and **OpenID.VCI.Token** both need it. |
+`url` is a keyword for the issuer of the OP. You may also write the url itself.
-#### What a wallet has to prove
+Add **OAuth.JWTVCIssuer** to the anonymous relying party:
-| Switch | Why you would touch it |
-|---|---|
-| `proofSignatureValidationConfiguration` | Which algorithms a key proof may be signed with. |
-| `proofClaimsValidator` | What the claims of a key proof have to say. |
-| `proofNonceGenerator` | The nonce a key proof carries. Set the same generator in **OpenID.VCI.Nonce**, which is what hands the nonce out, and `#{null}` in both to ask for no nonce at all. |
-
-#### Where server side state is kept
+```xml
+ <!-- A verifier finds the key of a url issuer through this -->
+ <ref bean="OAuth.JWTVCIssuer" />
+```
-Storage is only needed for what cannot be sealed into a token. All of these want real server
-side storage rather than the memory backed default of a cluster.
+and wire it in *etc/jetty-rewrite-rules.xml*:
-| Switch | Why you would touch it |
-|---|---|
-| `openidvci.StorageService` | Credential Offers, when the code is a stored one. |
-| `openidvci.status-list.StorageService` | The lists themselves. |
-| `openidvci.status-list.index.StorageService` | The counter of slots handed out. It has the churn of the three, so it may want storage of its own. |
-| `openidvci.status-list.assignment.StorageService` | The records of which credential took which slot, kept for collection. |
+```xml
+ <Call name="addRule">
+ <Arg>
+ <New class="org.eclipse.jetty.rewrite.handler.RewritePatternRule">
+ <Set name="pattern">/.well-known/jwt-vc-issuer</Set>
+ <Set name="replacement">/idp/profile/oauth/jwt-vc-issuer</Set>
+ </New>
+ </Arg>
+ </Call>
+```
-#### What the log calls each flow
+*static/jwt-vc-issuer.json* as it comes with the plugin is fine. Its `jwks_uri` is the keyset of
+the OP, served by `OIDC.Keyset` that is already in the anonymous relying party. The keyset has
+the keys of the OP, and those still sign.
-| Switch | Why you would touch it |
-|---|---|
-| `openidvci.logging.credentials`, `…credential-offer`, `…token`, `…nonce`, `…issuer-metadata`, `…status-list.list`, `…status-list.revoke`, `…status-list.assignments` | The logging id of a flow, which is what you filter your logs by. |
+Scope, scopes supported, attribute encoder and offer as in Step 2, with
+`GeantIncubatorDiploma_URL`.
-### Discovery documents
+### Step 4: Make the url credential an OpenID Federation credential
-A wallet reads three documents and every one of them comes from an endpoint of this plugin.
-Each endpoint reads a file of *static/*, and each of those files is a Velocity template where
-`$baseUrl`, `$issuer` and `$host` are resolved from the issuer of the OP. None of them names
-your host, so none of them needs editing to name it. A fourth document is published for
-verifiers rather than for wallets, and that one has no file behind it at all. It is described
-after these three.
+No new credential in this step. The deployment is stated as a Credential Issuer in OpenID
+Federation and the url credential of Step 3 names it. The federation itself, trust anchors,
+authority hints and so on, is configured in the plugins of [Dependencies](#dependencies), see
+[OIDFEDCommon](https://shibboleth.atlassian.net/wiki/spaces/IDPPLUGINS/pages/5601296385/OIDFEDCommon).
-| Document | Endpoint | File |
-|---|---|---|
-| `/.well-known/openid-credential-issuer` | `/idp/profile/openid/vci/issuer-metadata` | *static/openid-credential-issuer.json* |
-| `/.well-known/jwt-vc-issuer` | `/idp/profile/oauth/jwt-vc-issuer` | *static/jwt-vc-issuer.json* |
-| `/.well-known/oauth-authorization-server` | `/idp/profile/oauth/authorization-server` | *static/oauth-authorization-server.json* |
+What is new:
-One member is not in the files at all. `credential_configurations_supported` of the first is
-taken from *metadata/verifiable-credentials.json*, so that what you advertise is what you are
-able to issue. Everything else in all three documents is yours to write.
+- an own signing key replaces the keys of the OP in credential signing
+- the Entity Configuration at `/.well-known/openid-federation` has `openid_credential_issuer`
+ metadata and the credential signing keys
+- the url credential carries `"fed": "https://issuer.example.org"`, the other credentials not
-The Credential Issuer metadata is published only when it carries `credential_endpoint` and
-`credential_configurations_supported`. Without either one the endpoint answers `500
-server_error` and says which member is missing in the log, rather than publish a document a
-wallet cannot use. The second of them is missing when *metadata/verifiable-credentials.json*
-cannot be read, so a broken file of credentials takes the metadata endpoint down with it.
+#### Own signing key
-<details>
-<summary>Example contents of static/openid-credential-issuer.json</summary>
+The federation part reads the signing keys of the credentials from
+**openidvci.SigningCredentials**, so that list has to be defined now. Generate an EC key the
+same way as the keys of the OP:
-```json
-{
- "credential_issuer": "$baseUrl",
- "authorization_servers": [
- "$issuer"
- ],
- "credential_endpoint": "$baseUrl/idp/profile/openid/vci/credentials",
- "nonce_endpoint": "$baseUrl/idp/profile/openid/vci/nonce",
- "batch_credential_issuance": {
- "batch_size": 2
- },
- "logo_uri": "https://issuer.example.org/images/logo.png",
- "contacts": [
- "credentials at example.org"
- ],
- "display": [
- {
- "locale": "en",
- "name": "Incubator credentials",
- "description": "Trust and Identity Incubator credential issuer"
- },
- {
- "locale": "fi-FI",
- "name": "Incubator credentials",
- "description": "Trust and Identity Incubator -todistusten myöntäjä"
- }
- ]
-}
+```bash
+bin/jwtgen.sh -t EC -c P-256 -u sig -i signingvci | tail -n +2 > credentials/openid-vci-signing-es.jwk
```
-</details>
-
-`batch_credential_issuance` is the one member of this document the plugin reads back. Its
-`batch_size` is the largest `proofs` array a Credential Request may carry, and the credential
-endpoint rejects a longer one before it verifies a single proof. The specification allows the
-member only with a `batch_size` of 2 or greater; drop the member to accept one key proof per
-request. Where the member is absent the `batchSize` of **OpenID.VCI.Credentials** applies
-instead.
+Uncomment the key and the list in *conf/openid-vci-credentials.xml*. Keep the EC key of the OP
+in the list after the new one. It is defined here again from the same file, as the bean of the
+OP is not visible where this file is imported:
-`/.well-known/jwt-vc-issuer` tells a verifier which keys sign your credentials. Its `issuer`
-has to be the same value the credentials carry as `iss`. The document matters only when your
-Credential Issuer identifier is the plain issuer url. With `did:jwk`, which is the default, the
-key travels inside the credential, and with `did:web` the keys come from a document of their
-own. In neither case does anybody read this one. See
-[Credential Issuer identifiers](#credential-issuer-identifiers).
+```xml
+<bean id="openidvci.DefaultESSigningCredential" parent="shibboleth.JWKCredential"
+ p:resource="%{openidvci.signing.es.key}" />
-<details>
-<summary>Contents of static/jwt-vc-issuer.json as it comes with the plugin</summary>
+<bean id="openidvci.PreviousESSigningCredential" parent="shibboleth.JWKCredential"
+ p:resource="%{idp.signing.oidc.es.key}" />
-```json
-{
- "issuer":"$issuer",
- "jwks_uri":"$baseUrl/idp/profile/oidc/keyset"
-}
+<util:list id="openidvci.SigningCredentials">
+ <ref bean="openidvci.DefaultESSigningCredential" />
+ <ref bean="openidvci.PreviousESSigningCredential" />
+</util:list>
```
-</details>
+The first key that fits the algorithm signs, the others are there only to be published. This
+is a key rollover from the key of the OP to the own key. [Step 6](#step-6-rolling-the-signing-key)
+tells why the old key stays.
-A fourth document is published when your Credential Issuer identifier is `did:web`, and it is
-unlike the three above. `/.well-known/did.json` comes from `/idp/profile/did/document` and there
-is no file of *static/* behind it. The document is formed from the keys credentials and Status
-List Tokens are signed with, so there is nothing to write and nothing to keep in step by hand.
-See [DID.Document](#diddocument).
+Import the file into *conf/global.xml*. Normally it would go to *conf/credentials.xml*, but the
+Entity Configuration is published from the root context and it does not see beans of
+*conf/credentials.xml*:
-`/.well-known/oauth-authorization-server` tells a wallet where the token endpoint is. Note that
-`token_endpoint` is the VCI one and not the OP's own.
+```xml
+<import resource="openid-vci-credentials.xml"/>
+```
-`dpop_signing_alg_values_supported` names the algorithms a DPoP proof may be signed with, which
-FAPI 2.0 requires to carry at least one of `PS256`, `ES256`, `EdDSA` and `Ed25519`. Keep it in step
-with `idp.oauth2.dpop.proofAlgorithms` of *conf/oidc.properties*, the list a DPoP error response
-offers the wallet.
+The url credential names its key through the keyset of the OP, and the new key is not in that
+keyset. Publish it in *conf/oidc-credentials.xml*. The list replaces what is published, so
+the keys of the OP are listed too. If that file has an `alias` named
+`shibboleth.oidc.SigningCredentialsToPublish`, remove it:
-<details>
-<summary>Contents of static/oauth-authorization-server.json as it comes with the plugin</summary>
-
-```json
-{
- "issuer":"$issuer",
- "authorization_endpoint":"$baseUrl/idp/profile/oidc/authorize",
- "pushed_authorization_request_endpoint":"$baseUrl/idp/profile/oauth2/pushed-authorization",
- "require_pushed_authorization_requests":true,
- "token_endpoint":"$baseUrl/idp/profile/openid/vci/token",
- "jwks_uri":"$baseUrl/idp/profile/oidc/keyset",
- "token_endpoint_auth_methods_supported":[
- "none",
- "client_secret_basic",
- "client_secret_post",
- "client_secret_jwt"
- ],
- "response_types_supported":[
- "code"
- ],
- "grant_types_supported":[
- "authorization_code",
- "urn:ietf:params:oauth:grant-type:pre-authorized_code"
- ],
- "code_challenge_methods_supported":[
- "S256"
- ],
- "dpop_signing_alg_values_supported":[
- "ES256",
- "ES384",
- "ES512",
- "PS256",
- "PS384",
- "PS512"
- ],
- "scopes_supported":[
- "openid"
- ],
- "authorization_details_types_supported":[
- "openid_credential"
- ]
-}
+```xml
+<util:list id="shibboleth.oidc.SigningCredentialsToPublish">
+ <ref bean="shibboleth.oidc.DefaultRSSigningCredential" />
+ <ref bean="shibboleth.oidc.DefaultESSigningCredential" />
+ <ref bean="openidvci.DefaultESSigningCredential" />
+</util:list>
```
-</details>
+Publish the key before you start signing with it. A verifier that fetched the keyset earlier
+may have the old one in cache.
-`scopes_supported` is yours to write. A wallet of the code flow names the credential it wants
-as a scope, so every credential configuration id you want requestable that way belongs here.
-With the credentials of this document it reads:
-
-```json
- "scopes_supported":[
- "openid",
- "GeantIncubatorDiploma_SDJWT",
- "GeantIncubatorDiploma_W3C"
- ],
-```
+#### Federation
-`require_pushed_authorization_requests` and `code_challenge_methods_supported` describe the OP,
-they do not configure it. Both are off in the OP by default. Back them on the **OIDC.SSO** bean:
+The Entity Identifier and `credential_issuer` of the Credential Issuer metadata have to be the
+same value. The template publishes `$baseUrl` as `credential_issuer`, so in
+*conf/oidfed/oidfed.properties*:
-```xml
-<bean parent="OIDC.SSO"
- p:authorizationCodeClaimsSetManipulationStrategy-ref="openidvci.TokenManipulationStrategy"
- p:forcePKCE="true"
- p:requirePushedAuthorizationRequest="true" />
+```properties
+idp.oidfed.entityID = https://issuer.example.org
```
-Then the paths. No `/.well-known/` location is an IdP path, so you wire each one to its
-endpoint in Jetty. Wallets that run in a browser need also `Access-Control-Allow-Origin` in the
-responses, so that header belongs to the same file. With Jetty rewrite handler:
+Nothing checks that the two match, it is your responsibility.
-<details>
-<summary>Example contents of etc/jetty-rewrite-rules.xml</summary>
+Import the federation file into *conf/global.xml*, after the import above:
```xml
-<?xml version="1.0"?>
-<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "https://jetty.org/configure_10_0.dtd">
-<Configure id="Rewrite" class="org.eclipse.jetty.rewrite.handler.RuleContainer">
- <!-- Credential Issuer metadata comes from this plugin -->
- <Call name="addRule">
- <Arg>
- <New class="org.eclipse.jetty.rewrite.handler.RewritePatternRule">
- <Set name="pattern">/.well-known/openid-credential-issuer</Set>
- <Set name="replacement">/idp/profile/openid/vci/issuer-metadata</Set>
- </New>
- </Arg>
- </Call>
- <!-- OP metadata comes from the OP plugin -->
- <Call name="addRule">
- <Arg>
- <New class="org.eclipse.jetty.rewrite.handler.RewritePatternRule">
- <Set name="pattern">/.well-known/openid-configuration</Set>
- <Set name="replacement">/idp/profile/oidc/configuration</Set>
- </New>
- </Arg>
- </Call>
- <!-- These two come from this plugin as well -->
- <Call name="addRule">
- <Arg>
- <New class="org.eclipse.jetty.rewrite.handler.RewritePatternRule">
- <Set name="pattern">/.well-known/jwt-vc-issuer</Set>
- <Set name="replacement">/idp/profile/oauth/jwt-vc-issuer</Set>
- </New>
- </Arg>
- </Call>
- <Call name="addRule">
- <Arg>
- <New class="org.eclipse.jetty.rewrite.handler.RewritePatternRule">
- <Set name="pattern">/.well-known/oauth-authorization-server</Set>
- <Set name="replacement">/idp/profile/oauth/authorization-server</Set>
- </New>
- </Arg>
- </Call>
- <!-- Needed only when openidvci.issuer is did:web -->
- <Call name="addRule">
- <Arg>
- <New class="org.eclipse.jetty.rewrite.handler.RewritePatternRule">
- <Set name="pattern">/.well-known/did.json</Set>
- <Set name="replacement">/idp/profile/did/document</Set>
- </New>
- </Arg>
- </Call>
- <!-- The JSON-LD context of the vc+sd-jwt credential is a static file of Jetty -->
- <Call name="addRule">
- <Arg>
- <New class="org.eclipse.jetty.rewrite.handler.RewritePatternRule">
- <Set name="pattern">/credentials/GeantIncubatorDiploma_W3C</Set>
- <Set name="replacement">/credentials/GeantIncubatorDiploma_W3C.json</Set>
- </New>
- </Arg>
- </Call>
- <!-- Wallets running in a browser need this -->
- <Call name="addRule">
- <Arg>
- <New class="org.eclipse.jetty.rewrite.handler.HeaderPatternRule">
- <Set name="pattern">/*</Set>
- <Set name="headerName">Access-Control-Allow-Origin</Set>
- <Set name="headerValue">*</Set>
- </New>
- </Arg>
- </Call>
-</Configure>
+<import resource="openid-vci-oidfed.xml"/>
```
-</details>
+Add `OIDFED.Configuration` of the federation plugin to the anonymous relying party and wire
+`/.well-known/openid-federation` to `/idp/profile/oidfed/entity-configuration` in Jetty, the same
+way as the rules above.
+
+Once the import is there every credential states the Entity Identifier, unless it says
+`"oidfed": false`. We want only the url credential in the federation, so the three others get:
-The JSON-LD context of a `vc+sd-jwt` credential is the one document that stays a file of Jetty,
-in *webapps/root/credentials/*. The header rule above sets the header for every path of the
-deployment, which is what our test deployment does. You may want to limit it to the metadata
-paths only.
+```json
+ "_issuance_configuration": {
+ "oidfed": false
+ },
+```
-#### Notes
+`GeantIncubatorDiploma_DIDWEB` has `"issuer": "did:web"` there already, the two members go
+side by side. The url credential then carries:
-The OP's own `/.well-known/openid-configuration` does not describe the VCI endpoints. It
-advertises the OP token endpoint instead of `/idp/profile/openid/vci/token` and it does not
-list your credential scopes. A wallet that reads only that document fails, so wire
-`/.well-known/oauth-authorization-server` to **OAuth.AuthorizationServer** as above.
+```json
+{
+ "iss": "https://issuer.example.org",
+ "fed": "https://issuer.example.org"
+}
+```
-Each profile of this plugin is described below. Six of the eleven inherit the settings of the
-OAuth2/OIDC profile configurations of the OP plugin, `tokenEndpointAuthMethods`,
-`accessTokenLifetime`, `forcePKCE`, `securityConfiguration` and the DPoP settings among them.
-**OpenID.VCI.IssuerMetadata**, **OAuth.JWTVCIssuer**, **OAuth.AuthorizationServer**,
-**DID.Document** and **OAuth.StatusList** are the exceptions. They serve requests that have no client at all and
-inherit nothing of that. Every setting of this plugin is in
-[Profile configuration settings](#profile-configuration-settings), the ones a profile really
-reads are named with the profile.
+A `vc+sd-jwt` credential carries the same as `termsOfUse`:
-### OpenID.VCI.IssuerMetadata
+```json
+{
+ "termsOfUse": {
+ "type": "OpenIDFederation",
+ "policyId": "https://issuer.example.org"
+ }
+}
+```
-Publishes the Credential Issuer metadata. A `GET` of
-`/idp/profile/openid/vci/issuer-metadata`, no authentication and no request parameters. The
-document is *static/openid-credential-issuer.json* as described above.
+`display_name` and `logo_uri` of the federation entity are filled from Credential Issuer
+metadata. Set `openidvci.oidfed.decorateFederationEntity = false` to state them in the
+metadata file of the federation module instead.
-<details>
-<summary>Example snippet of conf/relying-party.xml</summary>
+### Step 5: Add a HAIP credential
-```xml
-<bean id="shibboleth.UnverifiedRelyingParty" parent="RelyingParty">
- <property name="profileConfigurations">
- <list>
- ...
- <ref bean="OpenID.VCI.IssuerMetadata" />
- </list>
- </property>
-</bean>
-```
+Last credential is for a HAIP wallet. HAIP wants the issuer key as a certificate chain in the
+`x5c` header of the credential, of the Status List Token and of signed metadata. The wallet
+authenticates with a Wallet Attestation and its key proof carries a key attestation. The wallet
+here is the OpenID Foundation conformance suite.
-</details>
+What is new:
-| Name | Type | Default | Description |
-|---|---|---|---|
-| `issuerMetadataSignatureSigningConfiguration` | SignatureSigningConfiguration | Bean named **openidvci.issuerMetadata.SigningConfiguration** | Signing configuration of the metadata. The document is published unsigned until **openidvci.issuerMetadata.SigningCredentials** exists. |
+- the signing key has a certificate and a chain, and it signs first
+- the HAIP credential is a url credential, `cnf` carries the `jwk` of the wallet
+- key attestation is required for the HAIP credential
+- the conformance suite is a wallet client with `attest_jwt_client_auth`
+- Credential Issuer metadata is published also signed
-### OAuth.JWTVCIssuer
+#### Certificate chain
-Publishes the JWT VC issuer metadata. A `GET` of `/idp/profile/oauth/jwt-vc-issuer`, no
-authentication and no request parameters. The document is *static/jwt-vc-issuer.json* as
-described above.
+You need an EC signing key, its certificate and the chain above it, from a CA of your own or of
+a 3rd party. The leaf names your host as a dNSName and as a URI in SAN, it is not a CA and it is
+not self-signed. The chain has the leaf and the issuing CA, not the root. The root is what a
+verifier trusts. Below they are *credentials/vci-signing.key*, *credentials/vci-signing.crt* and
+*credentials/vci-issuing-ca.crt*.
-<details>
-<summary>Example snippet of conf/relying-party.xml</summary>
+In *conf/openid-vci-credentials.xml* the key is defined with its certificates, leaf first, and
+put **first** in the list. The key needs a name, `p:keyNames`, for the did:web document:
```xml
-<bean id="shibboleth.UnverifiedRelyingParty" parent="RelyingParty">
- <property name="profileConfigurations">
+<bean id="openidvci.X509ESSigningCredential" parent="shibboleth.BasicX509CredentialFactoryBean"
+ p:privateKey="%{idp.home}/credentials/vci-signing.key" p:keyNames="signingvci-x509">
+ <property name="certificates">
<list>
- ...
- <ref bean="OAuth.JWTVCIssuer" />
+ <value>%{idp.home}/credentials/vci-signing.crt</value>
+ <value>%{idp.home}/credentials/vci-issuing-ca.crt</value>
</list>
</property>
</bean>
-```
-</details>
+<util:list id="openidvci.SigningCredentials">
+ <ref bean="openidvci.X509ESSigningCredential" />
+ <ref bean="openidvci.DefaultESSigningCredential" />
+ <ref bean="openidvci.PreviousESSigningCredential" />
+</util:list>
-You need this endpoint only when you set `openidvci.issuer` to the plain issuer url. A verifier
-reads the document to find the key that signed a credential, and it looks for the document only
-when the credential names its issuer by url. With the default `did:jwk` identifier the key
-travels inside the credential, nothing fetches this document and enabling it changes nothing.
+<util:list id="openidvci.issuerMetadata.SigningCredentials">
+ <ref bean="openidvci.X509ESSigningCredential" />
+</util:list>
+```
-There is nothing to configure per relying party. The document is the same for everyone.
+The chain is emitted whenever the issuer of the signed thing is an https url, there is no
+switch for it. That means the url credential of Steps 3 and 4 now carries `x5c` too, and no
+`kid`. A verifier of it moves from `/.well-known/jwt-vc-issuer` to the chain. The did:jwk and
+did:web credentials do not carry a chain, their identifier resolves the key. This is again a key
+rollover, see [Step 6](#step-6-rolling-the-signing-key).
-### OAuth.AuthorizationServer
+#### The credential
-Publishes the authorization server metadata. A `GET` of
-`/idp/profile/oauth/authorization-server`, no authentication and no request parameters. The
-document is *static/oauth-authorization-server.json* as described above.
+`GeantIncubatorDiploma_HAIP` in *metadata/verifiable-credentials.json*, a url credential with key
+attestation required:
<details>
-<summary>Example snippet of conf/relying-party.xml</summary>
+<summary>Example snippet of metadata/verifiable-credentials.json</summary>
-```xml
-<bean id="shibboleth.UnverifiedRelyingParty" parent="RelyingParty">
- <property name="profileConfigurations">
- <list>
- ...
- <ref bean="OAuth.AuthorizationServer" />
- </list>
- </property>
-</bean>
+```json
+ "GeantIncubatorDiploma_HAIP": {
+ "format": "dc+sd-jwt",
+ "vct": "GeantIncubatorDiploma_HAIP",
+ "scope": "GeantIncubatorDiploma_HAIP",
+ "cryptographic_binding_methods_supported": ["jwk"],
+ "proof_types_supported": {
+ "jwt": {
+ "proof_signing_alg_values_supported": ["ES256"],
+ "key_attestations_required": {
+ "key_storage": ["iso_18045_moderate"],
+ "user_authentication": ["iso_18045_moderate"]
+ }
+ }
+ },
+ "_issuance_configuration": {
+ "issuer": "url",
+ "oidfed": false
+ },
+ "credential_metadata": {
+ "display": [
+ { "name": "Incubator Diploma, HAIP", "locale": "en" }
+ ],
+ "claims": [
+ { "path": ["diploma"], "display": [{ "name": "Diploma", "locale": "en" }] },
+ { "path": ["mail"], "display": [{ "name": "Email", "locale": "en" }] },
+ { "path": ["eppn"], "display": [{ "name": "Principal name", "locale": "en" }] },
+ { "path": ["givenName"], "display": [{ "name": "Given names", "locale": "en" }] },
+ { "path": ["familyName"], "display": [{ "name": "Family name", "locale": "en" }] },
+ { "path": ["affiliation"], "display": [{ "name": "Affiliation", "locale": "en" }] }
+ ]
+ }
+ }
```
</details>
-You need this endpoint in both flows. A wallet finds the token endpoint through it, and the OP's
-own `/.well-known/openid-configuration` does not name the VCI one. Enable it unless you know
-that every wallet you serve is told the token endpoint some other way.
+#### Attestations
-Everything in the document is yours to write. A wallet naming a credential by scope rather than
-by configuration id needs that scope in `scopes_supported`, so add the ones you want
-requestable with the code flow.
+Key attestations and Wallet Attestations are signed by an attester, the wallet provider. You
+trust the certificate of the attester CA. For the conformance suite you need an attester key and
+certificate under a CA you have. The attester key and its certificate go to the suite
+configuration below, the CA certificate to *conf/global.xml*:
-There is nothing to configure per relying party.
+```xml
+<util:list id="openidvci.KeyAttestationTrustAnchors">
+ <bean parent="shibboleth.BasicX509CredentialFactoryBean"
+ p:entity="%{idp.home}/credentials/attester-ca.crt"
+ p:certificates="#{{'%{idp.home}/credentials/attester-ca.crt'}}" />
+</util:list>
-### DID.Document
+<util:list id="openidvci.ClientAttestationTrustAnchors">
+ <bean parent="shibboleth.BasicX509CredentialFactoryBean"
+ p:entity="%{idp.home}/credentials/attester-ca.crt"
+ p:certificates="#{{'%{idp.home}/credentials/attester-ca.crt'}}" />
+</util:list>
+```
-Publishes the did:web document of this deployment. A `GET` of `/idp/profile/did/document`, no
-authentication and no request parameters. Wire `/.well-known/did.json` to it, that is where a
-verifier looks.
+Key attestation is checked for every credential that has `key_attestations_required`. Without
+anchors such a request is refused.
-<details>
-<summary>Example snippet of conf/relying-party.xml</summary>
+Wallet Attestation is a client authentication method, so it is added to the login flow of
+clients in *conf/authn/oauth2client-authn-config.xml*. It has to be before the validator of
+public clients, otherwise a public client match authenticates an attested request without
+checking the attestation:
```xml
-<bean id="shibboleth.UnverifiedRelyingParty" parent="RelyingParty">
- <property name="profileConfigurations">
- <list>
- ...
- <ref bean="DID.Document" />
- </list>
- </property>
-</bean>
+<util:list id="shibboleth.authn.OAuth2Client.Validators">
+ <ref bean="shibboleth.OIDCClientInfoValidator" />
+ <ref bean="shibboleth.JWTValidator" />
+ <ref bean="openidvci.ClientAttestationValidator" />
+ <ref bean="openidvci.PublicClientValidator" />
+</util:list>
```
-</details>
+The VCI endpoints use login flow `OpenIDVCIClient` of this plugin, which knows the method. The
+PAR endpoint is the OP's, and it refuses a client registered with a method its own login flow
+does not know. Give the OAuth endpoints of the OP the same flow in *conf/oidc.properties*:
-You need this endpoint only when you set `openidvci.issuer` to `did:web`. A verifier resolves
-that identifier to this document to find the key that signed a credential, and it looks for the
-document only when the credential names its issuer that way. With `did:jwk` the key travels
-inside the credential and with the plain issuer url the keys come from
-`/.well-known/jwt-vc-issuer`, so in both cases nothing fetches this document and enabling it
-changes nothing.
+```properties
+idp.oauth2.authn.flows = OpenIDVCIClient
+```
-There is no file behind the document and no template. It is formed from
-**openidvci.SigningCredentials** and **openidvci.status-list.SigningCredentials**, the public
-half of each key, so that the keys it states cannot drift from the keys in use. A key that
-appears in both lists is stated once. The subject of the document is the `did:web` derived from
-the issuer of the OP, each key is a verification method named by the `did:web`, `#` and the name
-the key carries, and every one of them is listed under `assertionMethod`.
+#### The conformance suite as a client
-That makes those two lists the record of which keys are verifiable. A key is introduced by
-adding it to a list and retired by removing it, and removing one while credentials still name it
-leaves them unverifiable. See
-[Credential Issuer identifiers](#credential-issuer-identifiers) for the document itself and for
-what rollover asks of you.
+The suite runs two clients, the second one only differs in the redirect uri. The client id is
+the `sub` of the Wallet Attestation the suite makes. In *metadata/oidc-client.json*, `shibVciTest`
+being the alias of your test plan:
-The endpoint answers `500 server_error` when the issuer of the OP names no host, or when none of
-the signing credentials is an EC or RSA key, rather than publish a document without the keys a
-verifier came for.
+<details>
+<summary>Example snippet of metadata/oidc-client.json</summary>
-There is nothing to configure per relying party. The document is the same for everyone.
+```json
+ {
+ "client_id": "conformance_client_1",
+ "client_name": "OpenID Conformance Suite",
+ "redirect_uris": ["https://www.certification.openid.net/test/a/shibVciTest/callback"],
+ "scope": "openid GeantIncubatorDiploma_HAIP",
+ "audience": "credentials",
+ "token_endpoint_auth_method": "attest_jwt_client_auth",
+ "response_types": ["code"],
+ "grant_types": ["authorization_code", "urn:ietf:params:oauth:grant-type:pre-authorized_code"]
+ },
+ {
+ "client_id": "conformance_client_2",
+ "client_name": "OpenID Conformance Suite",
+ "redirect_uris": ["https://www.certification.openid.net/test/a/shibVciTest/callback?dummy1=lorem&dummy2=ipsum"],
+ "scope": "openid GeantIncubatorDiploma_HAIP",
+ "audience": "credentials",
+ "token_endpoint_auth_method": "attest_jwt_client_auth",
+ "response_types": ["code"],
+ "grant_types": ["authorization_code", "urn:ietf:params:oauth:grant-type:pre-authorized_code"]
+ }
+```
-### OpenID.VCI.CredentialOffer
+</details>
-Creates a Credential Offer. A `POST` of `application/json` to
-`/idp/profile/openid/vci/credential-offer` by a client of yours, not by a wallet.
+In *conf/relying-party.xml* PKCE and PAR are required, `iss` is returned in the authorization
+response and both token and PAR endpoints take the Wallet Attestation:
<details>
<summary>Example snippet of conf/relying-party.xml</summary>
```xml
-<bean parent="RelyingPartyByName" c:relyingPartyIds="offer-caller">
- <property name="profileConfigurations">
- <list>
- ...
- <bean parent="OpenID.VCI.CredentialOffer"
- p:tokenEndpointAuthMethods="#{{'client_secret_basic'}}" />
- </list>
- </property>
-</bean>
+ <bean parent="RelyingPartyByName"
+ c:relyingPartyIds="#{{'conformance_client_1','conformance_client_2'}}">
+ <property name="profileConfigurations">
+ <list>
+ <bean parent="OIDC.SSO"
+ p:authorizationCodeClaimsSetManipulationStrategy-ref="openidvci.TokenManipulationStrategy"
+ p:forcePKCE="true" p:requirePushedAuthorizationRequest="true"
+ p:includeIssuerInResponse="true" />
+ <bean parent="OpenID.VCI.Token"
+ p:accessTokenClaimsSetManipulationStrategy-ref="openidvci.TokenManipulationStrategy"
+ p:tokenEndpointAuthMethods="#{{'attest_jwt_client_auth'}}" />
+ <bean parent="OpenID.VCI.Credentials" />
+ <bean parent="OAUTH2.PAR" p:tokenEndpointAuthMethods="#{{'attest_jwt_client_auth'}}" />
+ </list>
+ </property>
+ </bean>
```
</details>
-| Name | Type | Description |
-|---|---|---|
-| `preAuthorizedCredentials` | Object | Required. The credentials of a pre-authorized code offer, keyed with the credential configuration id. The value of each is an array of `{"path": [...], "value": ...}`, the claims and the values as they are issued. |
-| `txCode` | Object | Transaction Code the wallet has to present in the token request. `value` is the code. `input_mode` (`numeric` or `text`), `length` and `description` are optional hints the wallet draws its input screen from and nothing checks them against `value`. `value` is not published in the offer, so delivering it to the user is the job of the caller and it has to happen in some other way than the offer itself. |
-| `client_id` | String | Identifies the caller when the request carries no client authentication. Of use only where you accept unauthenticated clients. |
+DPoP settings are left to their defaults, so a DPoP nonce is required.
-This endpoint serves the pre-authorized code flow. A request without
-`preAuthorizedCredentials` is an error. Every credential configuration id has to exist in
-*metadata/verifiable-credentials.json*. An authorization code flow offer is a static link,
-described under *Authorization code flow*.
+Add `<AttributeEncoder xsi:type="oidc:OIDCString" name="GeantIncubatorDiploma_HAIP" />` to the
+attribute definition and release the attribute also to the two conformance clients in
+*conf/attribute-filter.xml*.
-<details>
-<summary>Example of a call</summary>
+Advertise the rest in *static/oauth-authorization-server.json*. `GeantIncubatorDiploma_HAIP` goes to
+`scopes_supported` as before and these are added:
-```bash
-curl -u 'offer-caller:CHANGE-ME' \
- -H 'Content-Type: application/json' \
- https://issuer.example.org/idp/profile/openid/vci/credential-offer \
- -d '{
- "preAuthorizedCredentials": {
- "GeantIncubatorDiploma_SDJWT": [
- { "path": ["diploma"], "value": "Scrum Master" }
- ]
+```json
+ "token_endpoint_auth_methods_supported":[
+ "none",
+ "client_secret_basic",
+ "client_secret_post",
+ "client_secret_jwt",
+ "attest_jwt_client_auth"
+ ],
+ "client_attestation_signing_alg_values_supported":[ "ES256" ],
+ "client_attestation_pop_signing_alg_values_supported":[ "ES256" ],
+ "authorization_response_iss_parameter_supported":true,
+```
+
+#### Suite configuration
+
+The test plan of the suite is configured with JSON. The parts that come from this deployment:
+
+```json
+{
+ "alias": "shibVciTest",
+ "vci": {
+ "credential_issuer_url": "https://issuer.example.org",
+ "credential_configuration_id": "GeantIncubatorDiploma_HAIP"
+ },
+ "credential": {
+ "trust_anchor_pem": "<vci-root-ca.crt as one line, newlines as \\n>",
+ "status_list_trust_anchor_pem": "<the same>"
+ },
+ "client_attestation": {
+ "issuer": "https://wallet-provider.example.org",
+ "attester_jwks": { "keys": [ "<attester private key as JWK, attester certificate in x5c>" ] },
+ "key_attestation_jwks": { "keys": [ "<the same>" ] }
}
-}'
+}
```
-A worked example with every claim and with a Transaction Code is in
-[Pre-authorized code flow](#pre-authorized-code-flow).
+The client ids of the suite are `conformance_client_1` and `conformance_client_2` as registered
+above.
-</details>
+### Step 6: Rolling the signing key
+
+Steps 4 and 5 changed the signing key. What a key change means depends on the issuer
+identifier of the credential. The configuration is the same for all:
+
+1. The new key goes **first** in **openidvci.SigningCredentials**. The first key that fits the
+ algorithm signs everything new.
+2. The old key stays in the list, after the new one, until every credential signed with it has
+ expired. See the table below what happens if it is removed earlier.
+3. Where a verifier reads the key from something you publish, publish the new key before you
+ start signing with it.
+
+A key that is in the list but does not sign is still published in the did:web document and in
+the Entity Configuration, and it still signs the Status List Tokens of the credentials it
+signed, when the identifier names the key.
+
+| Identifier | What happens when the key changes |
+|---|---|
+| `did:jwk` | The identifier is the key, so it changes. New credentials name a new issuer. Old credentials verify on their own, the key is inside, but a verifier that trusts the issuer by its identifier has to be told the new one. The Status List url of an old credential names the old key by its thumbprint, and that list is signed with the old key. If the old key is not in the list anymore, the Status List Token of the old credentials is not served and they cannot be checked. |
+| `did:web` | The identifier stays. The did:web document lists every key of the list, each by its name, so the new key needs a name of its own. Old credentials name the old key by `kid` and it stays in the document as long as it is in the list. |
+| url, no chain | The identifier stays. The key is found by `kid` from the keyset of the OP, so the new key goes to **shibboleth.oidc.SigningCredentialsToPublish** before it signs, and the old one stays there until its credentials have expired. |
+| url, with chain | The identifier stays. The credential carries its chain, a verifier trusts the root. A new leaf under the same CA needs nothing published. The Status List of url credentials does not name a key, it is signed with whatever key is first. |
+| OpenID Federation | The Entity Configuration publishes the keys of the list. Same as did:web. |
+
+So did:jwk is not good for a deployment that rotates keys and revokes credentials. Use it for
+testing and for credentials that live shorter than the key.
+
+Status List Tokens may be signed with a key of their own, **openidvci.status-list.SigningCredentials**.
+Do not do that while any credential is did:jwk, the Status List Token would name an issuer that
+is another key than the one of the credential.
+
+### What we have now
+
+| Credential | Issuer | Key found from | Federation | Wallet |
+|---|---|---|---|---|
+| `GeantIncubatorDiploma_SDJWT`, `_W3C` | `did:jwk:…` | the identifier | no | Paradym |
+| `GeantIncubatorDiploma_DIDWEB` | `did:web:issuer.example.org` | `/.well-known/did.json` | no | Paradym |
+| `GeantIncubatorDiploma_URL` | `https://issuer.example.org` | `x5c`, earlier `/.well-known/jwt-vc-issuer` | yes | Paradym |
+| `GeantIncubatorDiploma_HAIP` | `https://issuer.example.org` | `x5c` | no | conformance suite |
+
+The whole *conf/relying-party.xml* is in [Relying party configuration](#relying-party-configuration)
+and the whole *etc/jetty-rewrite-rules.xml* in [Discovery documents](#discovery-documents).
+
+## Reference
+
+### Profiles
+
+**File(s):** *conf/relying-party.xml*
+**Format:** Native Spring
+
+An endpoint is enabled by adding its profile configuration bean to a relying party. Nothing is
+enabled by default.
+
+| Endpoint | Profile configuration bean | Profile ID | Caller | Relying party |
+|---|---|---|---|---|
+| `/idp/profile/openid/vci/issuer-metadata` | **OpenID.VCI.IssuerMetadata** | `http://geant.org/ns/profiles/openid/vci/issuer-metadata` | wallet, no authentication | anonymous |
+| `/idp/profile/oauth/authorization-server` | **OAuth.AuthorizationServer** | `http://geant.org/ns/profiles/oauth/authorization-server` | wallet, no authentication | anonymous |
+| `/idp/profile/oauth/jwt-vc-issuer` | **OAuth.JWTVCIssuer** | `http://geant.org/ns/profiles/oauth/jwt-vc-issuer` | verifier, no authentication | anonymous |
+| `/idp/profile/did/document` | **DID.Document** | `http://geant.org/ns/profiles/did/document` | verifier, no authentication | anonymous |
+| `/idp/profile/openid/vci/nonce` | **OpenID.VCI.Nonce** | `http://geant.org/ns/profiles/openid/vci/nonce` | wallet, no authentication | anonymous |
+| `/idp/profile/openid/vci/credential-offer` | **OpenID.VCI.CredentialOffer** | `http://geant.org/ns/profiles/openid/vci/credential-offer` | your own system, client authentication | the caller |
+| `/idp/profile/openid/vci/token` | **OpenID.VCI.Token** | `http://geant.org/ns/profiles/openid/vci/token` | wallet | the wallet, or anonymous for unregistered wallets |
+| `/idp/profile/openid/vci/credentials` | **OpenID.VCI.Credentials** | `http://geant.org/ns/profiles/openid/vci/credentials` | wallet, access token | the wallet, or anonymous for unregistered wallets |
+| `/idp/profile/oauth/status-list/list` | **OAuth.StatusList** | `http://geant.org/ns/profiles/oauth/status-list` | verifier, no authentication | anonymous |
+| `/idp/profile/oauth/status-list/assignments` | **OAuth.StatusList.Assignments** | `http://geant.org/ns/profiles/oauth/status-list/assignments` | your own system, client authentication | the caller |
+| `/idp/profile/oauth/status-list/revoke` | **OAuth.StatusList.Revoke** | `http://geant.org/ns/profiles/oauth/status-list/revoke` | your own system, client authentication | the caller |
+
+Profiles of the OP plugin needed with this plugin:
+
+| Profile configuration bean | Needed for |
+|---|---|
+| **OIDC.SSO** | Authorization endpoint of the authorization code flow |
+| **OAUTH2.PAR** | Pushed authorization requests |
+| **OIDC.Keyset** | `jwks_uri` of the metadata documents, keys of url credentials without a chain |
+| **OIDC.Configuration** | `/.well-known/openid-configuration` |
+| **OAUTH2.TokenAudience** | `audience` values of wallet clients |
+| **OIDFED.Configuration** | Entity Configuration, of the federation plugin |
+
+The whole file as it is at the end of [First installation, by example](#first-installation-by-example)
+is in [Relying party configuration](#relying-party-configuration).
+
+### Profile configuration settings
+
+**File(s):** *conf/relying-party.xml*
+**Format:** Native Spring
+
+**OpenID.VCI.CredentialOffer**, **OpenID.VCI.Token**, **OpenID.VCI.Credentials**,
+**OpenID.VCI.Nonce**, **OAuth.StatusList.Revoke** and **OAuth.StatusList.Assignments** inherit the
+OAuth2 settings of the OP plugin, `tokenEndpointAuthMethods`, `accessTokenLifetime`, `forcePKCE`,
+`securityConfiguration` and DPoP settings among them. The rest serve requests without a client
+and inherit nothing of that. Each setting also has a `…LookupStrategy` variant that takes a
+`Function` returning the value.
+
+#### Common to all profiles
| Name | Type | Default | Description |
|---|---|---|---|
-| `preAuthorizedCodeLifetime` | Duration | `PT10M` | Lifetime of the pre-authorized code. |
-| `preAuthorizedCodeLength` | Integer | `0` | Length of the pre-authorized code. `0` is a self-contained code, `10` or more is a code that refers to an offer in storage. |
+| `inboundInterceptorFlows` | List<String> | | Run after the request is decoded and the profile is selected, before authentication. |
+| `outboundInterceptorFlows` | List<String> | | Run after the response message is formed, before it is committed. Also for an error response. |
+| `messageHandler` | Function<MessageContext,Exception> | | Runs on the inbound message context before authentication, and on the outbound one before encoding. |
-#### Wallet Attestation as client authentication
+#### OpenID.VCI.IssuerMetadata
-A wallet authenticates with the `OAuth-Client-Attestation` and `OAuth-Client-Attestation-PoP`
-headers when you add **openidvci.ClientAttestationValidator** to the validators of the
-`OAuth2Client` login flow, in *conf/authn/oauth2client-authn-config.xml*:
+| Name | Type | Default | Description |
+|---|---|---|---|
+| `issuerMetadataSignatureSigningConfiguration` | SignatureSigningConfiguration | **openidvci.issuerMetadata.SigningConfiguration** | Signing of the metadata. Published unsigned until **openidvci.issuerMetadata.SigningCredentials** exists. Signed metadata is served for `Accept: application/jwt`. |
-```xml
-<util:list id="shibboleth.authn.OAuth2Client.Validators">
- <ref bean="shibboleth.OIDCClientInfoValidator" />
- <ref bean="shibboleth.JWTValidator" />
- <ref bean="openidvci.ClientAttestationValidator" />
-</util:list>
-```
+**OAuth.JWTVCIssuer**, **OAuth.AuthorizationServer** and **DID.Document** have no settings of
+this plugin.
-The attestation is accepted when its `x5c` chain validates to one of the certificates of
-**openidvci.ClientAttestationTrustAnchors**, the proof of possession verifies with the key of
-the attestation's `cnf`, its `aud` is the issuer of this deployment and its `jti` has not been
-seen before. The client identifier is the `sub` of the attestation. The same validator serves
-the PAR endpoint of the OP when you wire it there.
+#### OpenID.VCI.CredentialOffer
-### OpenID.VCI.Token
+| Name | Type | Default | Description |
+|---|---|---|---|
+| `preAuthorizedCodeLifetime` | Duration | `PT10M` | Lifetime of the pre-authorized code. Property `openidvci.preAuthorizedCode.defaultLifetime`. |
+| `preAuthorizedCodeLength` | Integer | `0` | `0` seals the whole offer in the code, no storage needed. `10` or more is a short code of an offer in storage. Property `openidvci.preAuthorizedCode.defaultLength`. |
-Turns a code into an access token. A `POST` of `application/x-www-form-urlencoded` to
-`/idp/profile/openid/vci/token`.
+Request, a `POST` of `application/json`:
-<details>
-<summary>Example snippet of conf/relying-party.xml</summary>
+| Name | Type | Description |
+|---|---|---|
+| `preAuthorizedCredentials` | Object | Required. Keyed with credential configuration id, each an array of `{"path": [...], "value": ...}`. |
+| `txCode` | Object | `value`, and hints `input_mode` (`numeric` or `text`), `length` and `description`. `value` is not in the offer. |
+| `client_id` | String | Only where unauthenticated callers are accepted. |
-```xml
-<bean parent="RelyingPartyByName" c:relyingPartyIds="offer-caller/wallet">
- <property name="profileConfigurations">
- <list>
- ...
- <bean parent="OpenID.VCI.Token"
- p:accessTokenClaimsSetManipulationStrategy-ref="openidvci.TokenManipulationStrategy"
- p:tokenEndpointAuthMethods="#{{'none'}}" />
- </list>
- </property>
-</bean>
-```
+Response is the Credential Offer as an `openid-credential-offer://` deep link.
-</details>
+#### OpenID.VCI.Token
+
+| Name | Type | Default | Description |
+|---|---|---|---|
+| `grantTypes` | Set<String> | `authorization_code`, `urn:ietf:params:oauth:grant-type:pre-authorized_code` | Grants served. The wallet client has to have the grant in its `grant_types`. |
+| `accessTokenClaimsSetManipulationStrategy` | Function | | Of the OP plugin. Must be **openidvci.TokenManipulationStrategy**. |
+| `tokenEndpointAuthMethods` | Collection<String> | | Of the OP plugin. `none` for public wallets, `attest_jwt_client_auth` for Wallet Attestation. |
+
+#### OpenID.VCI.Credentials
+
+| Name | Type | Default | Description |
+|---|---|---|---|
+| `credentialLifetime` | Duration | `P180D` | Validity of a credential. Decides also the Status List. Not read for a credential stating `lifetime`. |
+| `credentialSignatureSigningConfiguration` | SignatureSigningConfiguration | **openidvci.SigningConfiguration** | Signing of credentials. |
+| `proofSignatureValidationConfiguration` | SignatureValidationConfiguration | **openidvci.ProofSignatureValidationConfiguration** | Validation of key proof signatures. |
+| `proofClaimsValidator` | ClaimsValidator | **openidvci.DefaultProofBodyClaimsValidator** | Validation of key proof claims. |
+| `proofNonceGenerator` | Function<ProfileRequestContext,String> | **openidvci.DefaultOpenIDVCINonceGenerator** | Nonce a key proof has to carry. Same generator as in **OpenID.VCI.Nonce**. `#{null}` in both for no nonce. |
+| `batchSize` | Integer | `2` | Largest number of key proofs in one request. Not read when the Credential Issuer metadata has `batch_credential_issuance`, its `batch_size` is used. Property `openidvci.batchSize`. |
-The relying party is the wallet, not the caller that created the offer. Put the same bean in
-**shibboleth.UnverifiedRelyingParty** if you accept wallets you have not registered.
+#### OpenID.VCI.Nonce
-The wallet forms the request and its parameters are the ones of the specification, so there is
-nothing here for you to call.
+| Name | Type | Default | Description |
+|---|---|---|---|
+| `proofNonceGenerator` | Function<ProfileRequestContext,String> | **openidvci.DefaultOpenIDVCINonceGenerator** | Generator of the nonce. Same as in **OpenID.VCI.Credentials**. |
-`grantTypes` decides which grants this endpoint serves. It defaults to `authorization_code` and
-`urn:ietf:params:oauth:grant-type:pre-authorized_code`, and a wallet's own `grant_types` of
-*metadata/oidc-client.json* has to name the one it uses.
+#### OAuth.StatusList
-`accessTokenClaimsSetManipulationStrategy` is a setting of the OP plugin and it has to name
-**openidvci.TokenManipulationStrategy**, see
-[Pre-authorized code flow](#pre-authorized-code-flow) and
-[Authorization code flow](#authorization-code-flow).
+| Name | Type | Default | Description |
+|---|---|---|---|
+| `statusListSignatureSigningConfiguration` | SignatureSigningConfiguration | **openidvci.status-list.SigningConfiguration** | Signing of Status List Tokens. |
-### OpenID.VCI.Credentials
+Request, a `GET` with `id`, the month and the issuer tag as in the `uri` of a credential. Response
+is `application/statuslist+jwt`. A month that has ended is `410 retired_status_list`, a value
+that is not a month `404 unknown_status_list`.
-Issues the credential. A `POST` of `application/json` to
-`/idp/profile/openid/vci/credentials` with the access token in the `Authorization` header, as
-`Bearer` or as `DPoP`.
+#### OAuth.StatusList.Assignments
-<details>
-<summary>Example snippet of conf/relying-party.xml</summary>
+No settings of this plugin. Request, a `POST` of `application/json`:
-```xml
-<bean parent="RelyingPartyByName" c:relyingPartyIds="offer-caller/wallet">
- <property name="profileConfigurations">
- <list>
- ...
- <bean parent="OpenID.VCI.Credentials" />
- </list>
- </property>
-</bean>
-```
+| Name | Type | Description |
+|---|---|---|
+| `from` | Integer | Position to read from, `0` or more. Default `0`. |
+| `limit` | Integer | Records to return, `1` to `1000`. Default `100`. |
+| `client_id` | String | Only where unauthenticated callers are accepted. |
-</details>
+Response has `records`, `next` and `high_water`. Give `next` as `from` of the next call.
-A Credential Configuration that carries `key_attestations_required` in its
-`proof_types_supported.jwt` makes a key attestation mandatory in every key proof of that
-credential. The attestation is read from the `key_attestation` header of the proof, its `x5c`
-chain has to validate to one of the certificates of **openidvci.KeyAttestationTrustAnchors**,
-and the proof itself has to be signed by one of the keys the attestation attests. The
-`key_storage` and `user_authentication` values you require are checked against the ones the
-attestation asserts. Define the anchors in *conf/global.xml* or in any other imported file:
+#### OAuth.StatusList.Revoke
-```xml
-<util:list id="openidvci.KeyAttestationTrustAnchors">
- <bean parent="shibboleth.BasicX509CredentialFactoryBean"
- p:entity="%{idp.home}/credentials/key-attestation-ca.crt"
- p:certificates="#{{'%{idp.home}/credentials/key-attestation-ca.crt'}}" />
-</util:list>
-```
+No settings of this plugin. Request, a `POST` of `application/json`, success is `204 No Content`:
+
+| Name | Type | Description |
+|---|---|---|
+| `id` | String | The month, `uuuu-MM`, with or without the issuer tag. |
+| `idx` | Integer | The slot, `0` to `1048575`. |
+| `client_id` | String | Only where unauthenticated callers are accepted. |
+
+#### OP plugin settings used with this plugin
+
+| Profile | Name | Value |
+|---|---|---|
+| **OIDC.SSO** | `authorizationCodeClaimsSetManipulationStrategy` | **openidvci.TokenManipulationStrategy** |
+| **OIDC.SSO** | `forcePKCE`, `requirePushedAuthorizationRequest` | `true` for DIIP and HAIP |
+| **OIDC.SSO** | `includeIssuerInResponse` | `true` for HAIP |
+| **OAUTH2.PAR** | `tokenEndpointAuthMethods` | same as the token endpoint of the wallet |
-Publishing `key_attestations_required` without anchors is a configuration error and the request
-is refused, so publish the member and the anchors together.
+### Credential Configuration settings
-The wallet forms the request and its parameters are the ones of the specification. Two things
-of it are worth knowing. Only key proofs of type `jwt` are supported and one credential is
-issued per proof, each of them taking a status list slot of its own when the credential takes
-slots at all. Member
-`credential_response_encryption` is parsed and written to the log and nothing else is done with
-it, an encrypted credential response is not implemented.
+**File(s):** *metadata/verifiable-credentials.json*
+**Format:** JSON
-One request asks for one Credential Configuration, so the validity window and the Status List
-slots of a batch are those of that one credential. The window itself may come from the
-credential rather than from the settings below, see
-[Settings of your own, per credential](#settings-of-your-own-per-credential).
+Member `_issuance_configuration` of a credential configuration. It is not published.
| Name | Type | Default | Description |
|---|---|---|---|
-| `credentialLifetime` | Duration | `P180D` | Validity of the credential. It also decides the Status List the credential lands in. Not read for a credential whose configuration states a `lifetime` of its own. |
-| `credentialSignatureSigningConfiguration` | SignatureSigningConfiguration | Bean named **openidvci.SigningConfiguration** | Signing configuration of the credential. |
-| `proofSignatureValidationConfiguration` | SignatureValidationConfiguration | Bean named **openidvci.ProofSignatureValidationConfiguration** | Validation configuration of the key proof. |
-| `proofClaimsValidator` | ClaimsValidator | Bean named **openidvci.DefaultProofBodyClaimsValidator** | Validator of the claims of the key proof. |
-| `batchSize` | Integer | `2` | Largest number of key proofs one Credential Request may carry, so also the largest number of credentials one request issues and the largest number of Status List slots it consumes. Read only when the Credential Issuer metadata advertises no `batch_credential_issuance`; where that member exists its `batch_size` is enforced instead, so what a wallet is told is what it gets. Property `openidvci.batchSize`. |
-| `proofNonceGenerator` | Function<ProfileRequestContext,String> | Bean named **openidvci.DefaultOpenIDVCINonceGenerator** | Generator of the nonce a key proof has to carry. The same generator has to be in **OpenID.VCI.Nonce**, it is what hands the nonce out. |
+| `lifetime` | Duration | `credentialLifetime` | Length of the validity window. Overrides `credentialLifetime`. |
+| `not_before` | Duration, moment or date | instant of issuance | Where the validity window opens. `P30D`, `2026-08-01T06:00Z`, `2026-08-01T00:00+03:00` or `2026-08-01`. |
+| `status_list` | Boolean | `openidvci.status-list.includeByDefault` | Whether the credential takes a Status List slot. |
+| `issuer` | String | `openidvci.issuer` | `did:jwk`, `did:web`, `url` or the identifier itself. |
+| `oidfed` | Boolean | `true` | Whether the credential states the Entity Identifier, when *conf/openid-vci-oidfed.xml* is imported. |
-### OpenID.VCI.Nonce
+### Credential Issuer identifiers
-Hands out a nonce for a key proof. A `POST` to `/idp/profile/openid/vci/nonce`, no
-authentication and no request parameters.
+| Value | `iss` of the credential | Key found from | Needs | Status List url ends with |
+|---|---|---|---|---|
+| empty or `did:jwk`, the default | did:jwk of the signing key | the credential | nothing | `~did-jwk:<key thumbprint>` |
+| `did:web` | did:web of this deployment, from the issuer of the OP | `/.well-known/did.json`, header `kid` `did:web:…#<key name>` | **DID.Document** | `~did-web` |
+| `url` or the url itself | issuer of the OP | `x5c` when the key has certificates, otherwise `/.well-known/jwt-vc-issuer` | **OAuth.JWTVCIssuer** and the key in **shibboleth.oidc.SigningCredentialsToPublish**, when no `x5c` | `~url` |
-<details>
-<summary>Example snippet of conf/relying-party.xml</summary>
+The Status List Token of a credential names the same issuer as the credential.
-```xml
-<bean id="shibboleth.UnverifiedRelyingParty" parent="RelyingParty">
- <property name="profileConfigurations">
- <list>
- ...
- <ref bean="OpenID.VCI.Nonce" />
- </list>
- </property>
-</bean>
-```
+### Signing keys
-</details>
+**File(s):** *conf/openid-vci-credentials.xml*
+**Format:** Native Spring
-| Name | Type | Default | Description |
+The file is installed but not imported. Import it into *conf/credentials.xml*, or into
+*conf/global.xml* when *conf/openid-vci-oidfed.xml* is imported.
+
+| List | Signs | When undefined |
+|---|---|---|
+| **openidvci.SigningCredentials** | credentials | signing credentials of the OP |
+| **openidvci.status-list.SigningCredentials** | Status List Tokens | **openidvci.SigningCredentials**, then the ones of the OP |
+| **openidvci.issuerMetadata.SigningCredentials** | Credential Issuer metadata | nothing, published unsigned |
+
+A list replaces the credentials of the OP, it does not add to them. The first key that fits the
+algorithm signs. The others are published in the did:web document and the Entity Configuration,
+and sign Status List Tokens of did:jwk credentials they signed. EC keys only. A key with
+certificates emits `x5c` when the issuer is an https url. See
+[Step 6](#step-6-rolling-the-signing-key).
+
+### Beans
+
+Beans you may define or refer to. Define your own in *conf/global.xml* or in any file imported.
+
+| Bean ID | Type | Default | Function |
|---|---|---|---|
-| `proofNonceGenerator` | Function<ProfileRequestContext,String> | Bean named **openidvci.DefaultOpenIDVCINonceGenerator** | Generator of the nonce. Whatever **OpenID.VCI.Credentials** validates with has to be the same. |
+| **openidvci.TokenManipulationStrategy** | Function | built in | Carries authorization details and claim values into the authorization code and the access token. Referenced from **OIDC.SSO** and **OpenID.VCI.Token**. |
+| **openidvci.CredentialConfigurationsResolver** | CredentialConfigurationsResolver | built in | Reads *metadata/verifiable-credentials.json*. |
+| **openidvci.SigningCredentials** | List<Credential> | | Keys signing credentials. |
+| **openidvci.status-list.SigningCredentials** | List<Credential> | | Keys signing Status List Tokens. |
+| **openidvci.issuerMetadata.SigningCredentials** | List<Credential> | | Keys signing Credential Issuer metadata. Defining it enables signed metadata. |
+| **openidvci.SigningConfiguration** | SignatureSigningConfiguration | ES256, ES384, ES512 | Signing of credentials. |
+| **openidvci.status-list.SigningConfiguration** | SignatureSigningConfiguration | as above | Signing of Status List Tokens. |
+| **openidvci.issuerMetadata.SigningConfiguration** | SignatureSigningConfiguration | as above | Signing of Credential Issuer metadata. |
+| **openidvci.ProofSignatureValidationConfiguration** | SignatureValidationConfiguration | built in | Validation of key proof signatures. |
+| **openidvci.DefaultProofBodyClaimsValidator** | ClaimsValidator | built in | Validation of key proof claims, `aud` against the published `credential_issuer`. |
+| **openidvci.DefaultOpenIDVCINonceGenerator** | Function | built in | Nonce of key proofs. |
+| **openidvci.KeyAttestationTrustAnchors** | List<Credential> | | Certificates key attestations are accepted under. Needed for `key_attestations_required`. |
+| **openidvci.KeyAttestationClaimsValidator** | ClaimsValidator | | Validation of key attestation claims, `nonce` among them. |
+| **openidvci.ClientAttestationValidator** | CredentialValidator | built in | Authenticates a wallet with `OAuth-Client-Attestation` headers. Add to **shibboleth.authn.OAuth2Client.Validators** before **openidvci.PublicClientValidator**. |
+| **openidvci.ClientAttestationTrustAnchors** | List<Credential> | | Certificates Wallet Attestations are accepted under. |
+| **openidvci.PublicClientValidator** | CredentialValidator | built in | Authenticates a public client, `none`. |
+| **openidvci.CredentialIssuerMetadata** | Resource | | Replaces the Credential Issuer metadata document wholesale. |
+| **openidvci.JWTVCIssuerMetadata** | Resource | | Replaces the JWT VC issuer metadata document wholesale. |
+| **openidvci.AuthorizationServerMetadata** | Resource | | Replaces the authorization server metadata document wholesale. |
+| **openidvci.issuerMetadata.TemplateContext** | Map | `$issuer`, `$baseUrl`, `$host` | Velocity variables of the Credential Issuer metadata template. |
+| **openidvci.jwtVcIssuer.TemplateContext** | Map | as above | The same for JWT VC issuer metadata. |
+| **openidvci.authorizationServer.TemplateContext** | Map | as above | The same for authorization server metadata. |
+| **openidvci.credentials.MappedErrors** | Map | built in | Events of the credential endpoint mapped to error responses. |
+| **openidvci.token.MappedErrors** | Map | built in | Events of the token endpoint mapped to error responses. |
-### OAuth.StatusList
+### Properties
-Publishes a Status List Token. A `GET` of `/idp/profile/oauth/status-list/list`, no
-authentication. The response is of media type `application/statuslist+jwt`.
+**File(s):** *conf/openid-vci.properties*
+**Format:** Spring Properties
-The `id` parameter names the list, and a verifier takes the whole url from the credential it is
-checking. When credentials of more than one Credential Issuer identifier are issued, that url
-also says which identifier the credential was issued under, and the token is published under the
-same one so that the two agree. See
-[Credential Issuer identifiers](#credential-issuer-identifiers).
+The file comes with the plugin and is loaded automatically.
-<details>
-<summary>Example snippet of conf/relying-party.xml</summary>
+#### General
-```xml
-<bean id="shibboleth.UnverifiedRelyingParty" parent="RelyingParty">
- <property name="profileConfigurations">
- <list>
- ...
- <ref bean="OAuth.StatusList" />
- </list>
- </property>
-</bean>
-```
+| Name | Type | Default | Function |
+|---|---|---|---|
+| `openidvci.issuer` | String | | Issuer of credentials and Status List Tokens. Empty or `did:jwk`, `did:web`, `url`, or the identifier itself. |
+| `openidvci.authn.flows` | Regex | `OpenIDVCIClient` | Login flows of the VCI endpoints. `OpenIDVCIClient` is the `OAuth2Client` flow with `attest_jwt_client_auth`. |
+| `openidvci.credentialTimestampPrecision` | Duration | `PT1H` | Instant of issuance is rounded down to this. Keep it shorter than the shortest lifetime. |
+| `openidvci.batchSize` | Integer | `2` | Default of `batchSize`. |
+| `openidvci.StorageService` | Bean ID | **shibboleth.StorageService** | Storage of Credential Offers. Server side storage. |
+| `openidvci.preAuthorizedCode.defaultLifetime` | Duration | `PT10M` | Lifetime of pre-authorized codes. |
+| `openidvci.preAuthorizedCode.defaultLength` | Integer | `0` | `0` self-contained, `10` or more stored. |
+
+#### Credential Configurations
+
+| Name | Type | Default | Function |
+|---|---|---|---|
+| `openidvci.credentialConfigurations.resource` | File pathname | `%{idp.home}/metadata/verifiable-credentials.json` | The Credential Configurations. |
+| `openidvci.credentialConfigurations.minRefreshDelay` | Duration | `PT5M` | Lower bound of re-reading the file. |
+| `openidvci.credentialConfigurations.maxRefreshDelay` | Duration | `PT4H` | Upper bound of re-reading the file. |
+
+#### Metadata documents
-</details>
+| Name | Type | Default | Function |
+|---|---|---|---|
+| `openidvci.issuerMetadata.template` | File pathname | `%{idp.home}/static/openid-credential-issuer.json` | Credential Issuer metadata template. |
+| `openidvci.issuerMetadata.templated` | Boolean | `true` | Whether the file is Velocity. `false` if it has literal `$` or `#`. |
+| `openidvci.issuerMetadata.resolver` | Bean ID | **openidvci.DefaultCredentialIssuerMetadataResolver** | Resolver of the document. |
+| `openidvci.issuerMetadata.resolver.values` | Bean ID | **openidvci.issuerMetadata.DefaultDynamicValueResolvers** | Members resolved outside the file. |
+| `openidvci.issuerMetadata.minRefreshDelay`, `…maxRefreshDelay` | Duration | `PT5M`, `PT4H` | Bounds of re-reading the file. |
+| `openidvci.issuerMetadata.signedLifetime` | Duration | | `exp` of signed metadata. Unset, no `exp`. |
+| `openidvci.jwtVcIssuer.template` | File pathname | `%{idp.home}/static/jwt-vc-issuer.json` | JWT VC issuer metadata template. |
+| `openidvci.jwtVcIssuer.templated` | Boolean | `true` | Whether the file is Velocity. |
+| `openidvci.jwtVcIssuer.resolver` | Bean ID | **openidvci.DefaultJWTVCIssuerMetadataResolver** | Resolver of the document. |
+| `openidvci.jwtVcIssuer.minRefreshDelay`, `…maxRefreshDelay` | Duration | `PT5M`, `PT4H` | Bounds of re-reading the file. |
+| `openidvci.authorizationServer.template` | File pathname | `%{idp.home}/static/oauth-authorization-server.json` | Authorization server metadata template. |
+| `openidvci.authorizationServer.templated` | Boolean | `true` | Whether the file is Velocity. |
+| `openidvci.authorizationServer.resolver` | Bean ID | **openidvci.DefaultAuthorizationServerMetadataResolver** | Resolver of the document. |
+| `openidvci.authorizationServer.minRefreshDelay`, `…maxRefreshDelay` | Duration | `PT5M`, `PT4H` | Bounds of re-reading the file. |
+
+#### Signing keys
+
+| Name | Type | Default | Function |
+|---|---|---|---|
+| `openidvci.signing.es.key` | File pathname | `%{idp.home}/credentials/openid-vci-signing-es.jwk` | Credential signing key, when enabled in *conf/openid-vci-credentials.xml*. EC. |
+| `openidvci.signing.status-list.es.key` | File pathname | `%{idp.home}/credentials/openid-vci-status-list-signing-es.jwk` | Status List Token signing key, when enabled. EC. |
+| `openidvci.signing.issuerMetadata.es.key` | File pathname | `%{idp.home}/credentials/openid-vci-issuer-metadata-signing-es.jwk` | Metadata signing key, when enabled. EC. |
-A verifier fetches the list with the `uri` of a credential, which names the list in query
-parameter `id` as a month. A value that is not a month is `404 unknown_status_list` and a month
-that has ended is `410 retired_status_list`.
+#### Status List
-| Name | Type | Default | Description |
+| Name | Type | Default | Function |
|---|---|---|---|
-| `statusListSignatureSigningConfiguration` | SignatureSigningConfiguration | Bean named **openidvci.status-list.SigningConfiguration** | Signing configuration of the Status List Token. |
+| `openidvci.status-list.index.permutationKey` | String | `change_this_to_secret` | Key of the slot permutation. **Must be changed** before issuing, and not changed after. Move it to *credentials/secrets.properties*. |
+| `openidvci.status-list.includeByDefault` | Boolean | `true` | Whether a credential stating no `status_list` takes a slot. |
+| `openidvci.status-list.token.defaultLifetime` | Duration | `PT1H` | Lifetime of Status List Tokens, how long a verifier may cache. |
+| `openidvci.status-list.assignment.retention` | Duration | `P7D` | How long an assignment record is kept after it was written. |
+| `openidvci.status-list.StorageService` | Bean ID | **shibboleth.StorageService** | Storage of the lists. Server side storage. |
+| `openidvci.status-list.assignment.StorageService` | Bean ID | **shibboleth.StorageService** | Storage of assignment records. |
+| `openidvci.status-list.index.StorageService` | Bean ID | **shibboleth.StorageService** | Storage of the slot counter. Most churn of the three. |
-### OAuth.StatusList.Revoke
+#### OpenID Federation
-Revokes one slot of a Status List. A `POST` of `application/json` to
-`/idp/profile/oauth/status-list/revoke` with client authentication. A success is
-`204 No Content`.
+| Name | Type | Default | Function |
+|---|---|---|---|
+| `openidvci.oidfed.decorateFederationEntity` | Boolean | `true` | Fill `display_name` and `logo_uri` of the federation entity from Credential Issuer metadata. |
-<details>
-<summary>Example snippet of conf/relying-party.xml</summary>
+#### Logging
-```xml
-<bean parent="RelyingPartyByName" c:relyingPartyIds="offer-caller">
- <property name="profileConfigurations">
- <list>
- ...
- <bean parent="OAuth.StatusList.Revoke"
- p:tokenEndpointAuthMethods="#{{'client_secret_basic'}}" />
- </list>
- </property>
-</bean>
-```
+| Name | Type | Default | Function |
+|---|---|---|---|
+| `openidvci.logging.credentials` | String | `OpenID.VCI.Credentials` | Logging id of the credentials flow. |
+| `openidvci.logging.credential-offer` | String | `OpenID.VCI.CredentialOffer` | Logging id of the credential-offer flow. |
+| `openidvci.logging.token` | String | `OpenID.VCI.Token` | Logging id of the token flow. |
+| `openidvci.logging.nonce` | String | `OpenID.VCI.Nonce` | Logging id of the nonce flow. |
+| `openidvci.logging.issuer-metadata` | String | `OpenID.VCI.IssuerMetadata` | Logging id of the issuer-metadata flow. |
+| `openidvci.logging.jwt-vc-issuer` | String | `OAuth.JWTVCIssuer` | Logging id of the jwt-vc-issuer flow. |
+| `openidvci.logging.authorization-server` | String | `OAuth.AuthorizationServer` | Logging id of the authorization-server flow. |
+| `openidvci.logging.did-document` | String | `DID.Document` | Logging id of the did:web document flow. |
+| `openidvci.logging.status-list.list` | String | `OAuth.StatusList` | Logging id of the status list flow. |
+| `openidvci.logging.status-list.revoke` | String | `OAuth.StatusList.Revoke` | Logging id of the revoke flow. |
+| `openidvci.logging.status-list.assignments` | String | `OAuth.StatusList.Assignments` | Logging id of the assignments flow. |
-</details>
+### Discovery documents
-| Name | Type | Description |
+**File(s):** *static/openid-credential-issuer.json*, *static/jwt-vc-issuer.json*, *static/oauth-authorization-server.json*, *etc/jetty-rewrite-rules.xml*
+**Format:** Velocity JSON, Jetty XML
+
+| Location | Endpoint | File |
|---|---|---|
-| `id` | String | The list holding the slot, a month as `uuuu-MM`. |
-| `idx` | Integer | The slot, from `0` to `1048575`. |
-| `client_id` | String | Identifies the caller when the request carries no client authentication. |
+| `/.well-known/openid-credential-issuer` | `/idp/profile/openid/vci/issuer-metadata` | *static/openid-credential-issuer.json* |
+| `/.well-known/oauth-authorization-server` | `/idp/profile/oauth/authorization-server` | *static/oauth-authorization-server.json* |
+| `/.well-known/jwt-vc-issuer` | `/idp/profile/oauth/jwt-vc-issuer` | *static/jwt-vc-issuer.json* |
+| `/.well-known/did.json` | `/idp/profile/did/document` | none, formed from the signing keys |
+| `/.well-known/openid-federation` | `/idp/profile/oidfed/entity-configuration` | of the federation plugin |
+
+The templates are Velocity, `$issuer`, `$baseUrl` and `$host` come from the issuer of the OP.
+`credential_configurations_supported` is added from *metadata/verifiable-credentials.json*. The
+Credential Issuer metadata endpoint answers `500 server_error` when `credential_endpoint` or
+`credential_configurations_supported` is missing. `batch_credential_issuance.batch_size` is read
+back as the batch size. `token_endpoint` of the authorization server metadata is the VCI one,
+the `/.well-known/openid-configuration` of the OP does not describe it.
+`dpop_signing_alg_values_supported` should be in step with `idp.oauth2.dpop.proofAlgorithms` of
+*conf/oidc.properties*. `require_pushed_authorization_requests` and
+`code_challenge_methods_supported` describe the OP, they are set on **OIDC.SSO**.
<details>
-<summary>Example of a call</summary>
+<summary>Contents of static/openid-credential-issuer.json, example</summary>
-```bash
-curl -i -u 'offer-caller:CHANGE-ME' \
- -H 'Content-Type: application/json' \
- -d '{"id": "2027-03", "idx": 41337}' \
- https://issuer.example.org/idp/profile/oauth/status-list/revoke
+```json
+{
+ "credential_issuer": "$baseUrl",
+ "authorization_servers": [
+ "$issuer"
+ ],
+ "credential_endpoint": "$baseUrl/idp/profile/openid/vci/credentials",
+ "nonce_endpoint": "$baseUrl/idp/profile/openid/vci/nonce",
+ "batch_credential_issuance": {
+ "batch_size": 2
+ },
+ "logo_uri": "https://issuer.example.org/images/logo.png",
+ "contacts": [
+ "credentials at example.org"
+ ],
+ "display": [
+ {
+ "locale": "en",
+ "name": "Incubator credentials",
+ "description": "Trust and Identity Incubator credential issuer"
+ },
+ {
+ "locale": "fi-FI",
+ "name": "Incubator credentials",
+ "description": "Trust and Identity Incubator -todistusten myöntäjä"
+ }
+ ]
+}
```
</details>
-The profile has no settings of this plugin. It does not check that the caller is the one who
-issued the credential holding the slot, so enable it only where revoking is allowed.
+<details>
+<summary>Contents of static/jwt-vc-issuer.json as it comes with the plugin</summary>
-### OAuth.StatusList.Assignments
+```json
+{
+ "issuer":"$issuer",
+ "jwks_uri":"$baseUrl/idp/profile/oidc/keyset"
+}
+```
-Returns a page of the records of status list assignments. A `POST` of `application/json` to
-`/idp/profile/oauth/status-list/assignments` with client authentication.
+</details>
<details>
-<summary>Example snippet of conf/relying-party.xml</summary>
+<summary>Contents of static/oauth-authorization-server.json as it comes with the plugin</summary>
-```xml
-<bean parent="RelyingPartyByName" c:relyingPartyIds="offer-caller">
- <property name="profileConfigurations">
- <list>
- ...
- <bean parent="OAuth.StatusList.Assignments"
- p:tokenEndpointAuthMethods="#{{'client_secret_basic'}}" />
- </list>
- </property>
-</bean>
+```json
+{
+ "issuer":"$issuer",
+ "authorization_endpoint":"$baseUrl/idp/profile/oidc/authorize",
+ "pushed_authorization_request_endpoint":"$baseUrl/idp/profile/oauth2/pushed-authorization",
+ "require_pushed_authorization_requests":true,
+ "token_endpoint":"$baseUrl/idp/profile/openid/vci/token",
+ "jwks_uri":"$baseUrl/idp/profile/oidc/keyset",
+ "token_endpoint_auth_methods_supported":[
+ "none",
+ "client_secret_basic",
+ "client_secret_post",
+ "client_secret_jwt"
+ ],
+ "response_types_supported":[
+ "code"
+ ],
+ "grant_types_supported":[
+ "authorization_code",
+ "urn:ietf:params:oauth:grant-type:pre-authorized_code"
+ ],
+ "code_challenge_methods_supported":[
+ "S256"
+ ],
+ "dpop_signing_alg_values_supported":[
+ "ES256",
+ "ES384",
+ "ES512",
+ "PS256",
+ "PS384",
+ "PS512"
+ ],
+ "scopes_supported":[
+ "openid"
+ ],
+ "authorization_details_types_supported":[
+ "openid_credential"
+ ]
+}
```
-</details>
+Add the credential configuration ids to `scopes_supported`. For Wallet Attestation add
+`attest_jwt_client_auth` to `token_endpoint_auth_methods_supported`, and
+`client_attestation_signing_alg_values_supported`,
+`client_attestation_pop_signing_alg_values_supported` and
+`authorization_response_iss_parameter_supported`.
-| Name | Type | Description |
-|---|---|---|
-| `from` | Integer | Position to read from, `0` or more. Defaults to `0`. |
-| `limit` | Integer | Records to return, from `1` to `1000`. Defaults to `100`. |
-| `client_id` | String | Identifies the caller when the request carries no client authentication. |
+</details>
<details>
-<summary>Example of a call</summary>
+<summary>Example contents of etc/jetty-rewrite-rules.xml</summary>
-```bash
-curl -u 'offer-caller:CHANGE-ME' \
- -H 'Content-Type: application/json' \
- -d '{"from": 0, "limit": 100}' \
- https://issuer.example.org/idp/profile/oauth/status-list/assignments
+```xml
+<?xml version="1.0"?>
+<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "https://jetty.org/configure_10_0.dtd">
+<Configure id="Rewrite" class="org.eclipse.jetty.rewrite.handler.RuleContainer">
+ <!-- Credential Issuer metadata comes from this plugin -->
+ <Call name="addRule">
+ <Arg>
+ <New class="org.eclipse.jetty.rewrite.handler.RewritePatternRule">
+ <Set name="pattern">/.well-known/openid-credential-issuer</Set>
+ <Set name="replacement">/idp/profile/openid/vci/issuer-metadata</Set>
+ </New>
+ </Arg>
+ </Call>
+ <!-- OP metadata comes from the OP plugin -->
+ <Call name="addRule">
+ <Arg>
+ <New class="org.eclipse.jetty.rewrite.handler.RewritePatternRule">
+ <Set name="pattern">/.well-known/openid-configuration</Set>
+ <Set name="replacement">/idp/profile/oidc/configuration</Set>
+ </New>
+ </Arg>
+ </Call>
+ <!-- These two come from this plugin as well -->
+ <Call name="addRule">
+ <Arg>
+ <New class="org.eclipse.jetty.rewrite.handler.RewritePatternRule">
+ <Set name="pattern">/.well-known/jwt-vc-issuer</Set>
+ <Set name="replacement">/idp/profile/oauth/jwt-vc-issuer</Set>
+ </New>
+ </Arg>
+ </Call>
+ <Call name="addRule">
+ <Arg>
+ <New class="org.eclipse.jetty.rewrite.handler.RewritePatternRule">
+ <Set name="pattern">/.well-known/oauth-authorization-server</Set>
+ <Set name="replacement">/idp/profile/oauth/authorization-server</Set>
+ </New>
+ </Arg>
+ </Call>
+ <!-- Needed only for did:web credentials -->
+ <Call name="addRule">
+ <Arg>
+ <New class="org.eclipse.jetty.rewrite.handler.RewritePatternRule">
+ <Set name="pattern">/.well-known/did.json</Set>
+ <Set name="replacement">/idp/profile/did/document</Set>
+ </New>
+ </Arg>
+ </Call>
+ <!-- Needed only when the federation plugin publishes an Entity Configuration -->
+ <Call name="addRule">
+ <Arg>
+ <New class="org.eclipse.jetty.rewrite.handler.RewritePatternRule">
+ <Set name="pattern">/.well-known/openid-federation</Set>
+ <Set name="replacement">/idp/profile/oidfed/entity-configuration</Set>
+ </New>
+ </Arg>
+ </Call>
+ <!-- The JSON-LD context of the vc+sd-jwt credential is a static file of Jetty -->
+ <Call name="addRule">
+ <Arg>
+ <New class="org.eclipse.jetty.rewrite.handler.RewritePatternRule">
+ <Set name="pattern">/credentials/GeantIncubatorDiploma_W3C</Set>
+ <Set name="replacement">/credentials/GeantIncubatorDiploma_W3C.json</Set>
+ </New>
+ </Arg>
+ </Call>
+ <!-- Wallets running in a browser need this -->
+ <Call name="addRule">
+ <Arg>
+ <New class="org.eclipse.jetty.rewrite.handler.HeaderPatternRule">
+ <Set name="pattern">/*</Set>
+ <Set name="headerName">Access-Control-Allow-Origin</Set>
+ <Set name="headerValue">*</Set>
+ </New>
+ </Arg>
+ </Call>
+</Configure>
```
</details>
-The profile has no settings of this plugin. The records themselves, what a page of them looks
-like and how to collect them are described in [Status List](#status-list).
-
### Relying party configuration
**File(s):** *conf/relying-party.xml*
**Format:** Native Spring
-Every profile of this plugin activated in one file. The client ids are the ones used in the
-examples of this document. `OIDC.Keyset`, `OIDC.Configuration`, `OAUTH2.TokenAudience`,
-`OIDC.SSO` and `OAUTH2.PAR` come with the OP plugin and the wallets need them.
+The file as it is at the end of [First installation, by example](#first-installation-by-example).
+
+<details>
+<summary>Example contents of conf/relying-party.xml</summary>
```xml
...
@@ -2144,6 +2168,8 @@ examples of this document. `OIDC.Keyset`, `OIDC.Configuration`, `OAUTH2.TokenAud
<ref bean="OpenID.VCI.Nonce" />
<!-- Anyone may read a Status List -->
<ref bean="OAuth.StatusList" />
+ <!-- Anyone may read the Entity Configuration, of the federation plugin -->
+ <ref bean="OIDFED.Configuration" />
<!-- The three below are for unregistered wallets running the code flow -->
<bean parent="OIDC.SSO"
p:authorizationCodeClaimsSetManipulationStrategy-ref="openidvci.TokenManipulationStrategy"
@@ -2205,145 +2231,26 @@ examples of this document. `OIDC.Keyset`, `OIDC.Configuration`, `OAUTH2.TokenAud
</property>
</bean>
+ <!-- HAIP wallets, the conformance suite, authenticating with a Wallet Attestation -->
+ <bean parent="RelyingPartyByName"
+ c:relyingPartyIds="#{{'conformance_client_1','conformance_client_2'}}">
+ <property name="profileConfigurations">
+ <list>
+ <bean parent="OIDC.SSO"
+ p:authorizationCodeClaimsSetManipulationStrategy-ref="openidvci.TokenManipulationStrategy"
+ p:forcePKCE="true" p:requirePushedAuthorizationRequest="true"
+ p:includeIssuerInResponse="true" />
+ <bean parent="OpenID.VCI.Token"
+ p:accessTokenClaimsSetManipulationStrategy-ref="openidvci.TokenManipulationStrategy"
+ p:tokenEndpointAuthMethods="#{{'attest_jwt_client_auth'}}" />
+ <bean parent="OpenID.VCI.Credentials" />
+ <bean parent="OAUTH2.PAR" p:tokenEndpointAuthMethods="#{{'attest_jwt_client_auth'}}" />
+ </list>
+ </property>
+ </bean>
+
</util:list>
...
```
-The wallet that redeems a pre-authorized code and the wallet that runs the code flow are two
-different clients here. You do not need both, the walkthrough above takes them one at a time.
-
-### Properties
-
-**File(s):** *conf/openid-vci.properties*
-**Format:** Spring Properties
-
-Every property below is optional unless its Default column is empty. The file comes with the
-plugin and is loaded automatically, you do not need to touch `idp.additionalProperties`.
-
-| Name | Type | Default | Description |
-|---|---|---|---|
-| `openidvci.issuer` | String | | Credential Issuer value used in credentials and in Status List Tokens. Empty and `did:jwk` both mean the did:jwk of the signing key, `did:web` the did:web of this deployment and `url` the issuer of the OP. Any other value is the identifier itself. A Credential Configuration is able to state an `issuer` of its own and override this per credential. See [Credential Issuer identifiers](#credential-issuer-identifiers). |
-| `openidvci.authn.flows` | String | `OpenIDVCIClient` | Regular expression matching the login flows to enable for VCI endpoints. `OpenIDVCIClient` is the `OAuth2Client` flow of the OP with `attest_jwt_client_auth` as a client authentication method. |
-| `openidvci.credentialTimestampPrecision` | Duration | `PT1H` | Granularity the instant of issuance is rounded down to, so that credentials of one batch carry the same `iat` and `exp`. A `not_before` and a `lifetime` are measured from the rounded instant. Must be shorter than the shortest credential lifetime issued. |
-| `openidvci.StorageService` | Bean ID | Bean named **shibboleth.StorageService** | Storage for Credential Offers. Requires server-side storage. |
-| `openidvci.preAuthorizedCode.defaultLifetime` | Duration | `PT10M` | Lifetime of the pre-authorized code. |
-| `openidvci.preAuthorizedCode.defaultLength` | Integer | `0` | Length of pre-authorized codes. `0` means untruncated and self-contained; `10` or more means a stored offer. |
-| `openidvci.credentialConfigurations.resource` | File pathname | `%{idp.home}/metadata/verifiable-credentials.json` | The Credential Configurations document. Also carries the per credential settings of [Settings of your own, per credential](#settings-of-your-own-per-credential). |
-| `openidvci.credentialConfigurations.minRefreshDelay` | Duration | `PT5M` | Lower bound on re-reading that file. |
-| `openidvci.credentialConfigurations.maxRefreshDelay` | Duration | `PT4H` | Upper bound on re-reading that file. |
-| `openidvci.issuerMetadata.template` | File pathname | `%{idp.home}/static/openid-credential-issuer.json` | Credential Issuer metadata document. |
-| `openidvci.issuerMetadata.templated` | Boolean | `true` | Whether to evaluate the document as Velocity. Set `false` if it contains literal `$` or `#`. |
-| `openidvci.issuerMetadata.resolver` | Bean ID | Bean named **openidvci.DefaultCredentialIssuerMetadataResolver** | Resolver of the published metadata. |
-| `openidvci.issuerMetadata.resolver.values` | Bean ID | Bean named **openidvci.issuerMetadata.DefaultDynamicValueResolvers** | Members resolved outside the metadata file. |
-| `openidvci.issuerMetadata.minRefreshDelay` | Duration | `PT5M` | Lower bound on re-reading the metadata file. |
-| `openidvci.issuerMetadata.maxRefreshDelay` | Duration | `PT4H` | Upper bound on re-reading the metadata file. |
-| `openidvci.issuerMetadata.signedLifetime` | Duration | | `exp` of signed metadata. Unset publishes it without one. |
-| `openidvci.jwtVcIssuer.template` | File pathname | `%{idp.home}/static/jwt-vc-issuer.json` | The JWT VC issuer metadata document. |
-| `openidvci.jwtVcIssuer.templated` | Boolean | `true` | Whether to evaluate that document as Velocity. |
-| `openidvci.jwtVcIssuer.resolver` | Bean ID | Bean named **openidvci.DefaultJWTVCIssuerMetadataResolver** | Resolver of that document. |
-| `openidvci.jwtVcIssuer.minRefreshDelay` | Duration | `PT5M` | Lower bound on re-reading that file. |
-| `openidvci.jwtVcIssuer.maxRefreshDelay` | Duration | `PT4H` | Upper bound on re-reading that file. |
-| `openidvci.authorizationServer.template` | File pathname | `%{idp.home}/static/oauth-authorization-server.json` | The authorization server metadata document. |
-| `openidvci.authorizationServer.templated` | Boolean | `true` | Whether to evaluate that document as Velocity. |
-| `openidvci.authorizationServer.resolver` | Bean ID | Bean named **openidvci.DefaultAuthorizationServerMetadataResolver** | Resolver of that document. |
-| `openidvci.authorizationServer.minRefreshDelay` | Duration | `PT5M` | Lower bound on re-reading that file. |
-| `openidvci.authorizationServer.maxRefreshDelay` | Duration | `PT4H` | Upper bound on re-reading that file. |
-| `openidvci.signing.es.key` | File pathname | `%{idp.home}/credentials/openid-vci-signing-es.jwk` | Signing key of issued credentials. Used only if enabled in *conf/openid-vci-credentials.xml*. Must be EC. |
-| `openidvci.signing.status-list.es.key` | File pathname | `%{idp.home}/credentials/openid-vci-status-list-signing-es.jwk` | Signing key of Status List Tokens. Must be EC. |
-| `openidvci.signing.issuerMetadata.es.key` | File pathname | `%{idp.home}/credentials/openid-vci-issuer-metadata-signing-es.jwk` | Signing key of Credential Issuer metadata. Must be EC. |
-| `openidvci.status-list.index.permutationKey` | String | `change_this_to_secret` | Key used in permuting status list indices. **Must be changed.** |
-| `openidvci.status-list.includeByDefault` | Boolean | `true` | Whether a credential whose configuration states no `status_list` takes a slot. `false` makes the Status List opt-in per credential. |
-| `openidvci.status-list.token.defaultLifetime` | Duration | `PT1H` | Lifetime of Status List Tokens. |
-| `openidvci.status-list.assignment.retention` | Duration | `P7D` | How long an assignment record is kept for collection after it was written. |
-| `openidvci.status-list.StorageService` | Bean ID | Bean named **shibboleth.StorageService** | Storage for status list state. Requires server-side storage. |
-| `openidvci.status-list.assignment.StorageService` | Bean ID | Bean named **shibboleth.StorageService** | Storage for slot assignments. |
-| `openidvci.status-list.index.StorageService` | Bean ID | Bean named **shibboleth.StorageService** | Storage for slots handed out. |
-| `openidvci.oidfed.decorateFederationEntity` | Boolean | `true` | Whether to fill `display_name` and `logo_uri` of the federation entity from Credential Issuer metadata. |
-| `openidvci.logging.credentials` | String | `OpenID.VCI.Credentials` | Logging id of the credentials flow. |
-| `openidvci.logging.credential-offer` | String | `OpenID.VCI.CredentialOffer` | Logging id of the credential-offer flow. |
-| `openidvci.logging.token` | String | `OpenID.VCI.Token` | Logging id of the token flow. |
-| `openidvci.logging.nonce` | String | `OpenID.VCI.Nonce` | Logging id of the nonce flow. |
-| `openidvci.logging.issuer-metadata` | String | `OpenID.VCI.IssuerMetadata` | Logging id of the issuer-metadata flow. |
-| `openidvci.logging.jwt-vc-issuer` | String | `OAuth.JWTVCIssuer` | Logging id of the jwt-vc-issuer flow. |
-| `openidvci.logging.authorization-server` | String | `OAuth.AuthorizationServer` | Logging id of the authorization-server flow. |
-| `openidvci.logging.did-document` | String | `DID.Document` | Logging id of the did:web document flow. |
-| `openidvci.logging.status-list.list` | String | `OAuth.StatusList` | Logging id of the status list flow. |
-| `openidvci.logging.status-list.revoke` | String | `OAuth.StatusList.Revoke` | Logging id of the revoke flow. |
-| `openidvci.logging.status-list.assignments` | String | `OAuth.StatusList.Assignments` | Logging id of the assignments flow. |
-
-#### Notes
-
-`openidvci.status-list.index.permutationKey` is the one property you must not leave to its
-shipped value. It decides the order in which status list slots are handed out and with a known
-key an observer is able to correlate a credential with the time it was issued. Treat it like
-the OP's `idp.oidc.subject.salt` and move it to *credentials/secrets.properties*.
-
-The three `status-list` storage services are separate on purpose. The index allocation has a
-lot of churn and you may point it to different storage than the long living assignments. Leave
-them all to `shibboleth.StorageService` until that becomes a problem.
-
-### Profile configuration settings
-
-**File(s):** *conf/relying-party.xml*
-**Format:** Native Spring
-
-Every setting that is specific to this plugin in one list. Which profile reads which one is
-named with the profile above.
-
-All the **OpenID.VCI.\*** beans except **OpenID.VCI.IssuerMetadata**, and also
-**OAuth.StatusList.Revoke** and **OAuth.StatusList.Assignments**, inherit the OAuth2/OIDC
-settings that are documented for the profile configurations of the OP plugin.
-`tokenEndpointAuthMethods`, `accessTokenLifetime`, `forcePKCE`, `securityConfiguration` and the
-DPoP settings are among them.
-
-#### Interceptor flows and message handlers
-
-| Name | Type | Runs |
-|---|---|---|
-| `inboundInterceptorFlows` | List<String> | After the request is decoded and the profile is selected, before authentication. |
-| `outboundInterceptorFlows` | List<String> | After the response message is formed, before it is committed. Also for an error response. |
-| `messageHandler` | Function<MessageContext,Exception> | On the inbound message context before authentication, and on the outbound one before encoding. |
-
-| Name | Type | Default | Description |
-|---|---|---|---|
-| `preAuthorizedCodeLifetime` | Duration | `PT10M` | Lifetime of the pre-authorized code. |
-| `preAuthorizedCodeLength` | Integer | `0` | Length of the pre-authorized code. `0` is self-contained; `10` or more is stored. |
-| `credentialLifetime` | Duration | `P180D` | Validity of an issued credential. A credential whose configuration states a `lifetime` of its own does not read this. |
-| `credentialSignatureSigningConfiguration` | SignatureSigningConfiguration | Bean named **openidvci.SigningConfiguration** | Signing configuration for issued credentials. |
-| `proofSignatureValidationConfiguration` | SignatureValidationConfiguration | Bean named **openidvci.ProofSignatureValidationConfiguration** | Validation configuration for the wallet's key proof. |
-| `proofClaimsValidator` | ClaimsValidator | Bean named **openidvci.DefaultProofBodyClaimsValidator** | Validator of the key proof's claims. |
-| `proofNonceGenerator` | Function<ProfileRequestContext,String> | Bean named **openidvci.DefaultOpenIDVCINonceGenerator** | Generator of the nonce a key proof must carry. |
-| `batchSize` | Integer | `2` | Largest number of key proofs one Credential Request may carry. Overridden by `batch_credential_issuance.batch_size` of the Credential Issuer metadata when that member is present. **OpenID.VCI.Credentials** only. |
-| `issuerMetadataSignatureSigningConfiguration` | SignatureSigningConfiguration | Bean named **openidvci.issuerMetadata.SigningConfiguration** | Signing configuration for issuer metadata. **OpenID.VCI.IssuerMetadata** only. |
-| `statusListSignatureSigningConfiguration` | SignatureSigningConfiguration | Bean named **openidvci.status-list.SigningConfiguration** | Signing configuration for Status List Tokens. **OAuth.StatusList** only. |
-
-Each of them accepts also a `…LookupStrategy` variant that takes a `Function` returning the
-value, in the usual Shibboleth way.
-
-`credentialLifetime` is the only one of them a Credential Configuration is able to override, and
-it does so with the `lifetime` of its `_issuance_configuration` member. See
-[Settings of your own, per credential](#settings-of-your-own-per-credential) for that member and
-for the validity window it decides.
-
-### Beans
-
-Beans that come with the plugin. You may refer to them or replace them. You may place your own
-definitions in *conf/global.xml* or in any other location that is imported.
-
-| Name | Type | Description |
-|---|---|---|
-| **openidvci.TokenManipulationStrategy** | Function | Carries authorization details and claim values into the authorization code and access token. Reference it from **OIDC.SSO** and **OpenID.VCI.Token**. |
-| **openidvci.CredentialConfigurationsResolver** | CredentialConfigurationsResolver | Reads *metadata/verifiable-credentials.json*, shared by every flow. |
-| **openidvci.CredentialIssuerMetadata** | Resource | Define to replace the issuer metadata document wholesale. |
-| **openidvci.JWTVCIssuerMetadata** | Resource | Define to replace the JWT VC issuer metadata document wholesale. |
-| **openidvci.AuthorizationServerMetadata** | Resource | Define to replace the authorization server metadata document wholesale. |
-| **openidvci.issuerMetadata.TemplateContext** | Map | Define to add or replace the `$issuer`, `$baseUrl` and `$host` Velocity variables. |
-| **openidvci.jwtVcIssuer.TemplateContext** | Map | The same for the JWT VC issuer metadata document. |
-| **openidvci.authorizationServer.TemplateContext** | Map | The same for the authorization server metadata document. |
-| **openidvci.KeyAttestationTrustAnchors** | List<Credential> | Certificates a key attestation is accepted under. Define to accept key attestations. |
-| **openidvci.KeyAttestationClaimsValidator** | ClaimsValidator | Define to validate the claims of a key attestation, the `nonce` of it among them. |
-| **openidvci.ClientAttestationValidator** | CredentialValidator | Authenticates a wallet with the `OAuth-Client-Attestation` headers. Add it to **shibboleth.authn.OAuth2Client.Validators**. |
-| **openidvci.ClientAttestationTrustAnchors** | List<Credential> | Certificates a client attestation is accepted under. Define to use the validator above. |
-| **openidvci.SigningCredentials** | List<Credential> | Define in *conf/openid-vci-credentials.xml* to sign credentials with your own key. Replaces the OP's credentials. |
-| **openidvci.status-list.SigningCredentials** | List<Credential> | Define to sign Status List Tokens with a key of their own. |
-| **openidvci.issuerMetadata.SigningCredentials** | List<Credential> | Define to enable signed issuer metadata. |
+</details>
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list