[java-idp-plugin-vci] branch main updated: Improve readability of README and tag the parts that require more effort
Codeberg
noreply at shibboleth.net
Sun Sep 13 07:51:42 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/3ba111b32fe11be9b55dab2b1eaa3def028b4ae8
The following commit(s) were added to refs/heads/main by this push:
new 3ba111b Improve readability of README and tag the parts that require more effort
3ba111b is described below
commit 3ba111b32fe11be9b55dab2b1eaa3def028b4ae8
Author: Janne Lauros <janne.lauros at csc.fi>
AuthorDate: Sun Sep 13 10:51:28 2026 +0300
Improve readability of README and tag the parts that require more effort
---
README.md | 191 ++++++++------------------------------------------------------
1 file changed, 22 insertions(+), 169 deletions(-)
diff --git a/README.md b/README.md
index bda40ff..6bdbb1b 100644
--- a/README.md
+++ b/README.md
@@ -2,6 +2,7 @@
> **Note**
> This plugin is under construction and all the features are still subject to change.
+> This README pretends to be a wiki that it is not. It is very much WIP and a challenging read.
## Overview
@@ -32,18 +33,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: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
> **Info**
-> This plugin is not released and has no `plugin.sh -I` identifier yet. Until it is released,
-> the distribution is in directory *download/* of this repository and you install it from
+> This plugin is not released. Until it is released,
+> the distribution is in directory *download/* (only seldom updated) of this repository and you install it from
> there by url, see below.
| Plugin | Plugin ID | Module(s) | Depends on | Bug Reporting |
@@ -73,11 +68,7 @@ Requires IdP **5.2.3** or later.
> *conf/openid-vci-oidfed.xml* into *conf/global.xml* to state this deployment as a Credential
> Issuer in an Entity Configuration. Install them before that import, without them the IdP does
> not start.
->
-> If your IdP image ships released versions of the OIDC plugins, remove them with
-> `bin/plugin.sh -r` before you install the snapshots, `net.shibboleth.idp.plugin.oidc.op` and
-> `net.shibboleth.idp.plugin.oidc.config` of them. A released plugin and a snapshot of the same
-> plugin cannot live side by side.
+
### Installation example
@@ -114,17 +105,13 @@ bin/plugin.sh \
-i https://codeberg.org/Shibboleth/java-idp-plugin-vci/raw/branch/main/download/shibboleth-idp-plugin-openid-vci-0.9.0-SNAPSHOT.tar.gz
```
-The last command downloads the distribution and its signature from the same directory and
-verifies the signature against the truststore that was written just above it, so it does not
-need `--noCheck`.
-
</details>
Installing the plugin activates module `OpenID-VCI` and creates six files:
| File | Description |
|---|---|
-| *conf/openid-vci.properties* | Every deployer-facing property. Auto-loaded by `idp.searchForProperties`. |
+| *conf/openid-vci.properties* | Every deployer-facing property.|
| *conf/openid-vci-credentials.xml* | Which key signs credentials, Status List Tokens and issuer metadata. Inactive by default. |
| *conf/openid-vci-oidfed.xml* | States this deployment as a Credential Issuer in OpenID Federation. Not imported by default. |
| *static/openid-credential-issuer.json* | Credential Issuer metadata, as a Velocity template. |
@@ -136,11 +123,10 @@ Configurations. There is no template for it. See [Credential Configurations](#cr
## First installation, by example
-This chapter goes through one working deployment from start to end. First a credential is
+This chapter goes through one working (not guaranteed) deployment from start to end. First a credential is
issued to a wallet with the pre-authorized code flow, then the same credential with the
authorization code flow and finally the credential is withdrawn with the Status List. The
-examples are from a running test deployment so they fit together. They also take shortcuts you
-should not copy to production and those are mentioned where they appear.
+examples are from a running test deployment so they fit together.
`https://issuer.example.org` is your IdP's OIDC issuer everywhere below.
@@ -245,14 +231,6 @@ The example has the same credential in both formats, the one that is `dc+sd-jwt`
that is `vc+sd-jwt`. You do not need both, the rest of this document keeps them both around to
show where the formats differ.
-Members that depend on the format:
-
-| Member | `dc+sd-jwt` | `vc+sd-jwt` |
-|---|---|---|
-| `vct` | **required** | not used |
-| `credential_definition` | not used | **required**, carries `@context` and `type` |
-| Claim `path` | flat, one segment: `["mail"]` | nested under `credentialSubject`: `["credentialSubject","mail"]` |
-
The `@context` of a `vc+sd-jwt` credential has to resolve. The first entry is the W3C one and
the second one is yours, you publish it yourself as a static file. The terms of your claims are
in it:
@@ -275,9 +253,8 @@ in it:
</details>
-You publish it in the url the `@context` names. It is a static file of Jetty like the discovery
-documents are, in *webapps/root/credentials/*, and the rewrite rule that drops the `.json` from
-the url is in [Discovery documents](#discovery-documents).
+You publish it in the url the `@context` names. For this example deployment it is a static file of Jetty
+and included jetty rewrite example in in [Discovery documents](#discovery-documents).
If this feels like a lot of trouble, it is not needed at all when you do not issue the
`vc+sd-jwt` credential. Leave the configuration of that credential out and the file goes with
@@ -321,11 +298,6 @@ It goes inside the credential's own configuration, next to `format` and `vct`:
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.
-The settings live in *metadata/verifiable-credentials.json* and they are re-read the same way
-the rest of the file is, within the bounds of `openidvci.credentialConfigurations.minRefreshDelay`
-and `…maxRefreshDelay`. Adding a credential and deciding its validity is one edit of one file
-and it needs no reload.
-
##### The validity window
**The window is `not_before` until `not_before` plus `lifetime`.** The lifetime is the length of
@@ -338,18 +310,9 @@ Write `not_before` as one of these:
|---|---|---|
| `P30D`, `PT12H` | That much after the credential was issued | A window of the same length for everyone, whenever they ask |
| `2026-08-01T06:00Z` | At exactly that minute, `Z` being UTC | A moment you decide, the start of a school year |
-| `2026-08-01T00:00+03:00` | The same, in the offset you state | A moment your users think of in local time |
+| `2026-08-01T00:00+03:00` | The same, in the offset you state | Well, if UTC is too hard for you. I may remove this. |
| `2026-08-01` | At midnight UTC of that day | The same, when the time of day does not matter |
-A date is midnight UTC, not midnight where you are. Finnish summer time is three hours ahead,
-so `2026-08-01` opens the window at 03:00 local time.
-
-A window that has already closed issues nothing. The log says so when the file is read, and a
-request for such a credential is refused.
-
-Months and years are not durations. `P1Y` and `P6M` are rejected, write them as days, `P365D`.
-This is the same rule the Duration settings of the IdP follow everywhere else.
-
What ends up in the credential:
| Format | Members carrying the window |
@@ -363,17 +326,8 @@ A credential whose window opens at issuance carries no `nbf`.
Importing *conf/openid-vci-oidfed.xml* into *conf/global.xml* is what turns this on. That one
import states this deployment as a Credential Issuer in its Entity Configuration and makes its
-credentials name the same identifier. Without it a credential says nothing about a federation,
-which is what a deployment outside one wants.
-
-`"oidfed": false` in a Credential Configuration takes one credential back out. That is the only
-way to prevent it.
+credentials name the same identifier.
-The value is not yours to write. It is the `credential_issuer` of your Credential Issuer
-metadata and it is read from there, because
-[OpenID Federation Digital Credentials Profile](https://fidescommunity.github.io/DIIP/) requires
-the Entity Identifier and `credential_issuer` to be the same string. A `dc+sd-jwt` credential
-carries it in `fed`, a `vc+sd-jwt` one in `termsOfUse`:
```json
{
@@ -389,11 +343,12 @@ carries it in `fed`, a `vc+sd-jwt` one in `termsOfUse`:
}
}
```
+`"oidfed": false` in Credential Configuration can be used to exclude those claims
+from particulrt credential i.e. leave it out from federation.
The federation module decides the `iss` and `sub` of your Entity Configuration, from
-`idp.oidfed.entityID`, and nothing joins that up with `credential_issuer`. When the two differ
-the log says so as the Entity Configuration is built, because a credential points at
-`credential_issuer` and a verifier resolving it would reach a statement about another entity.
+`idp.oidfed.entityID`, and nothing joins that up with `credential_issuer`. It is your
+responsibility to define matching values.
##### Whether a credential goes on a Status List
@@ -411,26 +366,6 @@ round and make the Status List something each credential opts into with `"status
`credentialLifetime` you set on one wallet in *conf/relying-party.xml*. Leave `lifetime` out of
a credential whose validity you want to keep steering per wallet.
-#### Notes
-
-Do not set claims `"mandatory": true` while you are still experimenting. A mandatory claim
-makes the Credential Offer validation compare the offered paths against the configured ones for
-exact equality. Any difference rejects the offer instead of ignoring the extra claim.
-
-Claim nesting of `vc+sd-jwt` is done by the credential type when the credential is built, not
-by the paths you send in a Credential Offer. The `credentialSubject` prefix belongs to this
-file, not to the offer.
-
-`_issuance_configuration` is a member of this plugin's own and the specification knows nothing
-about it. The leading underscore is what keeps it that way. No specification of OpenID or of
-the IETF defines a JSON member that begins with one. A later version of the specification, or
-another extension of it, is unable to take the name from under you. The underscore also says at
-a glance that the member is not from the specification. Every other member of the file is.
-
-A member the plugin does not recognise inside `_issuance_configuration` is ignored. An
-unparsable value in one is not: it rejects the whole file, and the last file that did parse
-stays in use until you fix it.
-
### Signing keys
**File(s):** *conf/openid-vci-credentials.xml*, *conf/openid-vci.properties*
@@ -439,39 +374,15 @@ stays in use until you fix it.
Nothing here is active by default. Credentials are signed with the OP's own response signing
credentials and for a first installation you may leave it as it is. That works for as long as
the OP has an EC key, `credentials/idp-signing-es.jwk` of a normal installation. The signing of
-this plugin does ES256, ES384 and ES512 only, so the RSA key of the OP is of no use here and
-without the EC one there is nothing to sign a credential with.
+this plugin does ES256, ES384 and ES512 only, so the RSA key of the OP is of no use here.
If you do take a key of your own into use, defining **openidvci.SigningCredentials**
*replaces* the OP's credentials instead of adding to them. Nothing else changes while the
Credential Issuer identifier is `did:jwk`, which is the default. The key travels inside the
credential itself and there is nothing to publish.
-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.
+TODO: the numerous options how to manage keys and which identifier type expects what.
-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
-them there refuses to initialize without them. An IdP that imports *conf/openid-vci-oidfed.xml*
-while **openidvci.SigningCredentials** is undefined does not start.
-
-You need to import the file where its beans are needed:
-
-- into *conf/credentials.xml* normally
-- into *conf/global.xml* instead, if you state this deployment in OpenID Federation. Publishing
- the list in an Entity Configuration works only from the root context.
### Credential Issuer identifiers
@@ -489,8 +400,6 @@ Token, so the two always agree.
| `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
@@ -542,35 +451,9 @@ 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).
+TODO: Explain the relation between Status Lists, Identifiers and Signing keys.
#### OpenID Federation
@@ -741,30 +624,6 @@ Every configuration id of the body must exist in *metadata/verifiable-credential
offer is rejected. There is no attribute resolution when the wallet later redeems the code. The
values above are what is issued.
-#### Notes
-
-`openidvci.preAuthorizedCode.defaultLength` decides which one of two quite different codes you
-get. It is worth understanding before you pick a value.
-
-With the default `0` the code is **self-contained**. The whole offer is sealed inside it and no
-server side storage is used. Its single use is enforced by recording its identifier in
-`shibboleth.ReplayCache`, so the replay protection is only as good as that cache, i.e. in a
-cluster with memory backed storage it is per node. The code is also long, which makes a dense
-QR code.
-
-With value `10` or more the code is a short random identifier and the offer is kept in
-`openidvci.StorageService`. This needs real server side storage, gives you short codes and the
-offer is deleted when it is redeemed.
-
-`openidvci.TokenManipulationStrategy` in **OpenID.VCI.Token** is not decoration. It carries the
-credential content into the access token and without it no credential is issued. The bean comes
-with the plugin, you do not need to define it.
-
-`p:dpopProofNonceGenerator="#{null}"` turns off the server side DPoP nonce requirement. Wallet
-support for DPoP nonces is not very good, so start with it off.
-
-A wallet that redeems a pre-authorized code does not authenticate to the token endpoint, so
-`none` has to be in `idp.oidc.tokenEndpointAuthMethods`.
### Authorization code flow
@@ -974,7 +833,8 @@ into, is in
Withdrawing that credential is setting bit 41337 of list `2027-03`. The list a credential lands
in follows from the month the credential **expires**, not from the month it was issued. With the
default `credentialLifetime` of `P180D` a credential that is issued in September 2026 belongs to
-the list of March 2027. One list has 1 048 576 slots.
+the list of March 2027. As we need to keep track of credentials and revocation we place them
+to buckets per expiration.
The expiration is the one the credential actually gets, so a credential with a `lifetime` or a
`not_before` of its own lands in the list its own window chooses. A `not_before` fixed to a date
@@ -988,10 +848,8 @@ Three endpoints do the work and they have three different audiences:
| `/idp/profile/oauth/status-list/assignments` | your own systems | client | `POST` of JSON |
| `/idp/profile/oauth/status-list/revoke` | your own systems | client | `POST` of JSON |
-You enable them as in [Reference](#reference). **OAuth.StatusList** goes to
-**shibboleth.UnverifiedRelyingParty** as a verifier comes without registration.
-**OAuth.StatusList.Revoke** and **OAuth.StatusList.Assignments** go to the relying party of
-whoever is allowed to revoke. The `offer-caller` example above has both already.
+You enable them as in [Reference](#reference). See **OAuth.StatusList**, **OAuth.StatusList.Revoke**
+and **OAuth.StatusList.Assignments**.
One property you need to change before you issue anything:
@@ -1136,10 +994,6 @@ credential got. A record you did not collect within that week is a credential yo
to revoke anymore. Poll the endpoint on a schedule that is shorter than the retention, not when
you happen to need a record.
-`limit` may be 1000 at most and it defaults to 100. A read probes a limited number of
-positions, so a page may come back with fewer records than you asked while `next` is still
-short of `high_water`. Keep calling until the two meet, a short page is not the end.
-
A list is published until its month is over. By then every credential in it has expired. Asking
for a month that has ended is `410 retired_status_list` and asking for something that is not a
month at all is `404 unknown_status_list`, so a verifier is able to tell a withdrawn list from
@@ -1286,7 +1140,6 @@ one publishes and how a verifier reaches the key is in
| `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
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list