[java-idp-plugin-vci] branch main updated: did:web as issuer, related endpoints. Reworking statuslist so that it is able to form response to all identifier types accordingly. Statuslist supports now also case od did:jwk key rollover by maintaining old keys

Codeberg noreply at shibboleth.net
Fri Sep 11 14:05:02 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/eb33f63ba1a68ccd575eb013ac4128086ddc2f9d

The following commit(s) were added to refs/heads/main by this push:
     new eb33f63  did:web as issuer, related endpoints. Reworking statuslist so that it is able to form response to all identifier types accordingly. Statuslist supports now also case od did:jwk key rollover by maintaining old keys
eb33f63 is described below

commit eb33f63ba1a68ccd575eb013ac4128086ddc2f9d
Author: Janne Lauros <janne.lauros at csc.fi>
AuthorDate: Fri Sep 11 17:04:48 2026 +0300

    did:web as issuer, related endpoints. Reworking statuslist so that it is able to form response to all identifier types accordingly. Statuslist supports now also case od did:jwk key rollover by maintaining old keys
---
 README.md                                          | 252 ++++++++++++++++++--
 .../CredentialIssuanceConfiguration.java           |  25 +-
 .../openidvci/statuslist/StatusListIssuerTag.java  | 130 +++++++++++
 .../statuslist/context/StatusListTokenContext.java |  27 +++
 .../impl/DefaultMetadataDocumentConfiguration.java |   8 +-
 .../FormOutboundDidDocumentResponseMessage.java    | 256 +++++++++++++++++++++
 .../openidvci/profile/impl/SignJWTCredential.java  |   4 +-
 .../logic/CredentialIssuerLookupFunction.java      |  84 ++++++-
 .../logic/StatusListIssuerTagLookupFunction.java   | 154 +++++++++++++
 .../openidvci/security/impl/DidJwkSupport.java     | 112 ---------
 .../plugin/openidvci/security/impl/DidSupport.java | 254 ++++++++++++++++++++
 .../statuslist/profile/impl/AllocateIndex.java     |  23 +-
 .../profile/impl/FormStatusListToken.java          | 148 ++++++++++--
 .../profile/impl/RevokeStatusListIndex.java        |   4 +-
 .../profile/impl/SignStatusListToken.java          |  94 +++++++-
 .../idp/flows/did/document/document-beans.xml      |  43 ++++
 .../idp/flows/did/document/document-flow.xml       |  22 ++
 .../oauth/status-list/assign/assign-beans.xml      |   8 +
 .../flows/oauth/status-list/list/list-beans.xml    |   9 +-
 .../openid/vci/credentials/credentials-beans.xml   |   3 +-
 .../idp/service/relying-party/postconfig.xml       |   9 +
 .../plugin/openidvci/conf/openid-vci.properties    |  10 +-
 .../shibboleth/plugin/openidvci/module.properties  |   7 +
 .../logic/CredentialIssuerLookupFunctionTest.java  |  30 +++
 .../openidvci/security/impl/DidJwkSupportTest.java |  93 --------
 .../openidvci/security/impl/DidSupportTest.java    | 131 +++++++++++
 26 files changed, 1674 insertions(+), 266 deletions(-)

diff --git a/README.md b/README.md
index bc9d1a7..bda40ff 100644
--- a/README.md
+++ b/README.md
@@ -32,8 +32,12 @@ 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.
+- `did:webvh` is not supported as Credential Issuer identifier. The identifier is `did:jwk`,
+  `did:web` or the plain issuer url. See
+  [Credential Issuer identifiers](#credential-issuer-identifiers).
+- A wallet that names its own key by `did:web` is not supported. The key proof of a holder is
+  read as a `did:jwk` or as an embedded `jwk`, and the credential binds to the holder the same
+  way.
 
 ## Plugin Installation
 
@@ -282,8 +286,9 @@ it.
 #### 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 and whether it goes on a Status List, and it is
-dropped from the published `credential_configurations_supported`, so a wallet never sees it.
+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.
 
 It goes inside the credential's own configuration, next to `format` and `vct`:
 
@@ -296,7 +301,8 @@ 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
+        "status_list": true,
+        "issuer": "did:web"
       },
       "credential_metadata": { "...": "..." }
     }
@@ -310,6 +316,7 @@ It goes inside the credential's own configuration, next to `format` and `vct`:
 | `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. |
 
 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.
@@ -440,10 +447,20 @@ If you do take a key of your own into use, defining **openidvci.SigningCredentia
 Credential Issuer identifier is `did:jwk`, which is the default. The key travels inside the
 credential itself and there is nothing to publish.
 
-Publishing starts to matter when you set `openidvci.issuer` to the plain issuer url instead.
-The key is then named rather than carried, so add it to
-**shibboleth.oidc.SigningCredentialsToPublish** in *conf/oidc-credentials.xml*. Nobody is able
-to verify a credential that is signed with a key they cannot find.
+Publishing starts to matter when the identifier names the key rather than carries it. Set
+`openidvci.issuer` to the plain issuer url and the key is named, so add it to
+**shibboleth.oidc.SigningCredentialsToPublish** in *conf/oidc-credentials.xml*, which is where
+`/.well-known/jwt-vc-issuer` points a verifier. Set it to `did:web` and there is nothing to add
+anywhere, as the document at `/.well-known/did.json` is formed from
+**openidvci.SigningCredentials** and **openidvci.status-list.SigningCredentials** themselves.
+Either way, nobody is able to verify a credential that is signed with a key they cannot find.
+See [Credential Issuer identifiers](#credential-issuer-identifiers) for what each identifier
+asks you to publish.
+
+With `did:web` those two lists decide what is verifiable, so they are also where key rollover
+happens. Add the new key to the list, let the document be read with both keys in it, start
+signing with the new one and remove the old one only once nothing unexpired was signed with it.
+A key removed while credentials still name it is a key a verifier no longer finds.
 
 Stating this deployment in OpenID Federation turns the list from optional into required. The
 Entity Configuration publishes those keys as the `jwks` of `vc_issuer`, and the bean that puts
@@ -456,6 +473,114 @@ You need to import the file where its beans are needed:
 - 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.
 
+### Credential Issuer identifiers
+
+**File(s):** *conf/openid-vci.properties*, *conf/relying-party.xml*
+**Format:** Spring Properties, Native Spring
+
+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.
+
+| `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` |
+
+Values other than the three are used as the identifier itself. The Status List Token of such a
+credential names the issuer of the OP.
+
+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).
+
+**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).
+
+#### Where a verifier finds the key
+
+| `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** |
+
+<details>
+<summary>Example of the document at /.well-known/did.json</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"
+  ]
+}
+```
+
+Both signing credential lists are stated, a key that appears in both only once.
+
+</details>
+
+#### Differences
+
+| | `did:jwk` | `did:web` | `url` |
+|---|---|---|---|
+| Anything to publish | no | yes, from the plugin | yes, from *static/* and the OP keyset |
+| Identifier survives a key rollover | **no** | yes | yes |
+| Same identifier for credentials and Status List Tokens | yes | yes | yes |
+| Old signing keys have to be kept | yes, while credentials live | no | no |
+| Credential points back at a resolvable issuer | no | yes | yes |
+
+A `did:jwk` identifier is the signing key, so rotating the key changes the identifier. `did:web`
+and `url` name this deployment, so the identifier outlives every key.
+
+#### Status Lists
+
+A Status List Token is published under the identifier of the credential pointing at it. The
+status list url carries which identifier that is, so a deployment issuing more than one kind of
+credential is answered correctly for each.
+
+Rotating the signing key with `did:web` or `url`: publish the new key alongside the old one as in
+[Signing keys](#signing-keys), nothing else.
+
+Rotating it with `did:jwk`: add the new key to **openidvci.status-list.SigningCredentials**
+instead of replacing the old one, and keep every key that has signed credentials there while
+those credentials are unexpired. A key removed too early makes the endpoint answer `500` for the
+credentials naming it, and the log names the key it looked for.
+
+`"status_list": false` keeps a credential off the list, see
+[Settings of your own, per credential](#settings-of-your-own-per-credential).
+
+#### 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*
@@ -960,6 +1085,11 @@ 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
+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.
 
@@ -1032,14 +1162,15 @@ enabled by default.
 | `/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 and **OAuth.StatusList**
-serves verifiers. **OpenID.VCI.Token** and
+**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.
 
 <details>
@@ -1055,6 +1186,8 @@ serves verifiers. **OpenID.VCI.Token** and
             <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 -->
@@ -1144,9 +1277,16 @@ works for as long as the OP has an EC key. Signing does ES256, ES384 and ES512 o
 
 #### 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` | The `iss` of a credential is the plain issuer url instead of the `did:jwk` of the signing key. Empty is the default and gives `did:jwk`, which carries the key inside the credential and needs nothing published. |
+| `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**. |
+| `issuer` of a credential | One Credential Configuration says something else than the deployment. Credentials of more than one identifier are issued side by side, each with a Status List of its own to match. |
 
 #### What the discovery documents say
 
@@ -1221,7 +1361,9 @@ side storage rather than the memory backed default of a cluster.
 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.
+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.
 
 | Document | Endpoint | File |
 |---|---|---|
@@ -1274,7 +1416,9 @@ cannot be read, so a broken file of credentials takes the metadata endpoint down
 `/.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 nobody reads the document.
+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).
 
 <details>
 <summary>Contents of static/jwt-vc-issuer.json as it comes with the plugin</summary>
@@ -1288,6 +1432,12 @@ key travels inside the credential and nobody reads the document.
 
 </details>
 
+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).
+
 `/.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.
 
@@ -1364,6 +1514,15 @@ responses, so that header belongs to the same file. With Jetty rewrite handler:
       </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>
@@ -1400,11 +1559,11 @@ advertises the OP token endpoint instead of `/idp/profile/openid/vci/token` and
 list your credential scopes. A wallet that reads only that document fails, so wire
 `/.well-known/oauth-authorization-server` to **OAuth.AuthorizationServer** as above.
 
-Each profile of this plugin is described below. Six of the ten inherit the settings of the
+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** and
-**OAuth.StatusList** are the exceptions. They serve requests that have no client at all and
+**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.
@@ -1496,6 +1655,54 @@ requestable with the code flow.
 
 There is nothing to configure per relying party.
 
+### DID.Document
+
+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.
+
+<details>
+<summary>Example snippet of conf/relying-party.xml</summary>
+
+```xml
+<bean id="shibboleth.UnverifiedRelyingParty" parent="RelyingParty">
+    <property name="profileConfigurations">
+        <list>
+            ...
+            <ref bean="DID.Document" />
+        </list>
+    </property>
+</bean>
+```
+
+</details>
+
+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.
+
+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`.
+
+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 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.
+
+There is nothing to configure per relying party. The document is the same for everyone.
+
 ### OpenID.VCI.CredentialOffer
 
 Creates a Credential Offer. A `POST` of `application/json` to
@@ -1661,6 +1868,12 @@ authentication and no request parameters.
 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`.
 
+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).
+
 <details>
 <summary>Example snippet of conf/relying-party.xml</summary>
 
@@ -1793,6 +2006,8 @@ examples of this document. `OIDC.Keyset`, `OIDC.Configuration`, `OAUTH2.TokenAud
                 <ref bean="OpenID.VCI.IssuerMetadata" />
                 <!-- Anyone may read the keys that signed a credential -->
                 <ref bean="OAuth.JWTVCIssuer" />
+                <!-- Anyone may read the did:web document, needed when openidvci.issuer is did:web -->
+                <ref bean="DID.Document" />
                 <!-- Anyone may read the authorization server metadata -->
                 <ref bean="OAuth.AuthorizationServer" />
                 <!-- Anyone may ask for a nonce -->
@@ -1874,7 +2089,7 @@ plugin and is loaded automatically, you do not need to touch `idp.additionalProp
 
 | Name | Type | Default | Description |
 |---|---|---|---|
-| `openidvci.issuer` | String | | Credential Issuer value used in credentials. Empty means `did:jwk`. |
+| `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 | `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. |
@@ -1917,6 +2132,7 @@ plugin and is loaded automatically, you do not need to touch `idp.additionalProp
 | `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. |
diff --git a/openid-vci-api/src/main/java/org/geant/shibboleth/plugin/openidvci/credential/CredentialIssuanceConfiguration.java b/openid-vci-api/src/main/java/org/geant/shibboleth/plugin/openidvci/credential/CredentialIssuanceConfiguration.java
index cf13e63..4726f8f 100644
--- a/openid-vci-api/src/main/java/org/geant/shibboleth/plugin/openidvci/credential/CredentialIssuanceConfiguration.java
+++ b/openid-vci-api/src/main/java/org/geant/shibboleth/plugin/openidvci/credential/CredentialIssuanceConfiguration.java
@@ -52,10 +52,16 @@ public final class CredentialIssuanceConfiguration {
     @Nullable
     private final Boolean statusList;
 
-    /** Whether credentials of this configuration are stated in OpenID Federation. */
+    /**
+     * Whether credentials of this configuration are stated in OpenID Federation.
+     */
     @Nullable
     private final Boolean oidFederation;
 
+    /** Credential Issuer identifier credentials of this configuration carry. */
+    @Nullable
+    private final String issuer;
+
     /**
      * Constructor.
      *
@@ -67,16 +73,21 @@ public final class CredentialIssuanceConfiguration {
      *                           Status List slot
      * @param inOIDFederation    whether credentials of this configuration are
      *                           stated in OpenID Federation
+     * @param issuerIdentifier   Credential Issuer identifier credentials of this
+     *                           configuration carry, one of 'did:jwk', 'did:web'
+     *                           and 'url', overriding the one of the deployment
      */
     @JsonCreator
     private CredentialIssuanceConfiguration(@JsonProperty("lifetime") @Nullable final String credentialLifetime,
             @JsonProperty("not_before") @Nullable final String validityStart,
             @JsonProperty("status_list") @Nullable final Boolean inStatusList,
-            @JsonProperty("oidfed") @Nullable final Boolean inOIDFederation) {
+            @JsonProperty("oidfed") @Nullable final Boolean inOIDFederation,
+            @JsonProperty("issuer") @Nullable final String issuerIdentifier) {
         lifetime = parseLifetime(credentialLifetime);
         notBefore = validityStart == null || validityStart.trim().isEmpty() ? null : ValidityStart.parse(validityStart);
         statusList = inStatusList;
         oidFederation = inOIDFederation;
+        issuer = issuerIdentifier == null || issuerIdentifier.trim().isEmpty() ? null : issuerIdentifier.trim();
     }
 
     /**
@@ -120,6 +131,16 @@ public final class CredentialIssuanceConfiguration {
         return oidFederation;
     }
 
+    /**
+     * Get the Credential Issuer identifier credentials of this configuration carry.
+     *
+     * @return the identifier, or null to take the one of the deployment
+     */
+    @Nullable
+    public String getIssuer() {
+        return issuer;
+    }
+
     /**
      * Resolve the instant the validity window of a credential opens.
      *
diff --git a/openid-vci-api/src/main/java/org/geant/shibboleth/plugin/openidvci/statuslist/StatusListIssuerTag.java b/openid-vci-api/src/main/java/org/geant/shibboleth/plugin/openidvci/statuslist/StatusListIssuerTag.java
new file mode 100644
index 0000000..f244eee
--- /dev/null
+++ b/openid-vci-api/src/main/java/org/geant/shibboleth/plugin/openidvci/statuslist/StatusListIssuerTag.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (c) 2025, GÉANT
+ *
+ * Licensed under the Apache License, Version 2.0 (the “License”); you may not
+ * use this file except in compliance with the License. You may obtain a copy
+ * of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an “AS IS” BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.geant.shibboleth.plugin.openidvci.statuslist;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+/**
+ * Tag naming the issuer a Status List Token is published under, as it travels
+ * in the url of a status list.
+ *
+ * Storage is keyed by the bucket alone, so the tag is a runtime matter only and
+ * every credential of a month shares one list. A url with no tag names no
+ * issuer.
+ */
+public final class StatusListIssuerTag {
+
+    /**
+     * Separator of the bucket and the tag. Unreserved, and no part of a bucket id.
+     */
+    @Nonnull
+    public static final String SEPARATOR = "~";
+
+    /** Tag of a list handed out under the plain issuer url. */
+    @Nonnull
+    public static final String URL = "url";
+
+    /** Tag of a list handed out under the did:web of this deployment. */
+    @Nonnull
+    public static final String DID_WEB = "did-web";
+
+    /** Tag of a list handed out under a did:jwk, followed by the key it names. */
+    @Nonnull
+    public static final String DID_JWK = "did-jwk";
+
+    /** Separator of the did:jwk tag and the thumbprint of the key. */
+    @Nonnull
+    private static final String THUMBPRINT_SEPARATOR = ":";
+
+    /** Constructor. */
+    private StatusListIssuerTag() {
+    }
+
+    /**
+     * Build the tag of a did:jwk issuer.
+     *
+     * @param thumbprint thumbprint of the key the identifier names
+     * @return the tag
+     */
+    @Nonnull
+    public static String didJwk(@Nonnull final String thumbprint) {
+        return DID_JWK + THUMBPRINT_SEPARATOR + thumbprint;
+    }
+
+    /**
+     * Build the value a status list url carries.
+     *
+     * @param bucketId identifier of the bucket
+     * @param tag      tag of the issuer, or null for none
+     * @return the value, the bucket alone when there is no tag
+     */
+    @Nonnull
+    public static String reference(@Nonnull final String bucketId, @Nullable final String tag) {
+        return tag == null || tag.isEmpty() ? bucketId : bucketId + SEPARATOR + tag;
+    }
+
+    /**
+     * Get the bucket of a value a status list url carries, the part storage is
+     * keyed by.
+     *
+     * @param reference value to read
+     * @return the bucket, or null when there is nothing to read
+     */
+    @Nullable
+    public static String bucketOf(@Nullable final String reference) {
+        if (reference == null) {
+            return null;
+        }
+        final int separator = reference.indexOf(SEPARATOR);
+        return separator < 0 ? reference : reference.substring(0, separator);
+    }
+
+    /**
+     * Get the tag of a value a status list url carries.
+     *
+     * @param reference value to read
+     * @return the tag, or null when the value carries none
+     */
+    @Nullable
+    public static String tagOf(@Nullable final String reference) {
+        if (reference == null) {
+            return null;
+        }
+        final int separator = reference.indexOf(SEPARATOR);
+        if (separator < 0 || separator + SEPARATOR.length() >= reference.length()) {
+            return null;
+        }
+        return reference.substring(separator + SEPARATOR.length());
+    }
+
+    /**
+     * Get the thumbprint a did:jwk tag names.
+     *
+     * @param tag tag to read
+     * @return the thumbprint, or null when the tag names no key
+     */
+    @Nullable
+    public static String thumbprintOf(@Nullable final String tag) {
+        if (tag == null || !tag.startsWith(DID_JWK + THUMBPRINT_SEPARATOR)) {
+            return null;
+        }
+        final String thumbprint = tag.substring(DID_JWK.length() + THUMBPRINT_SEPARATOR.length());
+        return thumbprint.isEmpty() ? null : thumbprint;
+    }
+
+}
diff --git a/openid-vci-api/src/main/java/org/geant/shibboleth/plugin/openidvci/statuslist/context/StatusListTokenContext.java b/openid-vci-api/src/main/java/org/geant/shibboleth/plugin/openidvci/statuslist/context/StatusListTokenContext.java
index bb165a5..b297b34 100644
--- a/openid-vci-api/src/main/java/org/geant/shibboleth/plugin/openidvci/statuslist/context/StatusListTokenContext.java
+++ b/openid-vci-api/src/main/java/org/geant/shibboleth/plugin/openidvci/statuslist/context/StatusListTokenContext.java
@@ -38,6 +38,13 @@ public class StatusListTokenContext extends BaseContext {
     @Nullable
     private String signedToken;
 
+    /**
+     * Thumbprint of the key the token has to be signed with, null to leave it to
+     * the signing configuration.
+     */
+    @Nullable
+    private String keyThumbprint;
+
     /**
      * Get claims of status list token, before signing.
      *
@@ -76,4 +83,24 @@ public class StatusListTokenContext extends BaseContext {
         signedToken = token;
     }
 
+    /**
+     * Get the thumbprint of the key the token has to be signed with.
+     *
+     * @return the thumbprint, or null to leave the key to the signing configuration
+     */
+    @Nullable
+    public String getKeyThumbprint() {
+        return keyThumbprint;
+    }
+
+    /**
+     * Set the thumbprint of the key the token has to be signed with.
+     *
+     * @param thumbprint the thumbprint, or null to leave it to the signing
+     *                   configuration
+     */
+    public void setKeyThumbprint(@Nullable final String thumbprint) {
+        keyThumbprint = thumbprint;
+    }
+
 }
diff --git a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/config/impl/DefaultMetadataDocumentConfiguration.java b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/config/impl/DefaultMetadataDocumentConfiguration.java
index c843be7..0639be5 100644
--- a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/config/impl/DefaultMetadataDocumentConfiguration.java
+++ b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/config/impl/DefaultMetadataDocumentConfiguration.java
@@ -37,8 +37,12 @@ public class DefaultMetadataDocumentConfiguration extends AbstractInterceptorAwa
     /** ID for the authorization server metadata profile configuration. */
     @Nonnull
     @NotEmpty
-    public static final String PROFILE_ID_AUTHORIZATION_SERVER =
-            "http://geant.org/ns/profiles/oauth/authorization-server";
+    public static final String PROFILE_ID_AUTHORIZATION_SERVER = "http://geant.org/ns/profiles/oauth/authorization-server";
+
+    /** ID for the did:web document profile configuration. */
+    @Nonnull
+    @NotEmpty
+    public static final String PROFILE_ID_DID_DOCUMENT = "http://geant.org/ns/profiles/did/document";
 
     /**
      * Constructor.
diff --git a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/FormOutboundDidDocumentResponseMessage.java b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/FormOutboundDidDocumentResponseMessage.java
new file mode 100644
index 0000000..47c6902
--- /dev/null
+++ b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/FormOutboundDidDocumentResponseMessage.java
@@ -0,0 +1,256 @@
+/*
+ * Copyright (c) 2025, GÉANT
+ *
+ * Licensed under the Apache License, Version 2.0 (the “License”); you may not
+ * use this file except in compliance with the License. You may obtain a copy
+ * of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an “AS IS” BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.geant.shibboleth.plugin.openidvci.profile.impl;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.geant.shibboleth.plugin.openidvci.messaging.impl.MetadataDocumentSuccessResponse;
+import org.geant.shibboleth.plugin.openidvci.security.impl.DidSupport;
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.security.credential.Credential;
+import org.slf4j.Logger;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.nimbusds.jose.jwk.JWK;
+
+import net.shibboleth.idp.profile.AbstractProfileAction;
+import net.shibboleth.oidc.security.CredentialConversionUtil;
+import net.shibboleth.oidc.security.credential.JWKCredential;
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.collection.CollectionSupport;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * Action publishing the did:web document of this deployment.
+ *
+ * States the public half of every key credentials and Status List Tokens are
+ * signed with. Formed from the signing credentials rather than read from a
+ * file, so a key is published for as long as it is among them.
+ */
+public class FormOutboundDidDocumentResponseMessage extends AbstractProfileAction {
+
+    /** Member naming the JSON-LD contexts of the document. */
+    @Nonnull
+    public static final String CONTEXT = "@context";
+
+    /** Member naming the subject of the document. */
+    @Nonnull
+    public static final String ID = "id";
+
+    /** Member carrying the verification methods. */
+    @Nonnull
+    public static final String VERIFICATION_METHOD = "verificationMethod";
+
+    /**
+     * Member carrying the verification methods a credential may be asserted with.
+     */
+    @Nonnull
+    public static final String ASSERTION_METHOD = "assertionMethod";
+
+    /** Member naming the controller of a verification method. */
+    @Nonnull
+    public static final String CONTROLLER = "controller";
+
+    /** Member naming the type of a verification method. */
+    @Nonnull
+    public static final String TYPE = "type";
+
+    /** Member carrying the key of a verification method. */
+    @Nonnull
+    public static final String PUBLIC_KEY_JWK = "publicKeyJwk";
+
+    /** Member naming the algorithm of a key. */
+    @Nonnull
+    public static final String ALG = "alg";
+
+    /** Type given to every verification method of the document. */
+    @Nonnull
+    public static final String JSON_WEB_KEY = "JsonWebKey2020";
+
+    /** Contexts of the document. */
+    @Nonnull
+    private static final List<String> CONTEXTS = CollectionSupport.listOf("https://www.w3.org/ns/did/v1",
+            "https://w3id.org/security/suites/jws-2020/v1");
+
+    /** Class logger. */
+    @Nonnull
+    private final Logger log = LoggerFactory.getLogger(FormOutboundDidDocumentResponseMessage.class);
+
+    /**
+     * Url this deployment is reached at, the did:web identifier is derived from it.
+     */
+    @Nullable
+    private String deploymentIssuer;
+
+    /** Credentials issued Verifiable Credentials are signed with. */
+    @Nonnull
+    private List<Credential> credentialSigningCredentials = CollectionSupport.emptyList();
+
+    /** Credentials Status List Tokens are signed with. */
+    @Nonnull
+    private List<Credential> statusListSigningCredentials = CollectionSupport.emptyList();
+
+    /** The did:web identifier of this deployment. */
+    @NonnullAfterInit
+    private String didWeb;
+
+    /**
+     * Set the url this deployment is reached at.
+     *
+     * @param issuer url of this deployment
+     */
+    public void setDeploymentIssuer(@Nullable final String issuer) {
+        checkSetterPreconditions();
+        deploymentIssuer = issuer;
+    }
+
+    /**
+     * Set the credentials issued Verifiable Credentials are signed with.
+     *
+     * @param credentials the credentials, or null for none
+     */
+    public void setCredentialSigningCredentials(@Nullable final List<Credential> credentials) {
+        checkSetterPreconditions();
+        credentialSigningCredentials = credentials == null ? CollectionSupport.emptyList()
+                : CollectionSupport.copyToList(credentials);
+    }
+
+    /**
+     * Set the credentials Status List Tokens are signed with.
+     *
+     * @param credentials the credentials, or null for none
+     */
+    public void setStatusListSigningCredentials(@Nullable final List<Credential> credentials) {
+        checkSetterPreconditions();
+        statusListSigningCredentials = credentials == null ? CollectionSupport.emptyList()
+                : CollectionSupport.copyToList(credentials);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+
+        final String derived = DidSupport.toDidWeb(deploymentIssuer);
+        if (derived == null) {
+            throw new ComponentInitializationException(
+                    "Issuer '" + deploymentIssuer + "' names no host, unable to derive a did:web identifier");
+        }
+        didWeb = derived;
+
+        if (credentialSigningCredentials.isEmpty() && statusListSigningCredentials.isEmpty()) {
+            throw new ComponentInitializationException("No signing credentials to publish");
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+        final List<Object> methods = new ArrayList<>();
+        final List<Object> assertions = new ArrayList<>();
+        final Set<String> stated = new LinkedHashSet<>();
+
+        verificationMethods(credentialSigningCredentials, methods, assertions, stated);
+        verificationMethods(statusListSigningCredentials, methods, assertions, stated);
+
+        if (methods.isEmpty()) {
+            log.error("{} None of the signing keys could be stated, there is no document to publish", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.IO_ERROR);
+            return;
+        }
+
+        final Map<String, Object> document = new LinkedHashMap<>();
+        document.put(CONTEXT, CONTEXTS);
+        document.put(ID, didWeb);
+        document.put(VERIFICATION_METHOD, methods);
+        document.put(ASSERTION_METHOD, assertions);
+
+        try {
+            profileRequestContext.ensureOutboundMessageContext()
+                    .setMessage(new MetadataDocumentSuccessResponse(document));
+        } catch (final JsonProcessingException e) {
+            log.error("{} Could not serialize the did:web document", getLogPrefix(), e);
+            ActionSupport.buildEvent(profileRequestContext, EventIds.IO_ERROR);
+            return;
+        }
+
+        log.debug("{} Publishing the did:web document of {}, verification methods {}", getLogPrefix(), didWeb, stated);
+    }
+
+    /**
+     * State every credential of a list as a verification method of the document.
+     *
+     * A credential already stated is left out, the two lists this deployment signs
+     * with are commonly the same key.
+     *
+     * @param credentials credentials to state
+     * @param methods     verification methods to write to
+     * @param assertions  assertion methods to write to
+     * @param stated      verification methods already stated
+     */
+    private void verificationMethods(@Nonnull final List<Credential> credentials, @Nonnull final List<Object> methods,
+            @Nonnull final List<Object> assertions, @Nonnull final Set<String> stated) {
+
+        for (final Credential credential : credentials) {
+
+            final String keyName = DidSupport.keyName(credential);
+            if (keyName == null) {
+                log.warn("Signing credential names its key by nothing, leaving it out of the did:web document");
+                continue;
+            }
+
+            // Conversion keeps the public components only.
+            final JWK key = CredentialConversionUtil.credentialToKey(credential);
+            if (key == null) {
+                log.warn("Signing key '{}' is neither RSA nor EC, leaving it out of the did:web document", keyName);
+                continue;
+            }
+
+            final String id = didWeb + "#" + keyName;
+            if (!stated.add(id)) {
+                continue;
+            }
+
+            final Map<String, Object> published = new LinkedHashMap<>(key.toJSONObject());
+            if (credential instanceof final JWKCredential jwkCredential && jwkCredential.getAlgorithm() != null) {
+                published.put(ALG, jwkCredential.getAlgorithm().getName());
+            }
+
+            final Map<String, Object> method = new LinkedHashMap<>();
+            method.put(ID, id);
+            method.put(TYPE, JSON_WEB_KEY);
+            method.put(CONTROLLER, didWeb);
+            method.put(PUBLIC_KEY_JWK, published);
+
+            methods.add(method);
+            assertions.add(id);
+        }
+    }
+
+}
diff --git a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/SignJWTCredential.java b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/SignJWTCredential.java
index de257ed..abfba63 100644
--- a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/SignJWTCredential.java
+++ b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/SignJWTCredential.java
@@ -23,7 +23,7 @@ import java.util.function.Function;
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
-import org.geant.shibboleth.plugin.openidvci.security.impl.DidJwkSupport;
+import org.geant.shibboleth.plugin.openidvci.security.impl.DidSupport;
 import org.geant.shibboleth.plugin.openidvci.messaging.context.CredentialsContext;
 import org.geant.shibboleth.plugin.openidvci.messaging.impl.OpenIDVCICredentialsRequest;
 import org.geant.shibboleth.plugin.openidvci.profile.OpenIDVCIEventIds;
@@ -164,7 +164,7 @@ public class SignJWTCredential extends AbstractOIDCResponseAction {
         for (final JWTClaimsSet credential : ctx.getJWTCredentials()) {
             try {
                 credentials.add(new JWSTokenSigner(
-                        DidJwkSupport.nameKeyByIssuer(signatureSigningParameters, credential.getIssuer()))
+                        DidSupport.nameKeyByIssuer(signatureSigningParameters, credential.getIssuer()))
                         .sign(credential, format).serialize());
             } catch (final SignatureException e) {
                 log.error("{} Signing credential failed", getLogPrefix(), e);
diff --git a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/logic/CredentialIssuerLookupFunction.java b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/logic/CredentialIssuerLookupFunction.java
index 3ebb165..aa1410c 100644
--- a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/logic/CredentialIssuerLookupFunction.java
+++ b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/logic/CredentialIssuerLookupFunction.java
@@ -23,6 +23,10 @@ import java.util.function.Function;
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
+import org.geant.shibboleth.plugin.openidvci.credential.CredentialConfiguration;
+import org.geant.shibboleth.plugin.openidvci.credential.CredentialIssuanceConfiguration;
+import org.geant.shibboleth.plugin.openidvci.messaging.context.CredentialsContext;
+import org.geant.shibboleth.plugin.openidvci.security.impl.DidSupport;
 import org.opensaml.messaging.context.navigate.ChildContextLookup;
 import org.opensaml.profile.context.ProfileRequestContext;
 import org.opensaml.profile.context.navigate.OutboundMessageContextLookup;
@@ -39,14 +43,16 @@ import net.shibboleth.shared.logic.Constraint;
 import net.shibboleth.shared.primitive.LoggerFactory;
 
 /**
- * Issuer identifier for everything this plugin signs. By default the
- * <a href="https://github.com/quartzjer/did-jwk/blob/main/spec.md">did:jwk</a>
- * of the resolved signing key. Setting an identifier overrides this, that is a
- * placeholder for future types and this all propably needs additional
- * modifications at that point.
+ * Issuer identifier for everything this plugin signs.
  *
- * TODO: Whole did:jwk and key rotation needs still thinking over. Current
- * implementation is pretty much stuck with one key.
+ * By default the
+ * <a href="https://github.com/quartzjer/did-jwk/blob/main/spec.md">did:jwk</a>
+ * of the resolved signing key. A configured identifier overrides it,
+ * '{@value DidSupport#DID_WEB_KEYWORD}' giving the did:web of this deployment,
+ * '{@value DidSupport#DID_JWK_KEYWORD}' the did:jwk of the signing key,
+ * '{@value DidSupport#URL_KEYWORD}' the url of this deployment and anything
+ * else taken as the identifier itself. A Credential Configuration that states
+ * an issuer of its own overrides the deployment.
  */
 public class CredentialIssuerLookupFunction implements Function<ProfileRequestContext, String> {
 
@@ -66,6 +72,12 @@ public class CredentialIssuerLookupFunction implements Function<ProfileRequestCo
     @Nullable
     private String configuredIssuer;
 
+    /**
+     * Url this deployment is reached at, a did:web identifier is derived from it.
+     */
+    @Nullable
+    private String deploymentIssuer;
+
     /** Constructor. */
     public CredentialIssuerLookupFunction() {
         final Function<ProfileRequestContext, SecurityParametersContext> splcs = new ChildContextLookup<>(
@@ -88,8 +100,10 @@ public class CredentialIssuerLookupFunction implements Function<ProfileRequestCo
     /**
      * Set an issuer identifier to use in place of one derived from the signing key.
      *
-     * Placeholder for future types and this all propably needs additional
-     * modifications at that point.
+     * '{@value DidSupport#DID_WEB_KEYWORD}' asks for the did:web of this
+     * deployment, '{@value DidSupport#DID_JWK_KEYWORD}' for the did:jwk of the
+     * signing key and '{@value DidSupport#URL_KEYWORD}' for the url of this
+     * deployment. Any other value is the identifier itself.
      *
      * @param issuer issuer identifier, or null to derive one
      */
@@ -99,12 +113,35 @@ public class CredentialIssuerLookupFunction implements Function<ProfileRequestCo
         }
     }
 
+    /**
+     * Set the url this deployment is reached at, the did:web is derived from it.
+     *
+     * @param issuer url of this deployment
+     */
+    public void setDeploymentIssuer(@Nullable final String issuer) {
+        deploymentIssuer = issuer;
+    }
+
     /** {@inheritDoc} */
     @Override
     @Nullable
     public String apply(@Nullable final ProfileRequestContext profileRequestContext) {
-        if (configuredIssuer != null) {
-            return configuredIssuer;
+        final String configured = configuredIssuer(profileRequestContext);
+        if (DidSupport.DID_WEB_KEYWORD.equals(configured)) {
+            final String didWeb = DidSupport.toDidWeb(deploymentIssuer);
+            if (didWeb == null) {
+                log.error("Issuer '{}' names no host, unable to derive a did:web issuer", deploymentIssuer);
+            }
+            return didWeb;
+        }
+        if (DidSupport.URL_KEYWORD.equals(configured)) {
+            if (deploymentIssuer == null || deploymentIssuer.isEmpty()) {
+                log.error("No issuer of the OP to name this deployment by");
+            }
+            return deploymentIssuer;
+        }
+        if (configured != null && !DidSupport.DID_JWK_KEYWORD.equals(configured)) {
+            return configured;
         }
         if (profileRequestContext == null) {
             return null;
@@ -131,6 +168,31 @@ public class CredentialIssuerLookupFunction implements Function<ProfileRequestCo
         return toDidJwk(credential.getPublicKey());
     }
 
+    /**
+     * Get the issuer identifier configured for the credential in issue, the one of
+     * the Credential Configuration when it states one and the one of the deployment
+     * otherwise.
+     *
+     * @param profileRequestContext current profile request context
+     * @return the configured identifier, or null to derive one
+     */
+    @Nullable
+    private String configuredIssuer(@Nullable final ProfileRequestContext profileRequestContext) {
+
+        if (profileRequestContext != null && profileRequestContext.getInboundMessageContext() != null) {
+            final CredentialsContext ctx = profileRequestContext.getInboundMessageContext()
+                    .getSubcontext(CredentialsContext.class);
+            final CredentialConfiguration configuration = ctx == null ? null : ctx.getCredentialConfiguration();
+            final CredentialIssuanceConfiguration settings = configuration == null ? null
+                    : configuration.getIssuanceConfiguration();
+            final String perCredential = settings == null ? null : settings.getIssuer();
+            if (perCredential != null) {
+                return perCredential;
+            }
+        }
+        return configuredIssuer;
+    }
+
     /**
      * Encode a public key as a did:jwk identifier.
      *
diff --git a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/logic/StatusListIssuerTagLookupFunction.java b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/logic/StatusListIssuerTagLookupFunction.java
new file mode 100644
index 0000000..4cc19d7
--- /dev/null
+++ b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/logic/StatusListIssuerTagLookupFunction.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright (c) 2025, GÉANT
+ *
+ * Licensed under the Apache License, Version 2.0 (the “License”); you may not
+ * use this file except in compliance with the License. You may obtain a copy
+ * of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an “AS IS” BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.geant.shibboleth.plugin.openidvci.profile.logic;
+
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.geant.shibboleth.plugin.openidvci.security.impl.DidSupport;
+import org.geant.shibboleth.plugin.openidvci.statuslist.StatusListIssuerTag;
+import org.opensaml.messaging.context.navigate.ChildContextLookup;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.navigate.OutboundMessageContextLookup;
+import org.opensaml.security.credential.Credential;
+import org.slf4j.Logger;
+
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.jwk.JWK;
+
+import net.shibboleth.oidc.security.CredentialConversionUtil;
+import net.shibboleth.oidc.security.jose.SignatureSigningParameters;
+import net.shibboleth.oidc.security.jose.context.SecurityParametersContext;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+
+/**
+ * Tag naming the Credential Issuer identifier a credential is issued under, for
+ * the url of the status list it takes a slot of.
+ *
+ * A did:web or url identifier is the same for every credential of this
+ * deployment and takes a static tag. A did:jwk identifier names a key, so its
+ * tag carries the thumbprint of that key.
+ */
+public class StatusListIssuerTagLookupFunction implements Function<ProfileRequestContext, String> {
+
+    /** Class logger. */
+    @Nonnull
+    private final Logger log = LoggerFactory.getLogger(StatusListIssuerTagLookupFunction.class);
+
+    /** Strategy used to resolve the issuer of the credential in issue. */
+    @Nonnull
+    private Function<ProfileRequestContext, String> issuerLookupStrategy;
+
+    /** Strategy used to locate {@link SecurityParametersContext} to read. */
+    @Nonnull
+    private Function<ProfileRequestContext, SecurityParametersContext> securityParametersLookupStrategy;
+
+    /** Constructor. */
+    public StatusListIssuerTagLookupFunction() {
+        issuerLookupStrategy = new CredentialIssuerLookupFunction();
+        final Function<ProfileRequestContext, SecurityParametersContext> splcs = new ChildContextLookup<>(
+                SecurityParametersContext.class).compose(new OutboundMessageContextLookup());
+        assert splcs != null;
+        securityParametersLookupStrategy = splcs;
+    }
+
+    /**
+     * Set the strategy used to resolve the issuer of the credential in issue.
+     *
+     * @param strategy lookup strategy
+     */
+    public void setIssuerLookupStrategy(@Nonnull final Function<ProfileRequestContext, String> strategy) {
+        issuerLookupStrategy = Constraint.isNotNull(strategy, "Issuer lookup strategy cannot be null");
+    }
+
+    /**
+     * Set strategy used to locate the {@link SecurityParametersContext} to read.
+     *
+     * @param strategy lookup strategy
+     */
+    public void setSecurityParametersLookupStrategy(
+            @Nonnull final Function<ProfileRequestContext, SecurityParametersContext> strategy) {
+        securityParametersLookupStrategy = Constraint.isNotNull(strategy,
+                "SecurityParametersContext lookup strategy cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    @Nullable
+    public String apply(@Nullable final ProfileRequestContext profileRequestContext) {
+
+        final String issuer = issuerLookupStrategy.apply(profileRequestContext);
+        if (issuer == null) {
+            log.debug("No issuer resolved, status list url carries no tag");
+            return null;
+        }
+
+        if (issuer.startsWith(DidSupport.DID_WEB_PREFIX)) {
+            return StatusListIssuerTag.DID_WEB;
+        }
+        if (!issuer.startsWith(DidSupport.DID_JWK_PREFIX)) {
+            return StatusListIssuerTag.URL;
+        }
+
+        final String thumbprint = thumbprint(profileRequestContext);
+        if (thumbprint == null) {
+            log.error("Unable to name the key of issuer '{}', status list url carries no tag", issuer);
+            return null;
+        }
+        return StatusListIssuerTag.didJwk(thumbprint);
+    }
+
+    /**
+     * Get the thumbprint of the key the credential in issue is signed with.
+     *
+     * @param profileRequestContext current profile request context
+     * @return the thumbprint, or null when there is no key to take one of
+     */
+    @Nullable
+    private String thumbprint(@Nullable final ProfileRequestContext profileRequestContext) {
+
+        if (profileRequestContext == null) {
+            return null;
+        }
+
+        final SecurityParametersContext secParamCtx = securityParametersLookupStrategy.apply(profileRequestContext);
+        final SignatureSigningParameters params = secParamCtx == null ? null
+                : secParamCtx.getSignatureSigningParameters();
+        final Credential credential = params == null ? null : params.getSigningCredential();
+        if (credential == null) {
+            log.debug("No signing credential, unable to take a thumbprint of it");
+            return null;
+        }
+
+        final JWK key = CredentialConversionUtil.credentialToKey(credential);
+        if (key == null) {
+            log.debug("Signing key is neither RSA nor EC, unable to take a thumbprint of it");
+            return null;
+        }
+
+        try {
+            return key.computeThumbprint().toString();
+        } catch (final JOSEException e) {
+            log.error("Unable to take a thumbprint of the signing key", e);
+            return null;
+        }
+    }
+
+}
diff --git a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/security/impl/DidJwkSupport.java b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/security/impl/DidJwkSupport.java
deleted file mode 100644
index 7509889..0000000
--- a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/security/impl/DidJwkSupport.java
+++ /dev/null
@@ -1,112 +0,0 @@
-/*
- * Copyright (c) 2025, GÉANT
- *
- * Licensed under the Apache License, Version 2.0 (the “License”); you may not
- * use this file except in compliance with the License. You may obtain a copy
- * of the License at
- *
- *    http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an “AS IS” BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.geant.shibboleth.plugin.openidvci.security.impl;
-
-import java.security.PrivateKey;
-import java.security.PublicKey;
-
-import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
-import javax.crypto.SecretKey;
-
-import org.opensaml.security.credential.Credential;
-
-import net.shibboleth.oidc.security.credential.BasicJWKCredential;
-import net.shibboleth.oidc.security.credential.JWKCredential;
-import net.shibboleth.oidc.security.jose.SignatureSigningParameters;
-
-/**
- * Support for naming the signing key of a token by the did:jwk it is issued
- * under.
- *
- */
-public final class DidJwkSupport {
-
-    /** Prefix of a did:jwk identifier. */
-    @Nonnull
-    private static final String PREFIX = "did:jwk:";
-
-    /** Fragment naming the only verification method of a did:jwk document. */
-    @Nonnull
-    private static final String VERIFICATION_METHOD = "#0";
-
-    /** Constructor. */
-    private DidJwkSupport() {
-    }
-
-    /**
-     * Name the signing key of the parameters by the issuer of the token.
-     *
-     * A verifier resolves the key of a did:jwk issuer by the 'kid' header, which
-     * has to name the verification method of the document rather than the key
-     * itself. An issuer that is no did:jwk leaves the key named as it is.
-     *
-     * @param parameters parameters to name the key of
-     * @param issuer     issuer the token names
-     * @return parameters naming the key, or the given ones when there is nothing to
-     *         name it by
-     */
-    @Nonnull
-    public static SignatureSigningParameters nameKeyByIssuer(@Nonnull final SignatureSigningParameters parameters,
-            @Nullable final String issuer) {
-
-        final Credential credential = parameters.getSigningCredential();
-        if (issuer == null || !issuer.startsWith(PREFIX) || credential == null) {
-            return parameters;
-        }
-
-        final SignatureSigningParameters named = new SignatureSigningParameters();
-        named.setSignatureAlgorithm(parameters.getSignatureAlgorithm());
-        named.setSigningCredential(nameKey(credential, issuer + VERIFICATION_METHOD));
-        return named;
-    }
-
-    /**
-     * Copy a credential, naming its key.
-     *
-     * @param credential credential to copy
-     * @param keyName    name to give the key
-     * @return the copy
-     */
-    @Nonnull
-    private static Credential nameKey(@Nonnull final Credential credential, @Nonnull final String keyName) {
-
-        final BasicJWKCredential copy = new BasicJWKCredential();
-        copy.getKeyNames().add(keyName);
-        copy.setKid(keyName);
-
-        final PublicKey publicKey = credential.getPublicKey();
-        if (publicKey != null) {
-            copy.setPublicKey(publicKey);
-        }
-        final PrivateKey privateKey = credential.getPrivateKey();
-        if (privateKey != null) {
-            copy.setPrivateKey(privateKey);
-        }
-        final SecretKey secretKey = credential.getSecretKey();
-        if (secretKey != null) {
-            copy.setSecretKey(secretKey);
-        }
-        copy.setUsageType(credential.getUsageType());
-        copy.setEntityId(credential.getEntityId());
-        if (credential instanceof final JWKCredential jwk) {
-            copy.setAlgorithm(jwk.getAlgorithm());
-        }
-        return copy;
-    }
-
-}
diff --git a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/security/impl/DidSupport.java b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/security/impl/DidSupport.java
new file mode 100644
index 0000000..d82ac49
--- /dev/null
+++ b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/security/impl/DidSupport.java
@@ -0,0 +1,254 @@
+/*
+ * Copyright (c) 2025, GÉANT
+ *
+ * Licensed under the Apache License, Version 2.0 (the “License”); you may not
+ * use this file except in compliance with the License. You may obtain a copy
+ * of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an “AS IS” BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.geant.shibboleth.plugin.openidvci.security.impl;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.interfaces.ECPublicKey;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.crypto.SecretKey;
+
+import org.opensaml.security.credential.Credential;
+
+import com.nimbusds.jose.jwk.Curve;
+import com.nimbusds.jose.jwk.ECKey;
+import com.nimbusds.jose.util.Base64URL;
+
+import net.shibboleth.oidc.security.credential.BasicJWKCredential;
+import net.shibboleth.oidc.security.credential.JWKCredential;
+import net.shibboleth.oidc.security.jose.SignatureSigningParameters;
+
+/**
+ * Naming of a signing key by the DID a token is issued under.
+ *
+ * The 'kid' names a verification method of the DID document, '#0' for did:jwk
+ * and the name of the key for did:web. An issuer that is no DID leaves the key
+ * named as it is.
+ */
+public final class DidSupport {
+
+    /** Prefix of a did:jwk identifier. */
+    @Nonnull
+    public static final String DID_JWK_PREFIX = "did:jwk:";
+
+    /** Prefix of a did:web identifier. */
+    @Nonnull
+    public static final String DID_WEB_PREFIX = "did:web:";
+
+    /**
+     * Value of the configured issuer asking for a did:web derived from this
+     * deployment, rather than naming one literally.
+     */
+    @Nonnull
+    public static final String DID_WEB_KEYWORD = "did:web";
+
+    /**
+     * Value of the configured issuer asking for the did:jwk of the signing key,
+     * rather than naming an identifier literally.
+     */
+    @Nonnull
+    public static final String DID_JWK_KEYWORD = "did:jwk";
+
+    /**
+     * Value of the configured issuer asking for the url of this deployment, the
+     * issuer of the OP, rather than naming one literally.
+     */
+    @Nonnull
+    public static final String URL_KEYWORD = "url";
+
+    /** Fragment naming the only verification method of a did:jwk document. */
+    @Nonnull
+    private static final String DID_JWK_VERIFICATION_METHOD = "#0";
+
+    /** Separator of a DID and the fragment naming a verification method of it. */
+    @Nonnull
+    private static final String FRAGMENT = "#";
+
+    /**
+     * Encoded form of the port separator, a did:web identifier has no ':' of its
+     * own.
+     */
+    @Nonnull
+    private static final String ENCODED_PORT_SEPARATOR = "%3A";
+
+    /** Constructor. */
+    private DidSupport() {
+    }
+
+    /**
+     * Derive the did:web identifier of a deployment from the url it is reached at.
+     * Only the host is taken, a path is dropped and a port is encoded.
+     *
+     * @param deploymentIssuer url of this deployment
+     * @return the did:web identifier, or null if the url names no host
+     */
+    @Nullable
+    public static String toDidWeb(@Nullable final String deploymentIssuer) {
+
+        if (deploymentIssuer == null || deploymentIssuer.isEmpty()) {
+            return null;
+        }
+
+        final URI uri;
+        try {
+            uri = new URI(deploymentIssuer);
+        } catch (final URISyntaxException e) {
+            return null;
+        }
+
+        final String host = uri.getHost();
+        if (host == null || host.isEmpty()) {
+            return null;
+        }
+
+        final int port = uri.getPort();
+        return port == -1 ? DID_WEB_PREFIX + host : DID_WEB_PREFIX + host + ENCODED_PORT_SEPARATOR + port;
+    }
+
+    /**
+     * Encode a public key as a did:jwk identifier.
+     *
+     * @param publicKey key to encode
+     * @return the identifier, or null if the key is of an unsupported type
+     */
+    @Nullable
+    public static String toDidJwk(@Nullable final PublicKey publicKey) {
+
+        if (!(publicKey instanceof final ECPublicKey ecKey)) {
+            return null;
+        }
+        final Curve curve = Curve.forECParameterSpec(ecKey.getParams());
+        if (curve == null) {
+            return null;
+        }
+        return DID_JWK_PREFIX + Base64URL.encode(new ECKey.Builder(curve, ecKey).build().toJSONString()).toString();
+    }
+
+    /**
+     * Name the verification method a DID issuer resolves a key by.
+     *
+     * @param issuer  issuer of the token
+     * @param keyName name the key carries, for the methods that need one
+     * @return the verification method, or null when the issuer resolves no key
+     */
+    @Nullable
+    public static String verificationMethod(@Nullable final String issuer, @Nullable final String keyName) {
+
+        if (issuer == null) {
+            return null;
+        }
+        if (issuer.startsWith(DID_JWK_PREFIX)) {
+            return issuer + DID_JWK_VERIFICATION_METHOD;
+        }
+        if (issuer.startsWith(DID_WEB_PREFIX) && keyName != null && !keyName.isEmpty()) {
+            return issuer + FRAGMENT + keyName;
+        }
+        return null;
+    }
+
+    /**
+     * Get the name a credential carries its key by.
+     *
+     * @param credential credential to read
+     * @return the name, or null when the credential names its key by nothing
+     */
+    @Nullable
+    public static String keyName(@Nullable final Credential credential) {
+
+        if (credential instanceof final JWKCredential jwk) {
+            final String kid = jwk.getKid();
+            if (kid != null && !kid.isEmpty()) {
+                return kid;
+            }
+        }
+        if (credential != null) {
+            for (final String name : credential.getKeyNames()) {
+                if (name != null && !name.isEmpty()) {
+                    return name;
+                }
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Name the signing key of the parameters by the issuer of the token.
+     *
+     * @param parameters parameters to name the key of
+     * @param issuer     issuer the token names
+     * @return parameters naming the key, or the given ones when there is nothing to
+     *         name it by
+     */
+    @Nonnull
+    public static SignatureSigningParameters nameKeyByIssuer(@Nonnull final SignatureSigningParameters parameters,
+            @Nullable final String issuer) {
+
+        final Credential credential = parameters.getSigningCredential();
+        if (credential == null) {
+            return parameters;
+        }
+
+        final String method = verificationMethod(issuer, keyName(credential));
+        if (method == null) {
+            return parameters;
+        }
+
+        final SignatureSigningParameters named = new SignatureSigningParameters();
+        named.setSignatureAlgorithm(parameters.getSignatureAlgorithm());
+        named.setSigningCredential(nameKey(credential, method));
+        return named;
+    }
+
+    /**
+     * Copy a credential, naming its key.
+     *
+     * @param credential credential to copy
+     * @param keyName    name to give the key
+     * @return the copy
+     */
+    @Nonnull
+    private static Credential nameKey(@Nonnull final Credential credential, @Nonnull final String keyName) {
+
+        final BasicJWKCredential copy = new BasicJWKCredential();
+        copy.getKeyNames().add(keyName);
+        copy.setKid(keyName);
+
+        final PublicKey publicKey = credential.getPublicKey();
+        if (publicKey != null) {
+            copy.setPublicKey(publicKey);
+        }
+        final PrivateKey privateKey = credential.getPrivateKey();
+        if (privateKey != null) {
+            copy.setPrivateKey(privateKey);
+        }
+        final SecretKey secretKey = credential.getSecretKey();
+        if (secretKey != null) {
+            copy.setSecretKey(secretKey);
+        }
+        copy.setUsageType(credential.getUsageType());
+        copy.setEntityId(credential.getEntityId());
+        if (credential instanceof final JWKCredential jwk) {
+            copy.setAlgorithm(jwk.getAlgorithm());
+        }
+        return copy;
+    }
+
+}
diff --git a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/statuslist/profile/impl/AllocateIndex.java b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/statuslist/profile/impl/AllocateIndex.java
index ed372c9..252187a 100644
--- a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/statuslist/profile/impl/AllocateIndex.java
+++ b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/statuslist/profile/impl/AllocateIndex.java
@@ -28,6 +28,7 @@ import javax.annotation.Nullable;
 
 import org.geant.shibboleth.plugin.openidvci.statuslist.MonthlyBucketStrategy;
 import org.geant.shibboleth.plugin.openidvci.statuslist.StatusListBucket;
+import org.geant.shibboleth.plugin.openidvci.statuslist.StatusListIssuerTag;
 import org.geant.shibboleth.plugin.openidvci.statuslist.StatusListParameters;
 import org.geant.shibboleth.plugin.openidvci.statuslist.context.StatusListContext;
 import org.geant.shibboleth.plugin.openidvci.statuslist.storage.StatusListIndexAllocator;
@@ -82,6 +83,13 @@ public class AllocateIndex extends AbstractProfileAction {
     @Nonnull
     private String pathPrefix = "/idp/profile/oauth/status-list/list";
 
+    /**
+     * Strategy used to resolve the tag naming the issuer of the credential in
+     * issue.
+     */
+    @Nullable
+    private Function<ProfileRequestContext, String> issuerTagLookupStrategy;
+
     /** Expiration shared by credentials issued. */
     @Nullable
     private Instant expiration;
@@ -145,6 +153,17 @@ public class AllocateIndex extends AbstractProfileAction {
         }
     }
 
+    /**
+     * Set the strategy used to resolve the tag naming the issuer of the credential
+     * in issue. The tag travels in the status list url and nowhere else.
+     *
+     * @param strategy lookup strategy, or null to publish urls with no tag
+     */
+    public void setIssuerTagLookupStrategy(@Nullable final Function<ProfileRequestContext, String> strategy) {
+        checkSetterPreconditions();
+        issuerTagLookupStrategy = strategy;
+    }
+
     /** {@inheritDoc} */
     @Override
     protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
@@ -205,8 +224,10 @@ public class AllocateIndex extends AbstractProfileAction {
             assert scheme != null;
             final String serverName = request.getServerName();
             assert serverName != null;
+            final String tag = issuerTagLookupStrategy == null ? null
+                    : issuerTagLookupStrategy.apply(profileRequestContext);
             statusListUri = buildURIIgnoreDefaultPorts(scheme, serverName, request.getServerPort(), pathPrefix,
-                    StatusListParameters.BUCKET_PARAMETER + "=" + bucket.id());
+                    StatusListParameters.BUCKET_PARAMETER + "=" + StatusListIssuerTag.reference(bucket.id(), tag));
         } catch (final URISyntaxException e) {
             log.error("{} Unable to build status list URI", getLogPrefix(), e);
             ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
diff --git a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/statuslist/profile/impl/FormStatusListToken.java b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/statuslist/profile/impl/FormStatusListToken.java
index ceae6fc..6a7fb09 100644
--- a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/statuslist/profile/impl/FormStatusListToken.java
+++ b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/statuslist/profile/impl/FormStatusListToken.java
@@ -20,15 +20,19 @@ import java.io.IOException;
 import java.time.Duration;
 import java.time.Instant;
 import java.util.Date;
+import java.util.List;
 import java.util.Map;
 import java.util.function.Function;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
+import org.geant.shibboleth.plugin.openidvci.profile.logic.CredentialIssuerLookupFunction;
+import org.geant.shibboleth.plugin.openidvci.security.impl.DidSupport;
 import org.geant.shibboleth.plugin.openidvci.statuslist.MonthlyBucketStrategy;
 import org.geant.shibboleth.plugin.openidvci.statuslist.StatusListBucket;
 import org.geant.shibboleth.plugin.openidvci.statuslist.StatusListCodec;
+import org.geant.shibboleth.plugin.openidvci.statuslist.StatusListIssuerTag;
 import org.geant.shibboleth.plugin.openidvci.statuslist.StatusListParameters;
 import org.geant.shibboleth.plugin.openidvci.statuslist.context.StatusListTokenContext;
 import org.geant.shibboleth.plugin.openidvci.statuslist.profile.StatusListEventIds;
@@ -37,19 +41,22 @@ import org.opensaml.messaging.context.MessageContext;
 import org.opensaml.profile.action.ActionSupport;
 import org.opensaml.profile.action.EventIds;
 import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.security.credential.Credential;
 import org.slf4j.Logger;
 
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.jwk.JWK;
 import com.nimbusds.jwt.JWTClaimsSet;
 
 import net.shibboleth.idp.profile.AbstractProfileAction;
 import net.shibboleth.idp.profile.IdPEventIds;
+import net.shibboleth.oidc.security.CredentialConversionUtil;
 import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.collection.CollectionSupport;
 import net.shibboleth.shared.component.ComponentInitializationException;
 import net.shibboleth.shared.logic.Constraint;
 import net.shibboleth.shared.primitive.LoggerFactory;
 
-import org.geant.shibboleth.plugin.openidvci.profile.logic.CredentialIssuerLookupFunction;
-
 /**
  * Action that forms claims of a Status List Token and sets them to
  * {@link StatusListTokenContext} located under
@@ -86,10 +93,28 @@ public class FormStatusListToken extends AbstractProfileAction {
     @Nullable
     private StatusListBucket bucket;
 
+    /** Url this deployment is reached at, a did:web issuer is derived from it. */
+    @Nullable
+    private String deploymentIssuer;
+
+    /** Issuer identifier configured for this deployment. */
+    @Nullable
+    private String configuredIssuer;
+
+    /** Credentials a Status List Token may be signed with. */
+    @Nonnull
+    private List<Credential> signingCredentials = CollectionSupport.emptyList();
+
+    /** The value of the request parameter naming the list, tag and all. */
+    @Nullable
+    private String reference;
+
+    /** Thumbprint of the key this token has to be signed with. */
+    @Nullable
+    private String keyThumbprint;
+
     /** Constructor. */
     public FormStatusListToken() {
-        // TODO: Token must name the same issuer as the credentials referring to it.
-        // With did:jwk key rollover will need us to redesign things.
         issuerLookupStrategy = new CredentialIssuerLookupFunction();
     }
 
@@ -140,6 +165,93 @@ public class FormStatusListToken extends AbstractProfileAction {
                 StatusListParameters.CAPACITY, emptyList.length());
     }
 
+    /**
+     * Set the url this deployment is reached at, a did:web issuer is derived from
+     * it.
+     *
+     * @param issuer url of this deployment
+     */
+    public void setDeploymentIssuer(@Nullable final String issuer) {
+        checkSetterPreconditions();
+        deploymentIssuer = issuer;
+    }
+
+    /**
+     * Set the issuer identifier configured for this deployment, answering a list
+     * handed out under the plain issuer url. The keyword resolves to the issuer of
+     * the OP, a literal url to itself.
+     *
+     * @param issuer the configured identifier
+     */
+    public void setConfiguredIssuer(@Nullable final String issuer) {
+        checkSetterPreconditions();
+        configuredIssuer = issuer == null || issuer.isEmpty() ? null : issuer;
+    }
+
+    /**
+     * Set the credentials a Status List Token may be signed with. A list handed out
+     * under a did:jwk issuer names one of them, so every key that has signed such
+     * credentials belongs here while they live.
+     *
+     * @param credentials the credentials, or null for none
+     */
+    public void setSigningCredentials(@Nullable final List<Credential> credentials) {
+        checkSetterPreconditions();
+        signingCredentials = credentials == null ? CollectionSupport.emptyList()
+                : CollectionSupport.copyToList(credentials);
+    }
+
+    /**
+     * Resolve the issuer a tagged status list is published under.
+     *
+     * @param tag tag the request carries
+     * @return the issuer, or null when the tag names none this deployment answers
+     *         for
+     */
+    @Nullable
+    private String issuerOfTag(@Nonnull final String tag) {
+
+        if (StatusListIssuerTag.DID_WEB.equals(tag)) {
+            return DidSupport.toDidWeb(deploymentIssuer);
+        }
+        if (StatusListIssuerTag.URL.equals(tag)) {
+            return configuredIssuer != null && configuredIssuer.startsWith("https://") ? configuredIssuer
+                    : deploymentIssuer;
+        }
+
+        final String thumbprint = StatusListIssuerTag.thumbprintOf(tag);
+        if (thumbprint == null) {
+            log.error("Tag '{}' of the status list names no issuer this deployment answers for", tag);
+            return null;
+        }
+
+        for (final Credential candidate : signingCredentials) {
+            final JWK key = CredentialConversionUtil.credentialToKey(candidate);
+            if (key == null) {
+                continue;
+            }
+            try {
+                if (!thumbprint.equals(key.computeThumbprint().toString())) {
+                    continue;
+                }
+            } catch (final JOSEException e) {
+                log.warn("Unable to take a thumbprint of a signing key, leaving it out", e);
+                continue;
+            }
+            final String issuer = DidSupport.toDidJwk(candidate.getPublicKey());
+            if (issuer == null) {
+                log.error("Key '{}' is of a type a did:jwk issuer cannot name", thumbprint);
+                return null;
+            }
+            keyThumbprint = thumbprint;
+            return issuer;
+        }
+
+        log.error("No signing credential has thumbprint '{}', the key that signed those credentials is no longer "
+                + "among the ones Status List Tokens are signed with", thumbprint);
+        return null;
+    }
+
     /** {@inheritDoc} */
     @Override
     protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
@@ -157,21 +269,22 @@ public class FormStatusListToken extends AbstractProfileAction {
             return false;
         }
 
-        issuerId = issuerLookupStrategy.apply(profileRequestContext);
-        if (issuerId == null) {
-            log.error("{} Unable to determine issuer of the status list token", getLogPrefix());
-            ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_PROFILE_CONFIG);
-            return false;
-        }
-
-        final String bucketId = getHttpServletRequest().getParameter(StatusListParameters.BUCKET_PARAMETER);
-        bucket = MonthlyBucketStrategy.parse(bucketId);
+        reference = getHttpServletRequest().getParameter(StatusListParameters.BUCKET_PARAMETER);
+        bucket = MonthlyBucketStrategy.parse(StatusListIssuerTag.bucketOf(reference));
         if (bucket == null) {
-            log.error("{} '{}' in parameter '{}' does not name a status list", getLogPrefix(), bucketId,
+            log.error("{} '{}' in parameter '{}' does not name a status list", getLogPrefix(), reference,
                     StatusListParameters.BUCKET_PARAMETER);
             ActionSupport.buildEvent(profileRequestContext, StatusListEventIds.NO_STATUS_LIST);
             return false;
         }
+
+        final String tag = StatusListIssuerTag.tagOf(reference);
+        issuerId = tag == null ? issuerLookupStrategy.apply(profileRequestContext) : issuerOfTag(tag);
+        if (issuerId == null) {
+            log.error("{} Unable to determine issuer of the status list token", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_PROFILE_CONFIG);
+            return false;
+        }
         if (!bucket.end().isAfter(Instant.now())) {
             log.info("{} Status list {} ended at {} and is no longer published", getLogPrefix(), bucket.id(),
                     bucket.end());
@@ -197,8 +310,9 @@ public class FormStatusListToken extends AbstractProfileAction {
             ActionSupport.buildEvent(profileRequestContext, EventIds.IO_ERROR);
             return;
         }
+        // The subject has to be the url the credential points at, tag and all.
         final String subject = getHttpServletRequest().getRequestURL().append('?')
-                .append(StatusListParameters.BUCKET_PARAMETER).append('=').append(bucket.id()).toString();
+                .append(StatusListParameters.BUCKET_PARAMETER).append('=').append(reference).toString();
         final JWTClaimsSet claims = new JWTClaimsSet.Builder().issuer(issuerId).subject(subject)
                 .issueTime(Date.from(Instant.now())).expirationTime(Date.from(bucket.end()))
                 .claim("ttl", ttl.toSeconds())
@@ -206,7 +320,9 @@ public class FormStatusListToken extends AbstractProfileAction {
 
         final MessageContext outbound = profileRequestContext.getOutboundMessageContext();
         assert outbound != null;
-        outbound.ensureSubcontext(StatusListTokenContext.class).setClaims(claims);
+        final StatusListTokenContext tokenCtx = outbound.ensureSubcontext(StatusListTokenContext.class);
+        tokenCtx.setClaims(claims);
+        tokenCtx.setKeyThumbprint(keyThumbprint);
 
         log.debug("{} Formed status list token for {}", getLogPrefix(), subject);
     }
diff --git a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/statuslist/profile/impl/RevokeStatusListIndex.java b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/statuslist/profile/impl/RevokeStatusListIndex.java
index 9b097a1..274060b 100644
--- a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/statuslist/profile/impl/RevokeStatusListIndex.java
+++ b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/statuslist/profile/impl/RevokeStatusListIndex.java
@@ -21,6 +21,7 @@ import java.io.IOException;
 import javax.annotation.Nonnull;
 
 import org.geant.shibboleth.plugin.openidvci.statuslist.MonthlyBucketStrategy;
+import org.geant.shibboleth.plugin.openidvci.statuslist.StatusListIssuerTag;
 import org.geant.shibboleth.plugin.openidvci.statuslist.StatusListBucket;
 import org.geant.shibboleth.plugin.openidvci.statuslist.StatusListParameters;
 import org.geant.shibboleth.plugin.openidvci.statuslist.messaging.impl.StatusListRevokeRequest;
@@ -95,7 +96,8 @@ public class RevokeStatusListIndex extends AbstractProfileAction {
             return false;
         }
 
-        bucket = MonthlyBucketStrategy.parse(request.getId());
+        // The bucket alone keys storage, a tag the credential carries is stripped.
+        bucket = MonthlyBucketStrategy.parse(StatusListIssuerTag.bucketOf(request.getId()));
         if (bucket == null) {
             log.error("{} '{}' does not name a status list", getLogPrefix(), request.getId());
             ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MSG_CTX);
diff --git a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/statuslist/profile/impl/SignStatusListToken.java b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/statuslist/profile/impl/SignStatusListToken.java
index 1646ad2..85ba383 100644
--- a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/statuslist/profile/impl/SignStatusListToken.java
+++ b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/statuslist/profile/impl/SignStatusListToken.java
@@ -16,27 +16,34 @@
 
 package org.geant.shibboleth.plugin.openidvci.statuslist.profile.impl;
 
+import java.util.List;
 import java.util.function.Function;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
-import org.geant.shibboleth.plugin.openidvci.security.impl.DidJwkSupport;
+import org.geant.shibboleth.plugin.openidvci.security.impl.DidSupport;
 import org.geant.shibboleth.plugin.openidvci.statuslist.context.StatusListTokenContext;
 import org.opensaml.messaging.context.navigate.ChildContextLookup;
 import org.opensaml.profile.action.ActionSupport;
 import org.opensaml.profile.action.EventIds;
 import org.opensaml.profile.context.ProfileRequestContext;
 import org.opensaml.profile.context.navigate.OutboundMessageContextLookup;
+import org.opensaml.security.credential.Credential;
 import org.slf4j.Logger;
 
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.jwk.JWK;
+
 import net.shibboleth.idp.profile.AbstractProfileAction;
+import net.shibboleth.oidc.security.CredentialConversionUtil;
 import net.shibboleth.oidc.security.impl.JWSTokenSigner;
 import net.shibboleth.oidc.security.jose.SignatureException;
 import net.shibboleth.oidc.security.jose.SignatureSigningParameters;
 import net.shibboleth.oidc.security.jose.context.SecurityParametersContext;
 import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
 import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
+import net.shibboleth.shared.collection.CollectionSupport;
 import net.shibboleth.shared.logic.Constraint;
 import net.shibboleth.shared.primitive.LoggerFactory;
 
@@ -54,6 +61,10 @@ public class SignStatusListToken extends AbstractProfileAction {
     @Nonnull
     private static final String TOKEN_TYPE = "statuslist+jwt";
 
+    /** Credentials a Status List Token may be signed with. */
+    @Nonnull
+    private List<Credential> signingCredentials = CollectionSupport.emptyList();
+
     /** Class logger. */
     @Nonnull
     private Logger log = LoggerFactory.getLogger(SignStatusListToken.class);
@@ -157,13 +168,88 @@ public class SignStatusListToken extends AbstractProfileAction {
         return true;
     }
 
+    /**
+     * Set the credentials a Status List Token may be signed with, searched when the
+     * issuer names one key.
+     *
+     * @param credentials the credentials, or null for none
+     */
+    public void setSigningCredentials(@Nullable final List<Credential> credentials) {
+        checkSetterPreconditions();
+        signingCredentials = credentials == null ? CollectionSupport.emptyList()
+                : CollectionSupport.copyToList(credentials);
+    }
+
+    /**
+     * Take the parameters signing with the key a thumbprint names.
+     *
+     * @param parameters parameters resolved by the signing configuration
+     * @param thumbprint thumbprint of the required key, or null when any key does
+     * @return the parameters to sign with, or null when the required key is not
+     *         among the ones this deployment signs with
+     */
+    @Nullable
+    private SignatureSigningParameters withRequiredKey(@Nonnull final SignatureSigningParameters parameters,
+            @Nullable final String thumbprint) {
+
+        if (thumbprint == null) {
+            return parameters;
+        }
+        if (thumbprint.equals(thumbprintOf(parameters.getSigningCredential()))) {
+            return parameters;
+        }
+
+        for (final Credential candidate : signingCredentials) {
+            if (thumbprint.equals(thumbprintOf(candidate))) {
+                final SignatureSigningParameters required = new SignatureSigningParameters();
+                required.setSignatureAlgorithm(parameters.getSignatureAlgorithm());
+                required.setSigningCredential(candidate);
+                return required;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Get the thumbprint of the key of a credential.
+     *
+     * @param credential credential to read
+     * @return the thumbprint, or null when there is none to take
+     */
+    @Nullable
+    private String thumbprintOf(@Nullable final Credential credential) {
+
+        if (credential == null) {
+            return null;
+        }
+        final JWK key = CredentialConversionUtil.credentialToKey(credential);
+        if (key == null) {
+            return null;
+        }
+        try {
+            return key.computeThumbprint().toString();
+        } catch (final JOSEException e) {
+            log.warn("{} Unable to take a thumbprint of a signing key", getLogPrefix(), e);
+            return null;
+        }
+    }
+
     /** {@inheritDoc} */
     @Override
     protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+        final SignatureSigningParameters parameters = withRequiredKey(signatureSigningParameters,
+                ctx.getKeyThumbprint());
+        if (parameters == null) {
+            log.error("{} The key the issuer of this status list names is not among the ones Status List Tokens "
+                    + "are signed with", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_SEC_CFG);
+            return;
+        }
+
         try {
-            ctx.setSignedToken(new JWSTokenSigner(
-                    DidJwkSupport.nameKeyByIssuer(signatureSigningParameters, ctx.getClaims().getIssuer()))
-                            .sign(ctx.getClaims(), TOKEN_TYPE).serialize());
+            ctx.setSignedToken(new JWSTokenSigner(DidSupport.nameKeyByIssuer(parameters, ctx.getClaims().getIssuer()))
+                    .sign(ctx.getClaims(), TOKEN_TYPE).serialize());
 
             log.debug("{} Signed status list token for {}", getLogPrefix(), ctx.getClaims().getSubject());
         } catch (final SignatureException e) {
diff --git a/openid-vci-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/did/document/document-beans.xml b/openid-vci-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/did/document/document-beans.xml
new file mode 100644
index 0000000..6e0ce45
--- /dev/null
+++ b/openid-vci-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/did/document/document-beans.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beans
+ xmlns="http://www.springframework.org/schema/beans"
+ xmlns:c="http://www.springframework.org/schema/c"
+ xmlns:context="http://www.springframework.org/schema/context"
+ xmlns:p="http://www.springframework.org/schema/p"
+ xmlns:util="http://www.springframework.org/schema/util"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
+                           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
+                           http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"
+       default-init-method="initialize" default-destroy-method="destroy">
+
+  <bean id="openidvci.profileId" class="java.lang.String"
+        c:_0="#{T(org.geant.shibboleth.plugin.openidvci.profile.config.impl.DefaultMetadataDocumentConfiguration).PROFILE_ID_DID_DOCUMENT}"/>
+  <bean id="openidvci.loggingId" class="java.lang.String"
+        c:_0="%{openidvci.logging.did-document:DID.Document}"/>
+
+  <bean id="InitializeUnverifiedRelyingPartyContext"
+        class="net.shibboleth.idp.plugin.oidc.op.profile.impl.InitializeUnverifiedRelyingPartyContext" scope="prototype" />
+
+  <bean id="FormOutboundMessage"
+        class="org.geant.shibboleth.plugin.openidvci.profile.impl.FormOutboundDidDocumentResponseMessage"
+        scope="prototype"
+        p:deploymentIssuer="#{getObject('shibboleth.oidc.issuer')}"
+        p:credentialSigningCredentials="#{getObject('openidvci.SigningCredentials') ?: getObject('shibboleth.oidc.SigningCredentialsFactory')}"
+        p:statusListSigningCredentials="#{getObject('openidvci.status-list.SigningCredentials') ?: getObject('openidvci.SigningCredentials') ?: getObject('shibboleth.oidc.SigningCredentialsFactory')}" />
+
+  <bean id="BuildErrorResponseFromEvent" class="net.shibboleth.oidc.profile.impl.BuildJSONErrorResponseFromEvent"
+        scope="prototype" p:defaultStatusCode="500" p:defaultCode="server_error"
+        p:httpServletResponseSupplier-ref="shibboleth.HttpServletResponseSupplier">
+    <property name="eventContextLookupStrategy">
+      <bean class="net.shibboleth.idp.profile.context.navigate.WebFlowCurrentEventLookupFunction" />
+    </property>
+    <property name="mappedErrors">
+      <map value-type="com.nimbusds.oauth2.sdk.ErrorObject">
+        <entry key="#{T(org.opensaml.profile.action.EventIds).IO_ERROR}"
+               value="#{T(com.nimbusds.oauth2.sdk.OAuth2Error).SERVER_ERROR}" />
+      </map>
+    </property>
+  </bean>
+
+</beans>
diff --git a/openid-vci-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/did/document/document-flow.xml b/openid-vci-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/did/document/document-flow.xml
new file mode 100644
index 0000000..b4ebaa6
--- /dev/null
+++ b/openid-vci-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/did/document/document-flow.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0"?>
+<flow xmlns="http://www.springframework.org/schema/webflow"
+      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+      xsi:schemaLocation="http://www.springframework.org/schema/webflow http://www.springframework.org/schema/webflow/spring-webflow.xsd"
+      parent="openid/vci/abstract-api">
+
+  <action-state id="InitializeMandatoryContexts">
+    <evaluate expression="InitializeProfileRequestContext" />
+    <evaluate expression="PopulateMetricContext" />
+    <evaluate expression="FlowStartPopulateAuditContext" />
+    <evaluate expression="InitializeOutboundMessageContext" />
+    <evaluate expression="InitializeUnverifiedRelyingPartyContext" />
+    <evaluate expression="SelectRelyingPartyConfiguration" />
+    <evaluate expression="SelectProfileConfiguration" />
+    <evaluate expression="'proceed'" />
+
+    <transition on="proceed" to="BuildResponseMessage" />
+  </action-state>
+
+  <bean-import resource="classpath:/META-INF/net/shibboleth/idp/flows/did/document/document-beans.xml" />
+
+</flow>
diff --git a/openid-vci-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oauth/status-list/assign/assign-beans.xml b/openid-vci-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oauth/status-list/assign/assign-beans.xml
index e674bc0..f892fc3 100644
--- a/openid-vci-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oauth/status-list/assign/assign-beans.xml
+++ b/openid-vci-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oauth/status-list/assign/assign-beans.xml
@@ -34,6 +34,14 @@
     <property name="indexCountLookupStrategy">
       <bean class="org.geant.shibboleth.plugin.openidvci.profile.logic.CredentialCountLookupFunction" />
     </property>
+    <!--
+    Names the Credential Issuer identifier of the credential in the status list url, so the
+    endpoint answers under it. Storage is keyed by the bucket alone, the tag is url only.
+    -->
+    <property name="issuerTagLookupStrategy">
+      <bean class="org.geant.shibboleth.plugin.openidvci.profile.logic.StatusListIssuerTagLookupFunction"
+            p:issuerLookupStrategy-ref="openidvci.IssuerLookupStrategy" />
+    </property>
   </bean>
 
   <bean id="openidvci.status-list.AssignmentStore"
diff --git a/openid-vci-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oauth/status-list/list/list-beans.xml b/openid-vci-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oauth/status-list/list/list-beans.xml
index d9329b1..17040cb 100644
--- a/openid-vci-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oauth/status-list/list/list-beans.xml
+++ b/openid-vci-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/oauth/status-list/list/list-beans.xml
@@ -38,7 +38,8 @@
     
   <bean id="openidvci.IssuerLookupStrategy"
         class="org.geant.shibboleth.plugin.openidvci.profile.logic.CredentialIssuerLookupFunction"
-        p:issuer="%{openidvci.issuer:}" />
+        p:issuer="%{openidvci.issuer:}"
+        p:deploymentIssuer="#{getObject('shibboleth.oidc.issuer')}" />
 
   <bean id="openidvci.status-list.Store"
         class="org.geant.shibboleth.plugin.openidvci.statuslist.storage.StatusListStore"
@@ -52,11 +53,15 @@
         p:statusListStore-ref="openidvci.status-list.Store"
         p:ttl="%{openidvci.status-list.token.defaultLifetime:PT1H}"
         p:issuerLookupStrategy-ref="openidvci.IssuerLookupStrategy"
+        p:deploymentIssuer="#{getObject('shibboleth.oidc.issuer')}"
+        p:configuredIssuer="%{openidvci.issuer:}"
+        p:signingCredentials="#{getObject('openidvci.status-list.SigningCredentials') ?: getObject('openidvci.SigningCredentials') ?: getObject('shibboleth.oidc.SigningCredentialsFactory')}"
         p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier" />
 
   <bean id="SignStatusListToken"
         class="org.geant.shibboleth.plugin.openidvci.statuslist.profile.impl.SignStatusListToken"
-        scope="prototype" />
+        scope="prototype"
+        p:signingCredentials="#{getObject('openidvci.status-list.SigningCredentials') ?: getObject('openidvci.SigningCredentials') ?: getObject('shibboleth.oidc.SigningCredentialsFactory')}" />
 
   <bean id="FormOutboundMessage"
         class="org.geant.shibboleth.plugin.openidvci.statuslist.profile.impl.FormOutboundStatusListResponseMessage"
diff --git a/openid-vci-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/openid/vci/credentials/credentials-beans.xml b/openid-vci-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/openid/vci/credentials/credentials-beans.xml
index ed1d67f..a9eaf03 100644
--- a/openid-vci-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/openid/vci/credentials/credentials-beans.xml
+++ b/openid-vci-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/openid/vci/credentials/credentials-beans.xml
@@ -140,7 +140,8 @@
 
   <bean id="openidvci.IssuerLookupStrategy"
         class="org.geant.shibboleth.plugin.openidvci.profile.logic.CredentialIssuerLookupFunction"
-        p:issuer="%{openidvci.issuer:}" />
+        p:issuer="%{openidvci.issuer:}"
+        p:deploymentIssuer="#{getObject('shibboleth.oidc.issuer')}" />
 
   <bean id="AddCredentialShell" class="org.geant.shibboleth.plugin.openidvci.profile.impl.AddCredentialShell"
         scope="prototype" p:issuerLookupStrategy-ref="openidvci.IssuerLookupStrategy"
diff --git a/openid-vci-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml b/openid-vci-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml
index dcca201..ba16b66 100644
--- a/openid-vci-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml
+++ b/openid-vci-impl/src/main/resources/META-INF/net/shibboleth/idp/service/relying-party/postconfig.xml
@@ -71,6 +71,15 @@
         c:id="#{T(org.geant.shibboleth.plugin.openidvci.profile.config.impl.DefaultMetadataDocumentConfiguration).PROFILE_ID_AUTHORIZATION_SERVER}"
         p:securityConfiguration-ref="%{idp.security.oidc.config:shibboleth.oidc.DefaultSecurityConfiguration}" />
 
+    <!--
+    Endpoint publishing the did:web document of this deployment, the keys a verifier
+    resolves a credential of a did:web issuer by. Serves anyone, as the two above do.
+    -->
+    <bean id="DID.Document" lazy-init="true"
+        class="org.geant.shibboleth.plugin.openidvci.profile.config.impl.DefaultMetadataDocumentConfiguration"
+        c:id="#{T(org.geant.shibboleth.plugin.openidvci.profile.config.impl.DefaultMetadataDocumentConfiguration).PROFILE_ID_DID_DOCUMENT}"
+        p:securityConfiguration-ref="%{idp.security.oidc.config:shibboleth.oidc.DefaultSecurityConfiguration}" />
+
     <bean id="OAuth.StatusList" lazy-init="true"
         class="org.geant.shibboleth.plugin.openidvci.statuslist.profile.config.impl.DefaultStatusListConfiguration"
         p:securityConfiguration-ref="%{idp.security.oidc.config:shibboleth.oidc.DefaultSecurityConfiguration}"
diff --git a/openid-vci-impl/src/main/resources/org/geant/shibboleth/plugin/openidvci/conf/openid-vci.properties b/openid-vci-impl/src/main/resources/org/geant/shibboleth/plugin/openidvci/conf/openid-vci.properties
index 919a7d0..af47ca2 100644
--- a/openid-vci-impl/src/main/resources/org/geant/shibboleth/plugin/openidvci/conf/openid-vci.properties
+++ b/openid-vci-impl/src/main/resources/org/geant/shibboleth/plugin/openidvci/conf/openid-vci.properties
@@ -1,5 +1,13 @@
 # Set the Credential Issuer value used in credentials
-# Leave empty value for the default of did:jwk
+# Leave empty value for the default of did:jwk, which carries the key inside the credential and
+# needs nothing published. Value 'did:web' asks for the did:web identifier of this deployment,
+# derived from the issuer of the OP, and a verifier resolves it from the document published at
+# /.well-known/did.json. Any other value is the identifier itself, an https url being the one a
+# verifier resolves from /.well-known/jwt-vc-issuer.
+# A did:jwk identifier names the signing key, so it changes when that key does. Rotating the key
+# leaves every credential issued under the previous one naming an identifier this deployment no
+# longer answers for, and their Status List Tokens no longer match. Deployments that rotate keys
+# and revoke credentials want 'did:web' or a url here.
 openidvci.issuer =
 
 # Regular expression matching OpenID VCI login flows to enable.
diff --git a/openid-vci-impl/src/main/resources/org/geant/shibboleth/plugin/openidvci/module.properties b/openid-vci-impl/src/main/resources/org/geant/shibboleth/plugin/openidvci/module.properties
index a200663..f59c763 100644
--- a/openid-vci-impl/src/main/resources/org/geant/shibboleth/plugin/openidvci/module.properties
+++ b/openid-vci-impl/src/main/resources/org/geant/shibboleth/plugin/openidvci/module.properties
@@ -44,6 +44,13 @@ other two documents a wallet reads. Urls in static/jwt-vc-issuer.json and \
 static/oauth-authorization-server.json name your host already, both are Velocity templates \
 evaluated against the issuer of the OP. Add every credential configuration id you want \
 requestable with the authorization code flow to scopes_supported of the latter. \
+Set openidvci.issuer to did:web to name this deployment by its did:web identifier in the \
+credentials it issues, and wire /.well-known/did.json to /idp/profile/did/document to publish \
+the document a verifier resolves that identifier from. The document is formed from the keys \
+credentials and Status List Tokens are signed with, so there is no file to edit and nothing to \
+keep in step by hand. A key stays in it for as long as it is among those signing credentials, \
+so introduce a key by adding it to the list and retire it by removing it only once nothing \
+unexpired was signed with it. \
 If you run the OpenID Federation plugin, add <import resource="openid-vci-oidfed.xml" /> in \
 conf/global.xml to state this deployment as a Credential Issuer in its Entity Configuration. Do \
 not import it without that plugin, the IdP will not start.
diff --git a/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/profile/logic/CredentialIssuerLookupFunctionTest.java b/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/profile/logic/CredentialIssuerLookupFunctionTest.java
index 063e0f7..12fefb7 100644
--- a/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/profile/logic/CredentialIssuerLookupFunctionTest.java
+++ b/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/profile/logic/CredentialIssuerLookupFunctionTest.java
@@ -65,4 +65,34 @@ public class CredentialIssuerLookupFunctionTest {
         Assert.assertNull(new CredentialIssuerLookupFunction().apply(null));
     }
 
+    @Test
+    public void testDidWebKeyword() {
+        final CredentialIssuerLookupFunction function = new CredentialIssuerLookupFunction();
+        function.setIssuer("did:web");
+        function.setDeploymentIssuer("https://example.org/idp/path");
+        Assert.assertEquals(function.apply(null), "did:web:example.org");
+    }
+
+    @Test
+    public void testDidJwkKeywordIsNotTakenLiterally() {
+        final CredentialIssuerLookupFunction function = new CredentialIssuerLookupFunction();
+        function.setIssuer("did:jwk");
+        Assert.assertNull(function.apply(null));
+    }
+
+    @Test
+    public void testUrlKeyword() {
+        final CredentialIssuerLookupFunction function = new CredentialIssuerLookupFunction();
+        function.setIssuer("url");
+        function.setDeploymentIssuer("https://example.org/idp");
+        Assert.assertEquals(function.apply(null), "https://example.org/idp");
+    }
+
+    @Test
+    public void testIdentifierTakenAsItIs() {
+        final CredentialIssuerLookupFunction function = new CredentialIssuerLookupFunction();
+        function.setIssuer("https://example.org");
+        Assert.assertEquals(function.apply(null), "https://example.org");
+    }
+
 }
diff --git a/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/security/impl/DidJwkSupportTest.java b/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/security/impl/DidJwkSupportTest.java
deleted file mode 100644
index 1703a34..0000000
--- a/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/security/impl/DidJwkSupportTest.java
+++ /dev/null
@@ -1,93 +0,0 @@
-/*
- * Copyright (c) 2025, GÉANT
- *
- * Licensed under the Apache License, Version 2.0 (the “License”); you may not
- * use this file except in compliance with the License. You may obtain a copy
- * of the License at
- *
- *    http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an “AS IS” BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.geant.shibboleth.plugin.openidvci.security.impl;
-
-import org.testng.Assert;
-import org.testng.annotations.BeforeMethod;
-import org.testng.annotations.DataProvider;
-import org.testng.annotations.Test;
-
-import com.nimbusds.jose.jwk.Curve;
-import com.nimbusds.jose.jwk.ECKey;
-import com.nimbusds.jose.jwk.gen.ECKeyGenerator;
-
-import net.shibboleth.oidc.jwa.support.SignatureConstants;
-import net.shibboleth.oidc.security.CredentialConversionUtil;
-import net.shibboleth.oidc.security.credential.BasicJWKCredential;
-import net.shibboleth.oidc.security.jose.SignatureSigningParameters;
-
-/**
- * Unit tests for {@link DidJwkSupport}.
- */
-public class DidJwkSupportTest {
-
-    private static final String DID = "did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6IngiLCJ5IjoieSJ9";
-
-    private ECKey key;
-
-    private SignatureSigningParameters parameters;
-
-    @BeforeMethod
-    protected void setUp() throws Exception {
-        key = new ECKeyGenerator(Curve.P_256).keyID("signingvci").generate();
-
-        final BasicJWKCredential credential = new BasicJWKCredential();
-        credential.setPublicKey(key.toECPublicKey());
-        credential.setPrivateKey(key.toECPrivateKey());
-        credential.setKid(key.getKeyID());
-
-        parameters = new SignatureSigningParameters();
-        parameters.setSigningCredential(credential);
-        parameters.setSignatureAlgorithm(SignatureConstants.ALGO_ID_SIGNATURE_ES_256);
-    }
-
-    @Test
-    public void testNamedByDidJwk() throws Exception {
-        final SignatureSigningParameters named = DidJwkSupport.nameKeyByIssuer(parameters, DID);
-
-        Assert.assertEquals(CredentialConversionUtil.resolveKid(named.getSigningCredential()), DID + "#0");
-        Assert.assertEquals(named.getSignatureAlgorithm(), SignatureConstants.ALGO_ID_SIGNATURE_ES_256);
-        Assert.assertEquals(named.getSigningCredential().getPublicKey(), key.toECPublicKey());
-        Assert.assertEquals(named.getSigningCredential().getPrivateKey(), key.toECPrivateKey());
-    }
-
-    /** The key of the given parameters is left as it is. */
-    @Test
-    public void testGivenParametersUntouched() throws Exception {
-        DidJwkSupport.nameKeyByIssuer(parameters, DID);
-
-        Assert.assertEquals(CredentialConversionUtil.resolveKid(parameters.getSigningCredential()), "signingvci");
-    }
-
-    @Test(dataProvider = "notDidJwk")
-    public void testIssuerIsNoDidJwk(final String issuer) throws Exception {
-        Assert.assertSame(DidJwkSupport.nameKeyByIssuer(parameters, issuer), parameters);
-    }
-
-    @DataProvider(name = "notDidJwk")
-    public Object[][] notDidJwk() {
-        return new Object[][] { { null }, { "" }, { "https://example.org" }, { "did:web:example.org" }, };
-    }
-
-    @Test
-    public void testNoSigningCredential() throws Exception {
-        final SignatureSigningParameters empty = new SignatureSigningParameters();
-
-        Assert.assertSame(DidJwkSupport.nameKeyByIssuer(empty, DID), empty);
-    }
-
-}
diff --git a/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/security/impl/DidSupportTest.java b/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/security/impl/DidSupportTest.java
new file mode 100644
index 0000000..6dd2f74
--- /dev/null
+++ b/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/security/impl/DidSupportTest.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (c) 2025, GÉANT
+ *
+ * Licensed under the Apache License, Version 2.0 (the “License”); you may not
+ * use this file except in compliance with the License. You may obtain a copy
+ * of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an “AS IS” BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.geant.shibboleth.plugin.openidvci.security.impl;
+
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import com.nimbusds.jose.jwk.Curve;
+import com.nimbusds.jose.jwk.ECKey;
+import com.nimbusds.jose.jwk.gen.ECKeyGenerator;
+
+import net.shibboleth.oidc.jwa.support.SignatureConstants;
+import net.shibboleth.oidc.security.CredentialConversionUtil;
+import net.shibboleth.oidc.security.credential.BasicJWKCredential;
+import net.shibboleth.oidc.security.jose.SignatureSigningParameters;
+
+/** Unit tests for {@link DidSupport}. */
+public class DidSupportTest {
+
+    private static final String DID_JWK = "did:jwk:eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6IngiLCJ5IjoieSJ9";
+
+    private static final String DID_WEB = "did:web:example.org";
+
+    private ECKey key;
+
+    private SignatureSigningParameters parameters;
+
+    @BeforeMethod
+    protected void setUp() throws Exception {
+        key = new ECKeyGenerator(Curve.P_256).keyID("signingvci").generate();
+        final BasicJWKCredential credential = new BasicJWKCredential();
+        credential.setPublicKey(key.toECPublicKey());
+        credential.setPrivateKey(key.toECPrivateKey());
+        credential.setKid(key.getKeyID());
+        parameters = new SignatureSigningParameters();
+        parameters.setSigningCredential(credential);
+        parameters.setSignatureAlgorithm(SignatureConstants.ALGO_ID_SIGNATURE_ES_256);
+    }
+
+    @Test
+    public void testNamedByDidJwk() throws Exception {
+        final SignatureSigningParameters named = DidSupport.nameKeyByIssuer(parameters, DID_JWK);
+        Assert.assertEquals(CredentialConversionUtil.resolveKid(named.getSigningCredential()), DID_JWK + "#0");
+        Assert.assertEquals(named.getSignatureAlgorithm(), SignatureConstants.ALGO_ID_SIGNATURE_ES_256);
+        Assert.assertEquals(named.getSigningCredential().getPublicKey(), key.toECPublicKey());
+        Assert.assertEquals(named.getSigningCredential().getPrivateKey(), key.toECPrivateKey());
+    }
+
+    @Test
+    public void testNamedByDidWeb() throws Exception {
+        final SignatureSigningParameters named = DidSupport.nameKeyByIssuer(parameters, DID_WEB);
+        Assert.assertEquals(CredentialConversionUtil.resolveKid(named.getSigningCredential()),
+                DID_WEB + "#signingvci");
+        Assert.assertEquals(named.getSignatureAlgorithm(), SignatureConstants.ALGO_ID_SIGNATURE_ES_256);
+        Assert.assertEquals(named.getSigningCredential().getPublicKey(), key.toECPublicKey());
+        Assert.assertEquals(named.getSigningCredential().getPrivateKey(), key.toECPrivateKey());
+    }
+
+    @Test(dataProvider = "dids")
+    public void testGivenParametersUntouched(final String issuer) throws Exception {
+        DidSupport.nameKeyByIssuer(parameters, issuer);
+        Assert.assertEquals(CredentialConversionUtil.resolveKid(parameters.getSigningCredential()), "signingvci");
+    }
+
+    @DataProvider(name = "dids")
+    public Object[][] dids() {
+        return new Object[][] { { DID_JWK }, { DID_WEB }, };
+    }
+
+    @Test(dataProvider = "notDid")
+    public void testIssuerIsNoDid(final String issuer) throws Exception {
+        Assert.assertSame(DidSupport.nameKeyByIssuer(parameters, issuer), parameters);
+    }
+
+    @DataProvider(name = "notDid")
+    public Object[][] notDid() {
+        return new Object[][] { { null }, { "" }, { "https://example.org" }, { "did:webvh:abc:example.org" }, };
+    }
+
+    @Test
+    public void testDidWebWithoutKeyName() throws Exception {
+        final BasicJWKCredential unnamed = new BasicJWKCredential();
+        unnamed.setPublicKey(key.toECPublicKey());
+        final SignatureSigningParameters nameless = new SignatureSigningParameters();
+        nameless.setSigningCredential(unnamed);
+        Assert.assertSame(DidSupport.nameKeyByIssuer(nameless, DID_WEB), nameless);
+    }
+
+    @Test
+    public void testNoSigningCredential() throws Exception {
+        final SignatureSigningParameters empty = new SignatureSigningParameters();
+        Assert.assertSame(DidSupport.nameKeyByIssuer(empty, DID_JWK), empty);
+    }
+
+    @Test(dataProvider = "deploymentIssuers")
+    public void testToDidWeb(final String issuer, final String expected) throws Exception {
+        Assert.assertEquals(DidSupport.toDidWeb(issuer), expected);
+    }
+
+    @DataProvider(name = "deploymentIssuers")
+    public Object[][] deploymentIssuers() {
+        return new Object[][] {
+            { "https://example.org", "did:web:example.org" },
+            { "https://example.org/", "did:web:example.org" },
+            { "https://example.org/idp/path", "did:web:example.org" },
+            { "https://example.org:8443", "did:web:example.org%3A8443" },
+            { "https://example.org:8443/idp", "did:web:example.org%3A8443" },
+            { null, null },
+            { "", null },
+            { "not a url", null },
+            { "urn:example:org", null },
+        };
+    }
+
+}

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


More information about the commits mailing list