[java-idp-plugin-vci] 02/05: First effort to create instructions

Codeberg noreply at shibboleth.net
Mon Sep 7 14:32:28 UTC 2026


This is an automated email from the git hooks/post-receive script.

codeberg pushed a commit to branch dev/FirstInstructions
in repository java-idp-plugin-vci.

View the commit online:
https://codeberg.org/Shibboleth/java-idp-plugin-vci/commit/ec70045d5cb8b368fbf84fe8d95ff72d603f0e15

commit ec70045d5cb8b368fbf84fe8d95ff72d603f0e15
Author: Janne Lauros <janne.lauros at csc.fi>
AuthorDate: Mon Sep 7 17:07:29 2026 +0300

    First effort to create instructions
---
 README.md | 1644 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 1640 insertions(+), 4 deletions(-)

diff --git a/README.md b/README.md
index dbaee63..c7321c3 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,1641 @@
-# `java-idp-plugin-vci`
-Plugin that will implement [OpenID for Verifiable Credential Issuance](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html) for Shibboleth IdP. 
+# OpenID for Verifiable Credential Issuance
 
-# Status
-Initial work has been done to implement  [Pre-Authorized Code Flow](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-pre-authorized-code-flow ) and  [Code Flow](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-authorization-code-flow ) flows. Both flows have been executed successfully delivering SD JWT credential to a wallet. Plugin is not compliant with any standards and is not safe to deploy yet. Deploying plugin requires many [...]
+> **Note**
+> This plugin is under construction and all the features are still subject to change.
+
+## Overview
+
+Plugin that implements [OpenID for Verifiable Credential Issuance](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html)
+for Shibboleth Identity Provider. It makes the IdP a Credential Issuer. A wallet gets an access
+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.
+
+Both issuance flows of the specification 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
+  authenticated and the wallet redeems the code of that offer
+- the [Authorization Code Flow](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-authorization-code-flow),
+  where the wallet makes a normal OpenID Connect authorization request and the credential
+  content comes in an IdP attribute
+
+Two credential formats are issued, `dc+sd-jwt` (IETF SD-JWT VC) and `vc+sd-jwt` (W3C VCDM 2.0
+enveloped in SD-JWT). Both do selective disclosure. Revocation is published as
+[IETF Token Status List](https://datatracker.ietf.org/doc/draft-ietf-oauth-status-list/).
+
+Things you need to know about this add-on:
+
+- It is not certified against any conformance suite. Do not deploy it in production.
+- Credentials are signed with EC keys only. The signing actions do not support anything else.
+- `did:web` is not supported as Credential Issuer identifier. The issuer is either `did:jwk` or
+  the plain issuer URL.
+
+## Plugin Installation
+
+> **Info**
+> This plugin is not released and has no `plugin.sh -I` identifier yet. Until it is released,
+> the distribution is in directory *download/* of this repository and you install it from
+> there by url, see below.
+
+| Plugin | Plugin ID | Module(s) | Depends on | Bug Reporting |
+|---|---|---|---|---|
+| OpenID for Verifiable Credential Issuance | `org.geant.shibboleth.plugin.openidvci` | `OpenID-VCI` | `idp.oidc.OP.7` (SNAPSHOT), `idp.oidc.config.5` (SNAPSHOT) | janne.lauros at csc.fi |
+
+Requires IdP **5.2.3** or later.
+
+### Dependencies
+
+> **Note**
+> This plugin depends on OpenID Connect OP plugin and on what that plugin depends on. You need
+> to install them **first** and in this order. The plugin installer does not check it for you.
+>
+> 1. OpenID Connect Common Library (`oidc-common`)
+> 2. OpenID Connect Common Config (`idp-plugin-oidc-config`)
+> 3. OpenID Connect OP (`idp-plugin-oidc-op`)
+>
+> One dependency you need only in some cases:
+>
+> - **OpenID Federation.** 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 OpenID Federation Common Library (`oidfed-common`) and OpenID
+>   Federation OP plugin (`idp-plugin-oidfed-op`) before that import. If you do the import
+>   without them the IdP does not start.
+
+### Installation example
+
+The plugin and its dependencies are unreleased snapshots. The most convenient way to find links
+to the latest snapshots is the
+[Shibboleth IdP and Plugins status page](https://shibboleth.net/cgi-bin/projstatus.cgi). It has
+a `Current Snapshot` column with a link to each plugin.
+
+Snapshot distributions are signed with Shibboleth Jenkins' key. You may download the key from
+https://shibboleth.net/downloads/SHIBBOLETH_SNAPSHOT_PGP_KEYS and use it as the truststore.
+
+This plugin is not on the status page as it has no release. Its distribution, the signature of
+the distribution and the key that signed it are in directory *download/* of this repository:
+
+| File | Description |
+|---|---|
+| *shibboleth-idp-plugin-openid-vci-0.9.0-SNAPSHOT.tar.gz* | The distribution. |
+| *shibboleth-idp-plugin-openid-vci-0.9.0-SNAPSHOT.tar.gz.asc* | Signature of the distribution. `plugin.sh` fetches it from the same directory by itself. |
+| *keys.txt* | The key that signed the distribution. It becomes the truststore of the plugin. |
+
+The distribution is uploaded there by hand and seldom. Nothing builds it from every commit and
+there is no nightly build of it, so what you install is the state of some point in the past and
+not the head of this repository. If you need something newer than that you build the plugin
+yourself.
+
+If your IdP image ships released versions of the OIDC plugins you need to remove them first. A
+released plugin and a snapshot of the same plugin cannot live side by side.
+
+<details>
+<summary>Example installation of the plugin and every dependency</summary>
+
+```bash
+cd /opt/shibboleth-idp
+wget https://shibboleth.net/downloads/SHIBBOLETH_SNAPSHOT_PGP_KEYS
+
+# Remove released versions shipped in the base image.
+bin/plugin.sh -r net.shibboleth.idp.plugin.oidc.op
+bin/plugin.sh -r net.shibboleth.idp.plugin.oidc.config
+
+# Dependencies, in this order.
+bin/plugin.sh --noCheck --truststore SHIBBOLETH_SNAPSHOT_PGP_KEYS \
+  -i https://build.shibboleth.net/maven/snapshots/net/shibboleth/oidc/oidc-common-dist/3.4.0-SNAPSHOT/oidc-common-dist-3.4.0-SNAPSHOT.tar.gz
+bin/plugin.sh --noCheck --truststore SHIBBOLETH_SNAPSHOT_PGP_KEYS \
+  -i https://build.shibboleth.net/maven/snapshots/net/shibboleth/idp/plugin/config/oidc/idp-plugin-oidc-config-dist/3.1.0-SNAPSHOT/idp-plugin-oidc-config-dist-3.1.0-SNAPSHOT.tar.gz
+bin/plugin.sh --noCheck --truststore SHIBBOLETH_SNAPSHOT_PGP_KEYS \
+  -i https://build.shibboleth.net/maven/snapshots/net/shibboleth/idp/plugin/oidc/idp-plugin-oidc-op-distribution/4.4.0-SNAPSHOT/idp-plugin-oidc-op-distribution-4.4.0-SNAPSHOT.tar.gz
+
+# Optional. The authorization code flow example uses a ScriptedAttribute and IdP 5 ships no
+# JavaScript engine. Your base image may have this already, check with 'bin/plugin.sh -l'.
+bin/plugin.sh -I net.shibboleth.idp.plugin.nashorn
+
+# This plugin. The key that signed the distribution becomes the truststore.
+mkdir -p credentials/org.geant.shibboleth.plugin.openidvci
+wget -O credentials/org.geant.shibboleth.plugin.openidvci/truststore.asc \
+  https://codeberg.org/Shibboleth/java-idp-plugin-vci/raw/branch/main/download/keys.txt
+bin/plugin.sh \
+  -i https://codeberg.org/Shibboleth/java-idp-plugin-vci/raw/branch/main/download/shibboleth-idp-plugin-openid-vci-0.9.0-SNAPSHOT.tar.gz
+```
+
+Replace the dependency `-SNAPSHOT.tar.gz` urls with the dated snapshot builds from the status
+page. The last command downloads the distribution and its signature from the same directory and
+verifies the signature against the truststore that was written just above it, so it does not
+need `--noCheck`.
+
+</details>
+
+Installing the plugin activates module `OpenID-VCI` and creates six files:
+
+| File | Description |
+|---|---|
+| *conf/openid-vci.properties* | Every deployer-facing property. Auto-loaded by `idp.searchForProperties`. |
+| *conf/openid-vci-credentials.xml* | Which key signs credentials, Status List Tokens and issuer metadata. Inactive 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. You publish it yourself. |
+| *static/oauth-authorization-server.json* | Authorization server metadata of the VCI endpoints, for a wallet. You publish it yourself. |
+
+One file you need to write yourself, *metadata/verifiable-credentials.json*, the Credential
+Configurations. There is no template for it. See [Credential Configurations](#credential-configurations).
+
+## First installation, by example
+
+This chapter goes through one working 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. They also take shortcuts you
+should not copy to production and those are mentioned where they appear.
+
+`https://issuer.example.org` is your IdP's OIDC issuer everywhere below.
+
+### 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 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.
+
+<details>
+<summary>Example contents of metadata/verifiable-credentials.json</summary>
+
+```json
+{
+  "credential_configurations_supported": {
+    "GeantIncubatorDiploma_SDJWT": {
+      "format": "dc+sd-jwt",
+      "vct": "GeantIncubatorDiploma_SDJWT",
+      "scope": "GeantIncubatorDiploma_SDJWT",
+      "cryptographic_binding_methods_supported": ["did:jwk"],
+      "proof_types_supported": {
+        "jwt": {
+          "proof_signing_alg_values_supported": ["ES256"]
+        }
+      },
+      "credential_metadata": {
+        "display": [
+          {
+            "name": "Incubator Diploma",
+            "locale": "en",
+            "description": "Diploma issued by the Trust and Identity Incubator"
+          }
+        ],
+        "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" }] }
+        ]
+      }
+    },
+    "GeantIncubatorDiploma_W3C": {
+      "format": "vc+sd-jwt",
+      "scope": "GeantIncubatorDiploma_W3C",
+      "cryptographic_binding_methods_supported": ["did:jwk"],
+      "proof_types_supported": {
+        "jwt": {
+          "proof_signing_alg_values_supported": ["ES256"]
+        }
+      },
+      "credential_definition": {
+        "@context": [
+          "https://www.w3.org/ns/credentials/v2",
+          "https://issuer.example.org/credentials/GeantIncubatorDiploma_W3C"
+        ],
+        "type": [
+          "VerifiableCredential",
+          "GeantIncubatorDiploma_W3C"
+        ]
+      },
+      "credential_metadata": {
+        "display": [
+          {
+            "name": "Incubator Diploma, W3C",
+            "locale": "en",
+            "description": "Diploma issued by the Trust and Identity Incubator"
+          }
+        ],
+        "claims": [
+          { "path": ["credentialSubject","diploma"],     "display": [{ "name": "Diploma",        "locale": "en" }] },
+          { "path": ["credentialSubject","mail"],        "display": [{ "name": "Email",          "locale": "en" }] },
+          { "path": ["credentialSubject","eppn"],        "display": [{ "name": "Principal name", "locale": "en" }] },
+          { "path": ["credentialSubject","givenName"],   "display": [{ "name": "Given names",    "locale": "en" }] },
+          { "path": ["credentialSubject","familyName"],  "display": [{ "name": "Family name",    "locale": "en" }] },
+          { "path": ["credentialSubject","affiliation"], "display": [{ "name": "Affiliation",    "locale": "en" }] }
+        ]
+      }
+    }
+  }
+}
+```
+
+</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 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.
+
+Members that depend on the format:
+
+| Member | `dc+sd-jwt` | `vc+sd-jwt` |
+|---|---|---|
+| `vct` | **required** | not used |
+| `credential_definition` | not used | **required**, carries `@context` and `type` |
+| Claim `path` | flat, one segment: `["mail"]` | nested under `credentialSubject`: `["credentialSubject","mail"]` |
+
+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
+in it:
+
+<details>
+<summary>Example contents of webapps/root/credentials/GeantIncubatorDiploma_W3C.json</summary>
+
+```json
+{
+  "@context": {
+    "diploma": "https://vocabulary.example.org/terms#diploma",
+    "mail": "https://vocabulary.example.org/terms#mail",
+    "eppn": "https://vocabulary.example.org/terms#eppn",
+    "givenName": "https://vocabulary.example.org/terms#givenName",
+    "familyName": "https://vocabulary.example.org/terms#familyName",
+    "affiliation": "https://vocabulary.example.org/terms#affiliation"
+  }
+}
+```
+
+</details>
+
+You publish it in the url the `@context` names. It is a static file of Jetty like the discovery
+documents are, in *webapps/root/credentials/*, and the rewrite rule that drops the `.json` from
+the url is 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.
+
+#### Notes
+
+Do not set claims `"mandatory": true` while you are still experimenting. A mandatory claim
+makes the Credential Offer validation compare the offered paths against the configured ones for
+exact equality. Any difference rejects the offer instead of ignoring the extra claim.
+
+Claim nesting of `vc+sd-jwt` is done by the credential type when the credential is built, not
+by the paths you send in a Credential Offer. The `credentialSubject` prefix belongs to this
+file, not to the offer.
+
+### 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 and
+without the EC one there is nothing to sign a credential with.
+
+If you do take a key of your own into use you need to know what changes. Defining
+**openidvci.SigningCredentials** *replaces* the OP's credentials instead of adding to them. The
+key is not published in the OP's `jwks_uri` unless you add it also to
+**shibboleth.oidc.SigningCredentialsToPublish** in *conf/oidc-credentials.xml*. Nobody is able
+to verify a credential that is signed with an unpublished key. The exception is Credential
+Issuer identifier `did:jwk`, which is the default, as then the key travels inside the credential
+itself.
+
+You need to import the file where its beans are needed:
+
+- into *conf/credentials.xml* normally
+- into *conf/global.xml* instead, if you state this deployment in OpenID Federation. Publishing
+  the list in an Entity Configuration works only from the root context.
+
+### Pre-authorized code flow
+
+**File(s):** *metadata/oidc-client.json*, *conf/relying-party.xml*, *conf/openid-vci.properties*
+**Format:** JSON, Native Spring, Spring Properties
+
+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
+shown to the wallet, usually as a QR code. The credential content is decided at that moment.
+
+The caller is a confidential client. The wallet that redeems the code is a *second* and
+different identity. You register both:
+
+<details>
+<summary>Example contents of metadata/oidc-client.json</summary>
+
+```json
+[
+  {
+    "client_id": "offer-caller",
+    "client_name": "Credential offer caller",
+    "client_secret": "CHANGE-ME"
+  },
+  {
+    "client_id": "offer-caller/wallet",
+    "client_name": "Wallets redeeming a pre-authorized code minted by offer-caller",
+    "token_endpoint_auth_method": "none",
+    "grant_types": ["urn:ietf:params:oauth:grant-type:pre-authorized_code"]
+  }
+]
+```
+
+</details>
+
+The first of the three relying parties is the anonymous half of the deployment. A wallet reads
+the Credential Issuer metadata and asks for a nonce before it has any client identity, and the
+last three beans of that list 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 three are needed by the
+authorization code flow.
+
+The `/wallet` suffix is not a convention you may choose. 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. Register that exact string or the token
+request has no relying party.
+
+<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 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>
+              <bean parent="OpenID.VCI.CredentialOffer"
+                    p:tokenEndpointAuthMethods="#{{'client_secret_basic'}}" />
+              <bean parent="OAuth.StatusList.Revoke"
+                    p:tokenEndpointAuthMethods="#{{'client_secret_basic'}}" />
+              <bean parent="OAuth.StatusList.Assignments"
+                    p:tokenEndpointAuthMethods="#{{'client_secret_basic'}}" />
+          </list>
+      </property>
+    </bean>
+
+    <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'}}" />
+              <bean parent="OpenID.VCI.Credentials" p:dpopProofNonceGenerator="#{null}" />
+          </list>
+      </property>
+    </bean>
+...
+```
+
+</details>
+
+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:
+
+<details>
+<summary>Example of asking for a credential offer</summary>
+
+```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" },
+      { "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" }
+    ],
+    "GeantIncubatorDiploma_W3C": [
+      { "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" }
+    ]
+  },
+  "txCode": {
+    "input_mode": "numeric",
+    "length": 6,
+    "description": "The code we sent to your phone",
+    "value": "492851"
+  }
+}'
+```
+
+The response is the Credential Offer as a deep link, in one line and with no whitespace. This
+is what you put in the QR code. The `value` of the transaction code is not in it:
+
+```
+openid-credential-offer://?credential_offer={"credential_issuer":"https://issuer.example.org","credential_configuration_ids":["GeantIncubatorDiploma_SDJWT","GeantIncubatorDiploma_W3C"],"grants":{"urn:ietf:params:oauth:grant-type:pre-authorized_code":{"pre-authorized_code":"AAdzZWNyZXQxWKPQ...","tx_code":{"input_mode":"numeric","length":6,"description":"The code we sent to your phone"}}}}
+```
+
+</details>
+
+The offer asks for both credentials at once and the wallet then requests them one by one with
+the same access token. 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 show where the formats differ.
+
+Note that the paths are flat in both, also in the `vc+sd-jwt` one. The `credentialSubject`
+nesting of that format is done when the credential is built.
+
+Every configuration id of the body must exist in *metadata/verifiable-credentials.json* or the
+offer is rejected. There is no attribute resolution when the wallet later redeems the code. The
+values above are what is issued.
+
+#### Notes
+
+`openidvci.preAuthorizedCode.defaultLength` decides which one of two quite different codes you
+get. It is worth understanding before you pick a value.
+
+With the default `0` the code is **self-contained**. The whole offer is sealed inside it and no
+server side storage is used. Its single use is enforced by recording its identifier in
+`shibboleth.ReplayCache`, so the replay protection is only as good as that cache, i.e. in a
+cluster with memory backed storage it is per node. The code is also long, which makes a dense
+QR code.
+
+With value `10` or more the code is a short random identifier and the offer is kept in
+`openidvci.StorageService`. This needs real server side storage, gives you short codes and the
+offer is deleted when it is redeemed.
+
+`openidvci.TokenManipulationStrategy` in **OpenID.VCI.Token** is not decoration. It carries the
+credential content into the access token and without it no credential is issued. The bean comes
+with the plugin, you do not need to define it.
+
+`p:dpopProofNonceGenerator="#{null}"` turns off the server side DPoP nonce requirement. Wallet
+support for DPoP nonces is not very good, so start with it off.
+
+A wallet that redeems a pre-authorized code does not authenticate to the token endpoint, so
+`none` has to be in `idp.oidc.tokenEndpointAuthMethods`.
+
+### 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
+
+This flow starts with a Credential Offer as well, but of a different kind. It has no claim
+values in it, only the ids of the credentials the wallet may ask for. After it the wallet makes
+a normal authorization request, the user authenticates at the IdP as usual and the credential
+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 same url serves everybody
+and you may print it once as a QR code:
+
+```
+openid-credential-offer://?credential_offer={"credential_issuer":"https://issuer.example.org","credential_configuration_ids":["GeantIncubatorDiploma_SDJWT"],"grants":{"authorization_code":{}}}
+```
+
+The `authorization_code` grant is what tells the wallet to run this flow. What is issued comes
+from the attribute below, not from the offer.
+
+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:
+
+<details>
+<summary>Example contents of metadata/oidc-client.json</summary>
+
+```json
+[
+  {
+    "client_id": "id.animo.paradym",
+    "client_name": "Paradym Wallet",
+    "redirect_uris": ["https://paradym.id/invitation/redirect"],
+    "scope": "openid GeantIncubatorDiploma_SDJWT GeantIncubatorDiploma_W3C",
+    "audience": [
+      "credentials",
+      "urn:shibboleth.oidc.op.resource:https://issuer.example.org"
+    ],
+    "token_endpoint_auth_method": "none",
+    "response_types": ["code"],
+    "grant_types": [
+      "authorization_code",
+      "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.
+
+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:
+
+<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 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"
+              c:relyingPartyIds="#{{'id.animo.paradym'}}">
+          <property name="profileConfigurations">
+              <list>
+                  <bean parent="OIDC.SSO"
+                        p:authorizationCodeClaimsSetManipulationStrategy-ref="openidvci.TokenManipulationStrategy" />
+                  <bean parent="OpenID.VCI.Token"
+                        p:tokenEndpointAuthMethods="#{{'none'}}"
+                        p:accessTokenClaimsSetManipulationStrategy-ref="openidvci.TokenManipulationStrategy"
+                        p:dpopProofNonceGenerator="#{null}" />
+                  <bean parent="OpenID.VCI.Credentials" p:dpopProofNonceGenerator="#{null}" />
+                  <bean parent="OAUTH2.PAR" p:tokenEndpointAuthMethods="#{{'none'}}" />
+              </list>
+          </property>
+        </bean>
+
+    </util:list>
+...
+```
+
+</details>
+
+The *value* of the attribute is the serialized claim list. You define it as a
+`ScriptedAttribute` so that it may read the real attributes of the user:
+
+<details>
+<summary>Example snippet of conf/attribute-resolver.xml</summary>
+
+```xml
+<AttributeDefinition id="GeantIncubatorDiploma" xsi:type="ScriptedAttribute">
+  <InputAttributeDefinition ref="mail" />
+  <InputAttributeDefinition ref="eduPersonPrincipalName" />
+  <InputAttributeDefinition ref="givenName" />
+  <InputAttributeDefinition ref="sn" />
+  <InputAttributeDefinition ref="eduPersonAffiliation" />
+  <Script><![CDATA[
+      var credential = '{"requestedCredential":['
+        + '{"path":["diploma"],"value":"Scrum Master"},'
+        + '{"path":["mail"],"value":"' + mail.getValues().get(0) + '"},'
+        + '{"path":["eppn"],"value":"' + eduPersonPrincipalName.getValues().get(0) + '"},'
+        + '{"path":["givenName"],"value":"' + givenName.getValues().get(0) + '"},'
+        + '{"path":["familyName"],"value":"' + sn.getValues().get(0) + '"},'
+        + '{"path":["affiliation"],"value":"' + eduPersonAffiliation.getValues().get(0) + '"}'
+        + ']}';
+      GeantIncubatorDiploma.addValue(credential);
+  ]]></Script>
+  <AttributeEncoder xsi:type="oidc:OIDCString" name="GeantIncubatorDiploma_SDJWT" />
+  <AttributeEncoder xsi:type="oidc:OIDCString" name="GeantIncubatorDiploma_W3C" />
+</AttributeDefinition>
+```
+
+</details>
+
+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.
+
+Then you tell the OP plugin to embed the attribute into the access token in
+*conf/oidc.properties*:
+
+```properties
+# "Encoded" attributes are encrypted and embedded into the access token
+idp.oidc.encodedAttributes = GeantIncubatorDiploma
+```
+
+And you release it to the wallet in *conf/attribute-filter.xml*. This flow reads the
+**filtered** attribute set, so an attribute that is not released reaches nothing:
+
+<details>
+<summary>Example snippet of conf/attribute-filter.xml</summary>
+
+```xml
+<AttributeFilterPolicy id="GeantIncubatorDiplomaToWallets">
+    <PolicyRequirementRule xsi:type="Requester"
+        value="id.animo.paradym" />
+    <AttributeRule attributeID="GeantIncubatorDiploma">
+        <PermitValueRule xsi:type="ANY" />
+    </AttributeRule>
+</AttributeFilterPolicy>
+```
+
+</details>
+
+#### Notes
+
+This flow needs `openidvci.TokenManipulationStrategy` in two places. **OIDC.SSO** puts the
+credential content into the authorization code and **OpenID.VCI.Token** carries it from there
+into the access token. Miss either one and the wallet gets a token but no credential.
+
+Four things bind this attribute. Breaking any one of them fails quietly, the wallet gets a
+token and then no credential:
+
+- The `name` of `AttributeEncoder` must be **exactly** the credential configuration id. The
+  credential endpoint looks the value up with that string.
+- The attribute must be **single-valued**. The value is read as a string and `OIDCString` joins
+  multiple values with a space into something that does not parse anymore.
+- The value must be a JSON object with a **`requestedCredential`** array. This is *not* the
+  same shape the credential-offer endpoint takes, see the table below.
+- The attribute must be both resolved *and* released. It is easy to have one without the other.
+
+The two wire formats really are different and the difference is the most common thing to get
+wrong:
+
+| | Pre-authorized code flow | Authorization code flow |
+|---|---|---|
+| Where the claim list lives | credential-offer HTTP request body | value of an IdP attribute |
+| Keyed by configuration id | yes, under `preAuthorizedCredentials` | no, the attribute name carries it |
+| Wrapper around the array | none | **`requestedCredential`** |
+| Example | `{"preAuthorizedCredentials":{"GeantIncubatorDiploma_SDJWT":[…]}}` | `{"requestedCredential":[…]}` |
+
+About the script itself. Build the JSON by concatenating as in the example, not with backslash
+line continuations inside one string literal. A continued string keeps the indentation of every
+following line and that tempts you to clean it up with `credential.replaceAll("\\s+","")`. It
+strips also the spaces inside your values and turns `Scrum Master` into `ScrumMaster`.
+
+`getValues().get(0)` takes only the first value of a multi-valued attribute, which is usually
+what you want for `eduPersonAffiliation`. It throws if the attribute did not resolve at all, so
+guard the inputs that are not guaranteed to be there. A value with `"` or `\` in it breaks the
+JSON as well, escape them if they may come from a directory.
+
+### Status List
+
+**File(s):** *conf/relying-party.xml*, *conf/openid-vci.properties*
+**Format:** Native Spring, Spring Properties
+
+Every credential this plugin issues carries 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/).
+There is no per credential switch for it. The credential endpoint allocates a slot for every
+credential it signs, so the Status List is part of a working installation and not an extra you
+may skip.
+
+<details>
+<summary>Example of the status claim of an issued credential</summary>
+
+```json
+{
+  "status": {
+    "status_list": {
+      "idx": 41337,
+      "uri": "https://issuer.example.org/idp/profile/oauth/status-list/list?id=2027-03"
+    }
+  }
+}
+```
+
+</details>
+
+Withdrawing that credential is setting bit 41337 of list `2027-03`. The list a credential lands
+in follows from the month the credential **expires**, not from the month it was issued. With
+the default `credentialLifetime` of `P180D` a credential that is issued in September 2026
+belongs to the list of March 2027. One list has 1 048 576 slots.
+
+Three endpoints do the work and they have three different audiences:
+
+| Endpoint | Audience | Authentication | Request |
+|---|---|---|---|
+| `/idp/profile/oauth/status-list/list` | verifiers | none | `GET` with `?id=` |
+| `/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). **OAuth.StatusList** goes to
+**shibboleth.UnverifiedRelyingParty** as a verifier comes without registration.
+**OAuth.StatusList.Revoke** and **OAuth.StatusList.Assignments** go to the relying party of
+whoever is allowed to revoke. The `offer-caller` example above has both already.
+
+One property you need to change before you issue anything:
+
+```properties
+# Not the shipped value, and not changed afterwards
+openidvci.status-list.index.permutationKey = <a stable secret>
+```
+
+Slots are handed out as a keyed permutation of a counter, so the order in which credentials
+were issued cannot be read from their `idx`. The allocator does not initialise without a key
+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
+
+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 and nothing
+in the IdP reads them back.
+
+Shibboleth is not a register of issued credentials and it is not meant to become one. Somebody
+decides one day that a credential has to be withdrawn and the reason may be anything you are
+able to imagine, from a person being sacked to a whole department being closed. That decision
+is not made in Shibboleth and what is needed to make it is not in Shibboleth either. This is
+why the records are offered to the outside for a limited time only. You read them out, store
+them in the system that already knows about your people and make the decision there with
+tooling that is meant for it. When it is made, you call the revoke api of this plugin.
+
+You collect the records to whatever system of yours knows who holds which credential:
+
+<details>
+<summary>Example of collecting assignment records</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
+```
+
+```json
+{
+  "records": [
+    {
+      "seq": 17,
+      "id": "2027-03",
+      "idx": [41337],
+      "expires": 1804324320,
+      "details": {
+        "credential_configuration_id": "GeantIncubatorDiploma_SDJWT",
+        "format": "dc+sd-jwt",
+        "claims": {
+          "diploma": "Scrum Master",
+          "mail": "gemma.erasmus at example.org",
+          "eppn": "gemmaer at example.org"
+        }
+      }
+    }
+  ],
+  "next": 18,
+  "high_water": 18
+}
+```
+
+</details>
+
+Store `next` and give it back as `from` in the next call. `high_water` is where the records
+end, so `next == high_water` means you are up to date. Gaps in `seq` are normal. `expires` is
+the expiration of the credential that took the slots, which is also when revoking them stops
+mattering.
+
+#### Revoking a credential
+
+<details>
+<summary>Example of revoking one slot</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
+```
+
+```
+HTTP/1.1 204 No Content
+Cache-Control: no-store
+```
+
+</details>
+
+Revoking is idempotent and it goes one way only. Nothing in the plugin turns a slot back to
+valid.
+
+#### 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:
+
+<details>
+<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'
+```
+
+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",
+  "iat": 1788772320,
+  "exp": 1806537600,
+  "ttl": 3600,
+  "status_list": {
+    "bits": 1,
+    "lst": "eNrtwTEBAAAAwqD..."
+  }
+}
+```
+
+</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.
+
+`limit` may be 1000 at most and it defaults to 100. A read probes a limited number of
+positions, so a page may come back with fewer records than you asked while `next` is still
+short of `high_water`. Keep calling until the two meet, a short page is not the end.
+
+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.
+
+## Reference
+
+**File(s):** *conf/relying-party.xml*
+**Format:** Native Spring
+
+You enable an endpoint by adding its profile configuration bean to a relying party. Nothing is
+enabled by default.
+
+| 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/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** and **OpenID.VCI.Nonce**
+serve wallets and **OAuth.StatusList** serves verifiers. **OpenID.VCI.Token** and
+**OpenID.VCI.Credentials** belong there too if you accept unregistered wallets.
+
+<details>
+<summary>Example snippet of conf/relying-party.xml</summary>
+
+```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 ask for a nonce -->
+            <ref bean="OpenID.VCI.Nonce" />
+            <!-- Anyone may read a Status List -->
+            <ref bean="OAuth.StatusList" />
+        </list>
+    </property>
+</bean>
+```
+
+</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.
+
+### Discovery documents
+
+A wallet finds the issuer at `/.well-known/openid-credential-issuer`. The document comes from
+the issuer-metadata endpoint, which reads *static/openid-credential-issuer.json*. The file is a
+Velocity template and `$baseUrl`, `$issuer` and `$host` in it are resolved from the issuer of
+the OP. `credential_configurations_supported` is not in the file at all, it is taken from
+*metadata/verifiable-credentials.json* when the document is published:
+
+<details>
+<summary>Example contents of static/openid-credential-issuer.json</summary>
+
+```json
+{
+  "credential_issuer": "$baseUrl",
+  "authorization_servers": [
+    "$issuer"
+  ],
+  "credential_endpoint": "$baseUrl/idp/profile/openid/vci/credentials",
+  "nonce_endpoint": "$baseUrl/idp/profile/openid/vci/nonce",
+  "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>
+
+Two more documents that a wallet reads this plugin does not serve at all. Both of them come
+with the plugin as static files and you publish them yourself. They name no host, so you need
+to replace `{{ service_name }}` in them with yours.
+
+`/.well-known/jwt-vc-issuer` tells a verifier which keys sign your credentials. Its `issuer`
+must be the same value the credentials carry as `iss`:
+
+<details>
+<summary>Contents of static/jwt-vc-issuer.json as it comes with the plugin</summary>
+
+```json
+{
+   "issuer":"https://{{ service_name }}",
+   "jwks_uri":"https://{{ service_name }}/idp/profile/oidc/keyset"
+}
+```
+
+</details>
+
+The document matters when your Credential Issuer identifier is the plain issuer URL. With
+`did:jwk`, which is the default, the key travels inside the credential and nobody needs the
+document.
+
+`/.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. `scopes_supported` ships with `openid`
+only and you add to it every credential configuration id you want to be requestable with the
+code flow, `GeantIncubatorDiploma_SDJWT` and `GeantIncubatorDiploma_W3C` of the walkthrough above for
+instance:
+
+<details>
+<summary>Contents of static/oauth-authorization-server.json as it comes with the plugin</summary>
+
+```json
+{
+   "issuer":"https://{{ service_name }}",
+   "authorization_endpoint":"https://{{ service_name }}/idp/profile/oidc/authorize",
+   "pushed_authorization_request_endpoint":"https://{{ service_name }}/idp/profile/oauth2/pushed-authorization",
+   "token_endpoint":"https://{{ service_name }}/idp/profile/openid/vci/token",
+   "jwks_uri":"https://{{ service_name }}/idp/profile/oidc/keyset",
+   "token_endpoint_auth_methods_supported":[
+      "none",
+      "client_secret_basic",
+      "client_secret_post",
+      "client_secret_jwt"
+   ],
+   "response_types_supported":[
+      "code"
+   ],
+   "scopes_supported":[
+      "openid"
+   ],
+   "authorization_details_types_supported":[
+      "openid_credential"
+   ]
+}
+```
+
+</details>
+
+Then the paths. None of the three `/.well-known/` locations is an IdP path, so you wire them 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:
+
+<details>
+<summary>Example contents of etc/jetty-rewrite-rules.xml</summary>
+
+```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 are the static files you wrote -->
+  <Call name="addRule">
+    <Arg>
+      <New class="org.eclipse.jetty.rewrite.handler.RewritePatternRule">
+        <Set name="pattern">/.well-known/jwt-vc-issuer</Set>
+        <Set name="replacement">/.well-known/jwt-vc-issuer.json</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">/.well-known/oauth-authorization-server.json</Set>
+      </New>
+    </Arg>
+  </Call>
+  <!-- The JSON-LD context of the vc+sd-jwt credential -->
+  <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>
+
+You copy the two documents from *static/* to the root webapp of Jetty, to
+*webapps/root/.well-known/*, and the JSON-LD context of a `vc+sd-jwt` credential goes to
+*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.
+
+You need to do all of this only because the work is in progress. The plugin should publish
+every document a wallet reads and it should publish them in the paths the specifications name.
+Until it does, the paths are wired by hand and the two documents are yours to maintain, so
+expect this chapter to shrink.
+
+#### Notes
+
+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. Publish
+`/.well-known/oauth-authorization-server` with the VCI token endpoint in it.
+
+Each profile of this plugin is described below. Six of the eight 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** 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.
+
+### OpenID.VCI.IssuerMetadata
+
+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.
+
+<details>
+<summary>Example snippet of conf/relying-party.xml</summary>
+
+```xml
+<bean id="shibboleth.UnverifiedRelyingParty" parent="RelyingParty">
+    <property name="profileConfigurations">
+        <list>
+            ...
+            <ref bean="OpenID.VCI.IssuerMetadata" />
+        </list>
+    </property>
+</bean>
+```
+
+</details>
+
+| 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. |
+
+### OpenID.VCI.CredentialOffer
+
+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.
+
+<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>
+```
+
+</details>
+
+| Name | Type | Description |
+|---|---|---|
+| `preAuthorizedCredentials` | Object | 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. Exactly one of this and `codeCredentials`. |
+| `codeCredentials` | Array of String | The credential configuration ids of an authorization code offer. No values here, the claims are resolved from attributes when the user authenticates. Exactly one of this and `preAuthorizedCredentials`. |
+| `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. Read only together with `preAuthorizedCredentials`. |
+| `client_id` | String | Identifies the caller when the request carries no client authentication. Of use only where you accept unauthenticated clients. |
+
+Both `preAuthorizedCredentials` and `codeCredentials` in one request is an error and so is
+neither of them. Every credential configuration id has to exist in
+*metadata/verifiable-credentials.json*.
+
+<details>
+<summary>Example of a call</summary>
+
+```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" }
+    ]
+  }
+}'
+```
+
+A worked example with every claim and with a Transaction Code is in
+[Pre-authorized code flow](#pre-authorized-code-flow).
+
+</details>
+
+| 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. |
+
+### OpenID.VCI.Token
+
+Turns a code into an access token. A `POST` of `application/x-www-form-urlencoded` to
+`/idp/profile/openid/vci/token`.
+
+<details>
+<summary>Example snippet of conf/relying-party.xml</summary>
+
+```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>
+```
+
+</details>
+
+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.
+
+The wallet forms the request and its parameters are the ones of the specification, so there is
+nothing here for you to call.
+
+The profile has no settings of this plugin. `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).
+
+### OpenID.VCI.Credentials
+
+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`.
+
+<details>
+<summary>Example snippet of conf/relying-party.xml</summary>
+
+```xml
+<bean parent="RelyingPartyByName" c:relyingPartyIds="offer-caller/wallet">
+  <property name="profileConfigurations">
+      <list>
+          ...
+          <bean parent="OpenID.VCI.Credentials" p:dpopProofNonceGenerator="#{null}" />
+      </list>
+  </property>
+</bean>
+```
+
+</details>
+
+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. 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.
+
+| Name | Type | Default | Description |
+|---|---|---|---|
+| `credentialLifetime` | Duration | `P180D` | Validity of the credential. It also decides the Status List the credential lands in. |
+| `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. |
+| `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. |
+
+### OpenID.VCI.Nonce
+
+Hands out a nonce for a key proof. A `POST` to `/idp/profile/openid/vci/nonce`, no
+authentication and no request parameters.
+
+<details>
+<summary>Example snippet of conf/relying-party.xml</summary>
+
+```xml
+<bean id="shibboleth.UnverifiedRelyingParty" parent="RelyingParty">
+    <property name="profileConfigurations">
+        <list>
+            ...
+            <ref bean="OpenID.VCI.Nonce" />
+        </list>
+    </property>
+</bean>
+```
+
+</details>
+
+| Name | Type | Default | Description |
+|---|---|---|---|
+| `proofNonceGenerator` | Function<ProfileRequestContext,String> | Bean named **openidvci.DefaultOpenIDVCINonceGenerator** | Generator of the nonce. Whatever **OpenID.VCI.Credentials** validates with has to be the same. |
+
+### OAuth.StatusList
+
+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`.
+
+<details>
+<summary>Example snippet of conf/relying-party.xml</summary>
+
+```xml
+<bean id="shibboleth.UnverifiedRelyingParty" parent="RelyingParty">
+    <property name="profileConfigurations">
+        <list>
+            ...
+            <ref bean="OAuth.StatusList" />
+        </list>
+    </property>
+</bean>
+```
+
+</details>
+
+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`.
+
+| Name | Type | Default | Description |
+|---|---|---|---|
+| `statusListSignatureSigningConfiguration` | SignatureSigningConfiguration | Bean named **openidvci.status-list.SigningConfiguration** | Signing configuration of the Status List Token. |
+
+### OAuth.StatusList.Revoke
+
+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`.
+
+<details>
+<summary>Example snippet of conf/relying-party.xml</summary>
+
+```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>
+```
+
+</details>
+
+| Name | Type | Description |
+|---|---|---|
+| `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. |
+
+<details>
+<summary>Example of a call</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
+```
+
+</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.
+
+### OAuth.StatusList.Assignments
+
+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>
+<summary>Example snippet of conf/relying-party.xml</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>
+```
+
+</details>
+
+| 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>
+<summary>Example of a call</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
+```
+
+</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.
+
+```xml
+...
+    <bean id="shibboleth.UnverifiedRelyingParty" parent="RelyingParty">
+        <property name="profileConfigurations">
+            <list>
+                <ref bean="OIDC.Keyset" />
+                <ref bean="OIDC.Configuration" />
+                <ref bean="OAUTH2.TokenAudience" />
+                <!-- Anyone may read the Credential Issuer metadata -->
+                <ref bean="OpenID.VCI.IssuerMetadata" />
+                <!-- Anyone may ask for a nonce -->
+                <ref bean="OpenID.VCI.Nonce" />
+                <!-- Anyone may read a Status List -->
+                <ref bean="OAuth.StatusList" />
+                <!-- The three below are for unregistered wallets running the code flow -->
+                <bean parent="OIDC.SSO"
+                      p:authorizationCodeClaimsSetManipulationStrategy-ref="openidvci.TokenManipulationStrategy" />
+                <bean parent="OpenID.VCI.Token"
+                      p:tokenEndpointAuthMethods="#{{'none'}}"
+                      p:accessTokenClaimsSetManipulationStrategy-ref="openidvci.TokenManipulationStrategy" />
+                <bean parent="OpenID.VCI.Credentials" />
+            </list>
+        </property>
+    </bean>
+
+    <!-- This plugin does not need anything of shibboleth.DefaultRelyingParty -->
+
+    <util:list id="shibboleth.RelyingPartyOverrides">
+
+        <!-- The caller that creates Credential Offers and revokes what was issued -->
+        <bean parent="RelyingPartyByName" c:relyingPartyIds="offer-caller">
+          <property name="profileConfigurations">
+              <list>
+                  <bean parent="OpenID.VCI.CredentialOffer"
+                        p:tokenEndpointAuthMethods="#{{'client_secret_basic'}}" />
+                  <bean parent="OAuth.StatusList.Revoke"
+                        p:tokenEndpointAuthMethods="#{{'client_secret_basic'}}" />
+                  <bean parent="OAuth.StatusList.Assignments"
+                        p:tokenEndpointAuthMethods="#{{'client_secret_basic'}}" />
+              </list>
+          </property>
+        </bean>
+
+        <!-- Wallets that redeem a pre-authorized code created by offer-caller -->
+        <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'}}" />
+                  <bean parent="OpenID.VCI.Credentials" p:dpopProofNonceGenerator="#{null}" />
+              </list>
+          </property>
+        </bean>
+
+        <!-- A registered wallet running the authorization code flow -->
+        <bean parent="RelyingPartyByName"
+              c:relyingPartyIds="#{{'id.animo.paradym'}}">
+          <property name="profileConfigurations">
+              <list>
+                  <bean parent="OIDC.SSO"
+                        p:authorizationCodeClaimsSetManipulationStrategy-ref="openidvci.TokenManipulationStrategy" />
+                  <bean parent="OpenID.VCI.Token"
+                        p:tokenEndpointAuthMethods="#{{'none'}}"
+                        p:accessTokenClaimsSetManipulationStrategy-ref="openidvci.TokenManipulationStrategy"
+                        p:dpopProofNonceGenerator="#{null}" />
+                  <bean parent="OpenID.VCI.Credentials" p:dpopProofNonceGenerator="#{null}" />
+                  <bean parent="OAUTH2.PAR" p:tokenEndpointAuthMethods="#{{'none'}}" />
+              </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. Empty means `did:jwk`. |
+| `openidvci.authn.flows` | String | `OAuth2Client` | Regular expression matching the login flows to enable for VCI endpoints. |
+| `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. |
+| `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.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.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.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.
+
+| 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. |
+| `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. |
+| `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.
+
+### 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.issuerMetadata.TemplateContext** | Map | Define to add or replace the `$issuer`, `$baseUrl` and `$host` Velocity variables. |
+| **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. |

-- 
To stop receiving notification emails like this one, please contact
the administrator of this repository.


More information about the commits mailing list