[java-idp-plugin-vci] branch main updated: Copy VCDM members of credential_definition into vc+sd-jwt credentials

Codeberg noreply at shibboleth.net
Fri Oct 9 14:02:22 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/800ca674c24a8b92e49c93c49de45d591e710479

The following commit(s) were added to refs/heads/main by this push:
     new 800ca67  Copy VCDM members of credential_definition into vc+sd-jwt credentials
800ca67 is described below

commit 800ca674c24a8b92e49c93c49de45d591e710479
Author: Janne Lauros <janne.lauros at csc.fi>
AuthorDate: Fri Oct 9 17:02:04 2026 +0300

    Copy VCDM members of credential_definition into vc+sd-jwt credentials
---
 README.md                                          | 283 ++++++++++++++++++++-
 .../step-1/metadata/verifiable-credentials.json    |  33 ++-
 .../GeantIncubatorDiploma_W3C.schema.json          |  24 ++
 .../step-2/metadata/verifiable-credentials.json    |  33 ++-
 .../GeantIncubatorDiploma_W3C.schema.json          |  24 ++
 .../step-3/metadata/verifiable-credentials.json    |  33 ++-
 .../GeantIncubatorDiploma_W3C.schema.json          |  24 ++
 .../step-4/metadata/verifiable-credentials.json    |  33 ++-
 .../GeantIncubatorDiploma_W3C.schema.json          |  24 ++
 ...eth-idp-plugin-openid-vci-0.9.0-SNAPSHOT.tar.gz | Bin 446857 -> 451818 bytes
 ...idp-plugin-openid-vci-0.9.0-SNAPSHOT.tar.gz.asc |  22 +-
 .../credential/CredentialConfigurations.java       |  27 +-
 .../openidvci/credential/CredentialDefinition.java |  78 +++++-
 .../credential/CredentialConfigurationsTest.java   |  27 ++
 .../credential/CredentialDefinitionTest.java       |  23 ++
 .../CredentialDefinitionWithMembers.json           |  35 +++
 ...FormJsonLdSelectiveDisclosureJWTCredential.java |  45 +++-
 17 files changed, 726 insertions(+), 42 deletions(-)

diff --git a/README.md b/README.md
index 1634c7d..30702da 100644
--- a/README.md
+++ b/README.md
@@ -63,7 +63,7 @@ Both issuance flows are implemented:
   where the wallet makes a normal OAuth2 authorization request and the credential
   content comes in an IdP attribute
 
-Three credential formats are issued, `dc+sd-jwt` (IETF SD-JWT VC), `vc+sd-jwt` (W3C VCDM 2.0
+Three credential formats are issued, `dc+sd-jwt` (IETF SD-JWT VC), `vc+sd-jwt` ([W3C VCDM 2.0](https://www.w3.org/TR/vc-data-model-2.0/)
 enveloped in SD-JWT) and `mso_mdoc` (ISO mdoc). All three do selective disclosure. Revocation is
 published as [IETF Token Status List](https://datatracker.ietf.org/doc/draft-ietf-oauth-status-list/).
 
@@ -335,7 +335,38 @@ of the specification, per credential format. Below only the members that need ex
         "type": [
           "VerifiableCredential",
           "GeantIncubatorDiploma_W3C"
-        ]
+        ],
+        "name": [
+          { "@value": "Incubator Diploma", "@language": "en" },
+          { "@value": "Inkubaattorin tutkinto", "@language": "fi" }
+        ],
+        "description": [
+          { "@value": "Diploma issued by the Trust and Identity Incubator", "@language": "en" },
+          { "@value": "Trust and Identity Incubatorin myöntämä tutkinto", "@language": "fi" }
+        ],
+        "credentialSchema": {
+          "id": "https://issuer.example.org/credentials/GeantIncubatorDiploma_W3C.schema.json",
+          "type": "JsonSchema"
+        },
+        "termsOfUse": {
+          "id": "https://issuer.example.org/credentials/GeantIncubatorDiploma_W3C/terms",
+          "type": "IncubatorTermsOfUse"
+        },
+        "relatedResource": [
+          {
+            "id": "https://issuer.example.org/credentials/GeantIncubatorDiploma_W3C",
+            "digestSRI": "sha384-eIeXcHH79JdWiST9HYUoE0335npS6oIKV74bzZVqQUHWOH+v+7S+x/GLe57eCORS"
+          },
+          {
+            "id": "https://issuer.example.org/credentials/GeantIncubatorDiploma_W3C.schema.json",
+            "digestSRI": "sha384-5V21oQMDqipY/5lAXLyVIeL6+YnCC0fzWfcxPiiFpgddZFezHla6qpfWCkv12Qut"
+          }
+        ],
+        "evidence": {
+          "id": "https://issuer.example.org/credentials/GeantIncubatorDiploma_W3C/evidence",
+          "type": "Evidence",
+          "description": "Completion of the incubator programme, confirmed by the incubator board"
+        }
       },
       "credential_metadata": {
         "display": [
@@ -371,9 +402,17 @@ 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, this step keeps them both around to show where the
 formats differ. The later steps add `dc+sd-jwt` credentials only.
 
-The `@context` of a `vc+sd-jwt` credential has to resolve. The first entry is the W3C one and
-the second one is yours, you publish it yourself as a static file. The terms of your claims are
-in it:
+##### The W3C credential
+
+This part is for `vc+sd-jwt` only, a [W3C VCDM 2.0](https://www.w3.org/TR/vc-data-model-2.0/)
+credential. If you issue `dc+sd-jwt` credentials only, skip it: none of the files or members
+below are needed.
+
+The [`@context`](https://www.w3.org/TR/vc-data-model-2.0/#contexts) of a `vc+sd-jwt` credential
+has two entries and both have to resolve. The first is the
+[VCDM 2.0 base context](https://www.w3.org/TR/vc-data-model-2.0/#base-context) of W3C, always
+first, which defines the standard members of a credential. The second is yours. It defines the
+claims of your `credentialSubject`, and you publish it yourself as a static file:
 
 <details>
 <summary>webapps/root/credentials/GeantIncubatorDiploma_W3C.json</summary>
@@ -396,7 +435,64 @@ Full file: [webapps/root/credentials/GeantIncubatorDiploma_W3C.json](docs/exampl
 </details>
 
 You publish it at the url that is in `@context`, see [Metadata endpoints](#metadata-endpoints)
-below. Without `vc+sd-jwt` credentials the file is not needed.
+below.
+
+`@context` and [`type`](https://www.w3.org/TR/vc-data-model-2.0/#types) are required and
+published. The members below are optional and go to the credential only. Their content is yours,
+the plugin copies it to every credential as it is.
+
+| Member | Here |
+|---|---|
+| [`name`](https://www.w3.org/TR/vc-data-model-2.0/#names-and-descriptions) | Name in English and Finnish, as [language value objects](https://www.w3.org/TR/vc-data-model-2.0/#language-and-base-direction) |
+| [`description`](https://www.w3.org/TR/vc-data-model-2.0/#names-and-descriptions) | Description in English and Finnish, as [language value objects](https://www.w3.org/TR/vc-data-model-2.0/#language-and-base-direction) |
+| [`credentialSchema`](https://www.w3.org/TR/vc-data-model-2.0/#data-schemas) | A [JSON Schema](https://www.w3.org/TR/vc-json-schema/#jsonschema) of the credential, a static file next to the context |
+| [`termsOfUse`](https://www.w3.org/TR/vc-data-model-2.0/#terms-of-use) | A policy of your own, VCDM defines no types for it |
+| [`relatedResource`](https://www.w3.org/TR/vc-data-model-2.0/#integrity-of-related-resources) | [Subresource Integrity](https://www.w3.org/TR/SRI/) digests of the context and the schema |
+| [`evidence`](https://www.w3.org/TR/vc-data-model-2.0/#evidence) | How the diploma was granted, the same for everyone |
+
+The plugin adds [`id`](https://www.w3.org/TR/vc-data-model-2.0/#identifiers), [`issuer`](https://www.w3.org/TR/vc-data-model-2.0/#issuer),
+[`validFrom` and `validUntil`](https://www.w3.org/TR/vc-data-model-2.0/#validity-period) and
+[`credentialSubject`](https://www.w3.org/TR/vc-data-model-2.0/#credential-subject) itself. You cannot configure them. `termsOfUse` is
+the one member both of you can set. When a `vc+sd-jwt` credential is in a federation, see
+[Step 4](#step-4-make-the-url-credential-an-openid-federation-credential), the plugin adds a
+federation entry and your entries go in the same array. See
+[Credential Configuration settings](#credential-configuration-settings) for the full list.
+
+The schema of the example:
+
+<details>
+<summary>webapps/root/credentials/GeantIncubatorDiploma_W3C.schema.json</summary>
+
+```json
+{
+  "$schema": "https://json-schema.org/draft/2020-12/schema",
+  "$id": "https://issuer.example.org/credentials/GeantIncubatorDiploma_W3C.schema.json",
+  "title": "Incubator Diploma, W3C",
+  "type": "object",
+  "required": ["@context", "type", "issuer", "credentialSubject"],
+  "properties": {
+    "type": {
+      "type": "array",
+      "contains": { "const": "GeantIncubatorDiploma_W3C" }
+    },
+    "credentialSubject": {
+      "type": "object",
+      "properties": {
+        "diploma": { "type": "string" },
+        "mail": { "type": "string" },
+        "eppn": { "type": "string" },
+        "givenName": { "type": "string" },
+        "familyName": { "type": "string" },
+        "affiliation": { "type": "string" }
+      }
+    }
+  }
+}
+```
+
+Full file: [webapps/root/credentials/GeantIncubatorDiploma_W3C.schema.json](docs/example/step-1/webapps/root/credentials/GeantIncubatorDiploma_W3C.schema.json)
+
+</details>
 
 #### Settings of your own, per credential
 
@@ -452,7 +548,7 @@ What ends up in the credential:
 | Format | Members carrying the window |
 |---|---|
 | `dc+sd-jwt` | `iat` is the instant of issuance, `exp` closes the window, and `nbf` opens it whenever that is not the instant of issuance |
-| `vc+sd-jwt` | The same three, and `validFrom` and `validUntil` as the same two ends of the window |
+| `vc+sd-jwt` | The same three, and [`validFrom` and `validUntil`](https://www.w3.org/TR/vc-data-model-2.0/#validity-period) as the same two ends of the window |
 | `mso_mdoc` | `validityInfo` of the mobile security object, `signed` is the instant of issuance, `validFrom` and `validUntil` the two ends of the window |
 
 A credential whose window opens at issuance carries no `nbf`. The instant of issuance is rounded
@@ -1379,7 +1475,38 @@ Each file as it is after this step, the changes marked:
          "type": [
            "VerifiableCredential",
            "GeantIncubatorDiploma_W3C"
-         ]
+         ],
+         "name": [
+           { "@value": "Incubator Diploma", "@language": "en" },
+           { "@value": "Inkubaattorin tutkinto", "@language": "fi" }
+         ],
+         "description": [
+           { "@value": "Diploma issued by the Trust and Identity Incubator", "@language": "en" },
+           { "@value": "Trust and Identity Incubatorin myöntämä tutkinto", "@language": "fi" }
+         ],
+         "credentialSchema": {
+           "id": "https://issuer.example.org/credentials/GeantIncubatorDiploma_W3C.schema.json",
+           "type": "JsonSchema"
+         },
+         "termsOfUse": {
+           "id": "https://issuer.example.org/credentials/GeantIncubatorDiploma_W3C/terms",
+           "type": "IncubatorTermsOfUse"
+         },
+         "relatedResource": [
+           {
+             "id": "https://issuer.example.org/credentials/GeantIncubatorDiploma_W3C",
+             "digestSRI": "sha384-eIeXcHH79JdWiST9HYUoE0335npS6oIKV74bzZVqQUHWOH+v+7S+x/GLe57eCORS"
+           },
+           {
+             "id": "https://issuer.example.org/credentials/GeantIncubatorDiploma_W3C.schema.json",
+             "digestSRI": "sha384-5V21oQMDqipY/5lAXLyVIeL6+YnCC0fzWfcxPiiFpgddZFezHla6qpfWCkv12Qut"
+           }
+         ],
+         "evidence": {
+           "id": "https://issuer.example.org/credentials/GeantIncubatorDiploma_W3C/evidence",
+           "type": "Evidence",
+           "description": "Completion of the incubator programme, confirmed by the incubator board"
+         }
        },
        "credential_metadata": {
          "display": [
@@ -1872,7 +1999,38 @@ Each file as it is after this step, the changes marked:
          "type": [
            "VerifiableCredential",
            "GeantIncubatorDiploma_W3C"
-         ]
+         ],
+         "name": [
+           { "@value": "Incubator Diploma", "@language": "en" },
+           { "@value": "Inkubaattorin tutkinto", "@language": "fi" }
+         ],
+         "description": [
+           { "@value": "Diploma issued by the Trust and Identity Incubator", "@language": "en" },
+           { "@value": "Trust and Identity Incubatorin myöntämä tutkinto", "@language": "fi" }
+         ],
+         "credentialSchema": {
+           "id": "https://issuer.example.org/credentials/GeantIncubatorDiploma_W3C.schema.json",
+           "type": "JsonSchema"
+         },
+         "termsOfUse": {
+           "id": "https://issuer.example.org/credentials/GeantIncubatorDiploma_W3C/terms",
+           "type": "IncubatorTermsOfUse"
+         },
+         "relatedResource": [
+           {
+             "id": "https://issuer.example.org/credentials/GeantIncubatorDiploma_W3C",
+             "digestSRI": "sha384-eIeXcHH79JdWiST9HYUoE0335npS6oIKV74bzZVqQUHWOH+v+7S+x/GLe57eCORS"
+           },
+           {
+             "id": "https://issuer.example.org/credentials/GeantIncubatorDiploma_W3C.schema.json",
+             "digestSRI": "sha384-5V21oQMDqipY/5lAXLyVIeL6+YnCC0fzWfcxPiiFpgddZFezHla6qpfWCkv12Qut"
+           }
+         ],
+         "evidence": {
+           "id": "https://issuer.example.org/credentials/GeantIncubatorDiploma_W3C/evidence",
+           "type": "Evidence",
+           "description": "Completion of the incubator programme, confirmed by the incubator board"
+         }
        },
        "credential_metadata": {
          "display": [
@@ -2402,7 +2560,38 @@ Once the import is there every credential states the Entity Identifier, unless i
          "type": [
            "VerifiableCredential",
            "GeantIncubatorDiploma_W3C"
-         ]
+         ],
+         "name": [
+           { "@value": "Incubator Diploma", "@language": "en" },
+           { "@value": "Inkubaattorin tutkinto", "@language": "fi" }
+         ],
+         "description": [
+           { "@value": "Diploma issued by the Trust and Identity Incubator", "@language": "en" },
+           { "@value": "Trust and Identity Incubatorin myöntämä tutkinto", "@language": "fi" }
+         ],
+         "credentialSchema": {
+           "id": "https://issuer.example.org/credentials/GeantIncubatorDiploma_W3C.schema.json",
+           "type": "JsonSchema"
+         },
+         "termsOfUse": {
+           "id": "https://issuer.example.org/credentials/GeantIncubatorDiploma_W3C/terms",
+           "type": "IncubatorTermsOfUse"
+         },
+         "relatedResource": [
+           {
+             "id": "https://issuer.example.org/credentials/GeantIncubatorDiploma_W3C",
+             "digestSRI": "sha384-eIeXcHH79JdWiST9HYUoE0335npS6oIKV74bzZVqQUHWOH+v+7S+x/GLe57eCORS"
+           },
+           {
+             "id": "https://issuer.example.org/credentials/GeantIncubatorDiploma_W3C.schema.json",
+             "digestSRI": "sha384-5V21oQMDqipY/5lAXLyVIeL6+YnCC0fzWfcxPiiFpgddZFezHla6qpfWCkv12Qut"
+           }
+         ],
+         "evidence": {
+           "id": "https://issuer.example.org/credentials/GeantIncubatorDiploma_W3C/evidence",
+           "type": "Evidence",
+           "description": "Completion of the incubator programme, confirmed by the incubator board"
+         }
        },
        "credential_metadata": {
          "display": [
@@ -2713,7 +2902,7 @@ The url credential then carries:
 }
 ```
 
-A `vc+sd-jwt` credential carries the same as `termsOfUse`:
+A `vc+sd-jwt` credential carries the same as [`termsOfUse`](https://www.w3.org/TR/vc-data-model-2.0/#terms-of-use):
 
 ```json
 {
@@ -2724,6 +2913,26 @@ A `vc+sd-jwt` credential carries the same as `termsOfUse`:
 }
 ```
 
+If a `vc+sd-jwt` credential in the federation also has `termsOfUse` of its own, it carries both,
+the federation entry first:
+
+```json
+{
+  "termsOfUse": [
+    {
+      "type": "OpenIDFederation",
+      "policyId": "https://issuer.example.org"
+    },
+    {
+      "id": "https://issuer.example.org/terms",
+      "type": "IncubatorTermsOfUse"
+    }
+  ]
+}
+```
+
+The W3C credential of this story is `oidfed: false`, so it carries only its own `termsOfUse`.
+
 `display_name` and `logo_uri` of the federation entity are filled from Credential Issuer
 metadata. Set `openidvci.oidfed.decorateFederationEntity = false` to state them in the
 metadata file of the federation module instead.
@@ -2941,7 +3150,38 @@ attestation required:
 -        "type": [
 -          "VerifiableCredential",
 -          "GeantIncubatorDiploma_W3C"
--        ]
+-        ],
+-        "name": [
+-          { "@value": "Incubator Diploma", "@language": "en" },
+-          { "@value": "Inkubaattorin tutkinto", "@language": "fi" }
+-        ],
+-        "description": [
+-          { "@value": "Diploma issued by the Trust and Identity Incubator", "@language": "en" },
+-          { "@value": "Trust and Identity Incubatorin myöntämä tutkinto", "@language": "fi" }
+-        ],
+-        "credentialSchema": {
+-          "id": "https://issuer.example.org/credentials/GeantIncubatorDiploma_W3C.schema.json",
+-          "type": "JsonSchema"
+-        },
+-        "termsOfUse": {
+-          "id": "https://issuer.example.org/credentials/GeantIncubatorDiploma_W3C/terms",
+-          "type": "IncubatorTermsOfUse"
+-        },
+-        "relatedResource": [
+-          {
+-            "id": "https://issuer.example.org/credentials/GeantIncubatorDiploma_W3C",
+-            "digestSRI": "sha384-eIeXcHH79JdWiST9HYUoE0335npS6oIKV74bzZVqQUHWOH+v+7S+x/GLe57eCORS"
+-          },
+-          {
+-            "id": "https://issuer.example.org/credentials/GeantIncubatorDiploma_W3C.schema.json",
+-            "digestSRI": "sha384-5V21oQMDqipY/5lAXLyVIeL6+YnCC0fzWfcxPiiFpgddZFezHla6qpfWCkv12Qut"
+-          }
+-        ],
+-        "evidence": {
+-          "id": "https://issuer.example.org/credentials/GeantIncubatorDiploma_W3C/evidence",
+-          "type": "Evidence",
+-          "description": "Completion of the incubator programme, confirmed by the incubator board"
+-        }
 -      },
 -      "credential_metadata": {
 -        "display": [
@@ -3897,6 +4137,25 @@ Member `_issuance_configuration` of a credential configuration. It is not publis
 | `oidfed` | Boolean | `true` | Whether the credential states the Entity Identifier, when *conf/openid-vci-oidfed.xml* is imported. |
 | `claim_types` | Array | | `{"path": [...], "type": ...}` per claim of an mdoc. `full-date`, `tdate`, `bstr`, `int` or `bool`. |
 
+Members of a `vc+sd-jwt` credential, [W3C VCDM 2.0](https://www.w3.org/TR/vc-data-model-2.0/). The ones you set are members of
+`credential_definition`. Their content is purely yours, the plugin does not check it. Only
+`@context` and `type` are published, the rest goes to the credential only.
+
+| Member | Set by | In `credential_definition` | Description |
+|---|---|---|---|
+| [`@context`](https://www.w3.org/TR/vc-data-model-2.0/#contexts) | You | Required | Contexts, the W3C one first. Published. |
+| [`type`](https://www.w3.org/TR/vc-data-model-2.0/#types) | You | Required | Types, `VerifiableCredential` among them. Published. |
+| [`name`](https://www.w3.org/TR/vc-data-model-2.0/#names-and-descriptions) | You | Optional | Name of the credential. |
+| [`description`](https://www.w3.org/TR/vc-data-model-2.0/#names-and-descriptions) | You | Optional | Description of the credential. |
+| [`credentialSchema`](https://www.w3.org/TR/vc-data-model-2.0/#data-schemas) | You | Optional | Schema of the credential. |
+| [`termsOfUse`](https://www.w3.org/TR/vc-data-model-2.0/#terms-of-use) | You and the plugin | Optional | Terms of use. The plugin adds an `OpenIDFederation` entry when the credential states the Entity Identifier. When both are there, the credential has one array, the plugin's entry first. |
+| [`relatedResource`](https://www.w3.org/TR/vc-data-model-2.0/#integrity-of-related-resources) | You | Optional | Integrity information of resources the credential refers to. |
+| [`evidence`](https://www.w3.org/TR/vc-data-model-2.0/#evidence) | You | Optional | Evidence of the claims. The same for every credential of the configuration. |
+| [`id`](https://www.w3.org/TR/vc-data-model-2.0/#identifiers) | The plugin | Not read | `urn:uuid:` and a random UUID, per credential. |
+| [`issuer`](https://www.w3.org/TR/vc-data-model-2.0/#issuer) | The plugin | Not read | The issuer identifier, see [Credential Issuer identifiers](#credential-issuer-identifiers). |
+| [`validFrom`, `validUntil`](https://www.w3.org/TR/vc-data-model-2.0/#validity-period) | The plugin | Not read | The validity window, see `lifetime` and `not_before` above. |
+| [`credentialSubject`](https://www.w3.org/TR/vc-data-model-2.0/#credential-subject) | The plugin | Not read | The claims, from the IdP attribute. Selectively disclosable. |
+
 ### Credential Issuer identifiers
 
 | Value | `iss` of the credential | Key found from | Needs | Status List url ends with |
diff --git a/docs/example/step-1/metadata/verifiable-credentials.json b/docs/example/step-1/metadata/verifiable-credentials.json
index 009ed64..620908d 100644
--- a/docs/example/step-1/metadata/verifiable-credentials.json
+++ b/docs/example/step-1/metadata/verifiable-credentials.json
@@ -47,7 +47,38 @@
         "type": [
           "VerifiableCredential",
           "GeantIncubatorDiploma_W3C"
-        ]
+        ],
+        "name": [
+          { "@value": "Incubator Diploma", "@language": "en" },
+          { "@value": "Inkubaattorin tutkinto", "@language": "fi" }
+        ],
+        "description": [
+          { "@value": "Diploma issued by the Trust and Identity Incubator", "@language": "en" },
+          { "@value": "Trust and Identity Incubatorin myöntämä tutkinto", "@language": "fi" }
+        ],
+        "credentialSchema": {
+          "id": "https://issuer.example.org/credentials/GeantIncubatorDiploma_W3C.schema.json",
+          "type": "JsonSchema"
+        },
+        "termsOfUse": {
+          "id": "https://issuer.example.org/credentials/GeantIncubatorDiploma_W3C/terms",
+          "type": "IncubatorTermsOfUse"
+        },
+        "relatedResource": [
+          {
+            "id": "https://issuer.example.org/credentials/GeantIncubatorDiploma_W3C",
+            "digestSRI": "sha384-eIeXcHH79JdWiST9HYUoE0335npS6oIKV74bzZVqQUHWOH+v+7S+x/GLe57eCORS"
+          },
+          {
+            "id": "https://issuer.example.org/credentials/GeantIncubatorDiploma_W3C.schema.json",
+            "digestSRI": "sha384-5V21oQMDqipY/5lAXLyVIeL6+YnCC0fzWfcxPiiFpgddZFezHla6qpfWCkv12Qut"
+          }
+        ],
+        "evidence": {
+          "id": "https://issuer.example.org/credentials/GeantIncubatorDiploma_W3C/evidence",
+          "type": "Evidence",
+          "description": "Completion of the incubator programme, confirmed by the incubator board"
+        }
       },
       "credential_metadata": {
         "display": [
diff --git a/docs/example/step-1/webapps/root/credentials/GeantIncubatorDiploma_W3C.schema.json b/docs/example/step-1/webapps/root/credentials/GeantIncubatorDiploma_W3C.schema.json
new file mode 100644
index 0000000..e8a1863
--- /dev/null
+++ b/docs/example/step-1/webapps/root/credentials/GeantIncubatorDiploma_W3C.schema.json
@@ -0,0 +1,24 @@
+{
+  "$schema": "https://json-schema.org/draft/2020-12/schema",
+  "$id": "https://issuer.example.org/credentials/GeantIncubatorDiploma_W3C.schema.json",
+  "title": "Incubator Diploma, W3C",
+  "type": "object",
+  "required": ["@context", "type", "issuer", "credentialSubject"],
+  "properties": {
+    "type": {
+      "type": "array",
+      "contains": { "const": "GeantIncubatorDiploma_W3C" }
+    },
+    "credentialSubject": {
+      "type": "object",
+      "properties": {
+        "diploma": { "type": "string" },
+        "mail": { "type": "string" },
+        "eppn": { "type": "string" },
+        "givenName": { "type": "string" },
+        "familyName": { "type": "string" },
+        "affiliation": { "type": "string" }
+      }
+    }
+  }
+}
diff --git a/docs/example/step-2/metadata/verifiable-credentials.json b/docs/example/step-2/metadata/verifiable-credentials.json
index eae9cc8..c8dc76e 100644
--- a/docs/example/step-2/metadata/verifiable-credentials.json
+++ b/docs/example/step-2/metadata/verifiable-credentials.json
@@ -47,7 +47,38 @@
         "type": [
           "VerifiableCredential",
           "GeantIncubatorDiploma_W3C"
-        ]
+        ],
+        "name": [
+          { "@value": "Incubator Diploma", "@language": "en" },
+          { "@value": "Inkubaattorin tutkinto", "@language": "fi" }
+        ],
+        "description": [
+          { "@value": "Diploma issued by the Trust and Identity Incubator", "@language": "en" },
+          { "@value": "Trust and Identity Incubatorin myöntämä tutkinto", "@language": "fi" }
+        ],
+        "credentialSchema": {
+          "id": "https://issuer.example.org/credentials/GeantIncubatorDiploma_W3C.schema.json",
+          "type": "JsonSchema"
+        },
+        "termsOfUse": {
+          "id": "https://issuer.example.org/credentials/GeantIncubatorDiploma_W3C/terms",
+          "type": "IncubatorTermsOfUse"
+        },
+        "relatedResource": [
+          {
+            "id": "https://issuer.example.org/credentials/GeantIncubatorDiploma_W3C",
+            "digestSRI": "sha384-eIeXcHH79JdWiST9HYUoE0335npS6oIKV74bzZVqQUHWOH+v+7S+x/GLe57eCORS"
+          },
+          {
+            "id": "https://issuer.example.org/credentials/GeantIncubatorDiploma_W3C.schema.json",
+            "digestSRI": "sha384-5V21oQMDqipY/5lAXLyVIeL6+YnCC0fzWfcxPiiFpgddZFezHla6qpfWCkv12Qut"
+          }
+        ],
+        "evidence": {
+          "id": "https://issuer.example.org/credentials/GeantIncubatorDiploma_W3C/evidence",
+          "type": "Evidence",
+          "description": "Completion of the incubator programme, confirmed by the incubator board"
+        }
       },
       "credential_metadata": {
         "display": [
diff --git a/docs/example/step-2/webapps/root/credentials/GeantIncubatorDiploma_W3C.schema.json b/docs/example/step-2/webapps/root/credentials/GeantIncubatorDiploma_W3C.schema.json
new file mode 100644
index 0000000..e8a1863
--- /dev/null
+++ b/docs/example/step-2/webapps/root/credentials/GeantIncubatorDiploma_W3C.schema.json
@@ -0,0 +1,24 @@
+{
+  "$schema": "https://json-schema.org/draft/2020-12/schema",
+  "$id": "https://issuer.example.org/credentials/GeantIncubatorDiploma_W3C.schema.json",
+  "title": "Incubator Diploma, W3C",
+  "type": "object",
+  "required": ["@context", "type", "issuer", "credentialSubject"],
+  "properties": {
+    "type": {
+      "type": "array",
+      "contains": { "const": "GeantIncubatorDiploma_W3C" }
+    },
+    "credentialSubject": {
+      "type": "object",
+      "properties": {
+        "diploma": { "type": "string" },
+        "mail": { "type": "string" },
+        "eppn": { "type": "string" },
+        "givenName": { "type": "string" },
+        "familyName": { "type": "string" },
+        "affiliation": { "type": "string" }
+      }
+    }
+  }
+}
diff --git a/docs/example/step-3/metadata/verifiable-credentials.json b/docs/example/step-3/metadata/verifiable-credentials.json
index ddc2950..5c0f72d 100644
--- a/docs/example/step-3/metadata/verifiable-credentials.json
+++ b/docs/example/step-3/metadata/verifiable-credentials.json
@@ -47,7 +47,38 @@
         "type": [
           "VerifiableCredential",
           "GeantIncubatorDiploma_W3C"
-        ]
+        ],
+        "name": [
+          { "@value": "Incubator Diploma", "@language": "en" },
+          { "@value": "Inkubaattorin tutkinto", "@language": "fi" }
+        ],
+        "description": [
+          { "@value": "Diploma issued by the Trust and Identity Incubator", "@language": "en" },
+          { "@value": "Trust and Identity Incubatorin myöntämä tutkinto", "@language": "fi" }
+        ],
+        "credentialSchema": {
+          "id": "https://issuer.example.org/credentials/GeantIncubatorDiploma_W3C.schema.json",
+          "type": "JsonSchema"
+        },
+        "termsOfUse": {
+          "id": "https://issuer.example.org/credentials/GeantIncubatorDiploma_W3C/terms",
+          "type": "IncubatorTermsOfUse"
+        },
+        "relatedResource": [
+          {
+            "id": "https://issuer.example.org/credentials/GeantIncubatorDiploma_W3C",
+            "digestSRI": "sha384-eIeXcHH79JdWiST9HYUoE0335npS6oIKV74bzZVqQUHWOH+v+7S+x/GLe57eCORS"
+          },
+          {
+            "id": "https://issuer.example.org/credentials/GeantIncubatorDiploma_W3C.schema.json",
+            "digestSRI": "sha384-5V21oQMDqipY/5lAXLyVIeL6+YnCC0fzWfcxPiiFpgddZFezHla6qpfWCkv12Qut"
+          }
+        ],
+        "evidence": {
+          "id": "https://issuer.example.org/credentials/GeantIncubatorDiploma_W3C/evidence",
+          "type": "Evidence",
+          "description": "Completion of the incubator programme, confirmed by the incubator board"
+        }
       },
       "credential_metadata": {
         "display": [
diff --git a/docs/example/step-3/webapps/root/credentials/GeantIncubatorDiploma_W3C.schema.json b/docs/example/step-3/webapps/root/credentials/GeantIncubatorDiploma_W3C.schema.json
new file mode 100644
index 0000000..e8a1863
--- /dev/null
+++ b/docs/example/step-3/webapps/root/credentials/GeantIncubatorDiploma_W3C.schema.json
@@ -0,0 +1,24 @@
+{
+  "$schema": "https://json-schema.org/draft/2020-12/schema",
+  "$id": "https://issuer.example.org/credentials/GeantIncubatorDiploma_W3C.schema.json",
+  "title": "Incubator Diploma, W3C",
+  "type": "object",
+  "required": ["@context", "type", "issuer", "credentialSubject"],
+  "properties": {
+    "type": {
+      "type": "array",
+      "contains": { "const": "GeantIncubatorDiploma_W3C" }
+    },
+    "credentialSubject": {
+      "type": "object",
+      "properties": {
+        "diploma": { "type": "string" },
+        "mail": { "type": "string" },
+        "eppn": { "type": "string" },
+        "givenName": { "type": "string" },
+        "familyName": { "type": "string" },
+        "affiliation": { "type": "string" }
+      }
+    }
+  }
+}
diff --git a/docs/example/step-4/metadata/verifiable-credentials.json b/docs/example/step-4/metadata/verifiable-credentials.json
index 3811ba8..16482e0 100644
--- a/docs/example/step-4/metadata/verifiable-credentials.json
+++ b/docs/example/step-4/metadata/verifiable-credentials.json
@@ -53,7 +53,38 @@
         "type": [
           "VerifiableCredential",
           "GeantIncubatorDiploma_W3C"
-        ]
+        ],
+        "name": [
+          { "@value": "Incubator Diploma", "@language": "en" },
+          { "@value": "Inkubaattorin tutkinto", "@language": "fi" }
+        ],
+        "description": [
+          { "@value": "Diploma issued by the Trust and Identity Incubator", "@language": "en" },
+          { "@value": "Trust and Identity Incubatorin myöntämä tutkinto", "@language": "fi" }
+        ],
+        "credentialSchema": {
+          "id": "https://issuer.example.org/credentials/GeantIncubatorDiploma_W3C.schema.json",
+          "type": "JsonSchema"
+        },
+        "termsOfUse": {
+          "id": "https://issuer.example.org/credentials/GeantIncubatorDiploma_W3C/terms",
+          "type": "IncubatorTermsOfUse"
+        },
+        "relatedResource": [
+          {
+            "id": "https://issuer.example.org/credentials/GeantIncubatorDiploma_W3C",
+            "digestSRI": "sha384-eIeXcHH79JdWiST9HYUoE0335npS6oIKV74bzZVqQUHWOH+v+7S+x/GLe57eCORS"
+          },
+          {
+            "id": "https://issuer.example.org/credentials/GeantIncubatorDiploma_W3C.schema.json",
+            "digestSRI": "sha384-5V21oQMDqipY/5lAXLyVIeL6+YnCC0fzWfcxPiiFpgddZFezHla6qpfWCkv12Qut"
+          }
+        ],
+        "evidence": {
+          "id": "https://issuer.example.org/credentials/GeantIncubatorDiploma_W3C/evidence",
+          "type": "Evidence",
+          "description": "Completion of the incubator programme, confirmed by the incubator board"
+        }
       },
       "credential_metadata": {
         "display": [
diff --git a/docs/example/step-4/webapps/root/credentials/GeantIncubatorDiploma_W3C.schema.json b/docs/example/step-4/webapps/root/credentials/GeantIncubatorDiploma_W3C.schema.json
new file mode 100644
index 0000000..e8a1863
--- /dev/null
+++ b/docs/example/step-4/webapps/root/credentials/GeantIncubatorDiploma_W3C.schema.json
@@ -0,0 +1,24 @@
+{
+  "$schema": "https://json-schema.org/draft/2020-12/schema",
+  "$id": "https://issuer.example.org/credentials/GeantIncubatorDiploma_W3C.schema.json",
+  "title": "Incubator Diploma, W3C",
+  "type": "object",
+  "required": ["@context", "type", "issuer", "credentialSubject"],
+  "properties": {
+    "type": {
+      "type": "array",
+      "contains": { "const": "GeantIncubatorDiploma_W3C" }
+    },
+    "credentialSubject": {
+      "type": "object",
+      "properties": {
+        "diploma": { "type": "string" },
+        "mail": { "type": "string" },
+        "eppn": { "type": "string" },
+        "givenName": { "type": "string" },
+        "familyName": { "type": "string" },
+        "affiliation": { "type": "string" }
+      }
+    }
+  }
+}
diff --git a/download/shibboleth-idp-plugin-openid-vci-0.9.0-SNAPSHOT.tar.gz b/download/shibboleth-idp-plugin-openid-vci-0.9.0-SNAPSHOT.tar.gz
index e5f20b3..0708877 100644
Binary files a/download/shibboleth-idp-plugin-openid-vci-0.9.0-SNAPSHOT.tar.gz and b/download/shibboleth-idp-plugin-openid-vci-0.9.0-SNAPSHOT.tar.gz differ
diff --git a/download/shibboleth-idp-plugin-openid-vci-0.9.0-SNAPSHOT.tar.gz.asc b/download/shibboleth-idp-plugin-openid-vci-0.9.0-SNAPSHOT.tar.gz.asc
index 101e7f3..b96fe84 100644
--- a/download/shibboleth-idp-plugin-openid-vci-0.9.0-SNAPSHOT.tar.gz.asc
+++ b/download/shibboleth-idp-plugin-openid-vci-0.9.0-SNAPSHOT.tar.gz.asc
@@ -1,14 +1,14 @@
 -----BEGIN PGP SIGNATURE-----
 
-iQHPBAABCAA5FiEEoCV/y8Vr4Y+djZGw+mRLGOJQ2wsFAmrFBPcbFIAAAAAABAAO
-bWFudTIsMi41KzEuMTIsMCwzAAoJEPpkSxjiUNsL7FoMALM74CYF5+4Wobd40Lhi
-oVgcz03K09+6GWFfFRb1aXXH3kYCNdGhtwXTtGOnhRVNkiMC8QEoaD6pHY0d4QZo
-urho9Wv5LbpYP4gHfwmO/zdkvG/kZSmf8CZItB051WGcpddB3y2SmCkHdemXKyeC
-UFyU+BlKnJ1AO4ct8p+U7uaIJNa1t8En0/yM4Z3yau1XZ6IY1dG0dAkuNnPN2e3Q
-CEZEg4aHU3a0G3FvmQxAH0c4l7yfOkr1btNO1ihUVV/pJ0vTT4uyTsg4c9goLQv3
-tmSM7n9apbBZD2R4B7GGgRy+mTcAWuXL3klPzEnuDgGOIe79MGedfGgAolppdFG3
-y/ZL5uyEo++GrDxjEP+C7UxtdMHxJG04QYnvX+0BEA72/EbYTEd8LXQOyLvdmIk0
-RuIHUL4hcwXNoT87cpy1YiUwZ15WI2zrAwIjH2ZiinVWZxR7cQr3OE7PrxvNIZPb
-nnNNgOTphIb16n3aWTq3II/V9z8m05fC9TDxwgLkrjqr0A==
-=4Hc9
+iQHPBAABCAA5FiEEoCV/y8Vr4Y+djZGw+mRLGOJQ2wsFAmrI81MbFIAAAAAABAAO
+bWFudTIsMi41KzEuMTIsMCwzAAoJEPpkSxjiUNsLrvYMAMOGNopv23P+QO/fOGpX
+F9OsUGCkNgz+5rT+ErWcPIH52IFXD55VA/Lcjf+D5nru2YlhMc9Kg41YH7/T7Lys
+I18o6lVrFwd6oeHynHrDKGHoSNUkk5X9Sk4c+U6QStZ+DglNP6YTzSA+IfCE+gUj
+XMAMaNJH0hUi+rtP4OPgGH89aGsMU1MPl/O3z8IYdiTDHUk6s/8SqG6ok4I0AJFT
+q42Xj7tavRN5Gd/pQGoc7TsCbmM5KePOsWaoAdO/pIK5Z8pi8TuN+GKz5bBdj4MJ
+P7T2MtL9Pn+Lp0JhhVV4arRisJd5DMUNee3wZf3Zz7SneHd0GH026vgA6RxqUr5w
+TagXmGp/+cPY/0v0Fqv1Gq6c5ZCAy50Is2A6XOgHJq3BhXaWkEpvaAoVgP/pS+RJ
+hlO5Ffz9fwm0kcqzLLlhgfpsQQvIqefSt/d7KNDKIPfwVRVvPoMt8+SyroKvmqii
+lFVafzLJYIi5m9Hwbvp7vGeOGTF9bTrJZUybe2mhyyiblQ==
+=NDZf
 -----END PGP SIGNATURE-----
diff --git a/openid-vci-api/src/main/java/org/geant/shibboleth/plugin/openidvci/credential/CredentialConfigurations.java b/openid-vci-api/src/main/java/org/geant/shibboleth/plugin/openidvci/credential/CredentialConfigurations.java
index 8955d03..0e39871 100644
--- a/openid-vci-api/src/main/java/org/geant/shibboleth/plugin/openidvci/credential/CredentialConfigurations.java
+++ b/openid-vci-api/src/main/java/org/geant/shibboleth/plugin/openidvci/credential/CredentialConfigurations.java
@@ -24,6 +24,7 @@ import java.util.LinkedHashMap;
 import java.util.Map;
 
 import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
 
 import org.geant.shibboleth.plugin.openidvci.metadata.CredentialIssuerMetadata;
 import org.slf4j.Logger;
@@ -48,10 +49,15 @@ import com.fasterxml.jackson.databind.ObjectMapper;
  *
  * The '{@value CredentialConfiguration#ISSUANCE_CONFIGURATION}' member is the
  * exception. It states what a deployer decided about a credential, so it is
- * parsed and then dropped from the source that gets published.
+ * parsed and then dropped from the source that gets published. So are the
+ * {@link CredentialDefinition#CREDENTIAL_MEMBERS} of a credential definition.
  */
 public final class CredentialConfigurations {
 
+    /** Name of the credential definition member. */
+    @Nonnull
+    private static final String CREDENTIAL_DEFINITION = "credential_definition";
+
     /** Class logger. */
     @Nonnull
     private static Logger log = LoggerFactory.getLogger(CredentialConfigurations.class);
@@ -170,9 +176,15 @@ public final class CredentialConfigurations {
         for (final Map.Entry<String, Object> entry : source.entrySet()) {
             final Object configuration = entry.getValue();
             if (configuration instanceof Map<?, ?> members
-                    && members.containsKey(CredentialConfiguration.ISSUANCE_CONFIGURATION)) {
+                    && (members.containsKey(CredentialConfiguration.ISSUANCE_CONFIGURATION)
+                            || hasCredentialMembers(members.get(CREDENTIAL_DEFINITION)))) {
                 final Map<Object, Object> copy = new LinkedHashMap<>(members);
                 copy.remove(CredentialConfiguration.ISSUANCE_CONFIGURATION);
+                if (copy.get(CREDENTIAL_DEFINITION) instanceof Map<?, ?> definition) {
+                    final Map<Object, Object> definitionCopy = new LinkedHashMap<>(definition);
+                    CredentialDefinition.CREDENTIAL_MEMBERS.forEach(definitionCopy::remove);
+                    copy.put(CREDENTIAL_DEFINITION, Collections.unmodifiableMap(definitionCopy));
+                }
                 result.put(entry.getKey(), Collections.unmodifiableMap(copy));
             } else {
                 result.put(entry.getKey(), configuration);
@@ -181,6 +193,17 @@ public final class CredentialConfigurations {
         return Collections.unmodifiableMap(result);
     }
 
+    /**
+     * Whether a credential definition has members copied into the credential.
+     *
+     * @param definition credential definition as written
+     * @return whether it has any of {@link CredentialDefinition#CREDENTIAL_MEMBERS}
+     */
+    private static boolean hasCredentialMembers(@Nullable final Object definition) {
+        return definition instanceof Map<?, ?> members
+                && CredentialDefinition.CREDENTIAL_MEMBERS.stream().anyMatch(members::containsKey);
+    }
+
     /**
      * Warn of Credential Configurations whose validity window is fixed on the
      * calendar and has closed, or closes soon.
diff --git a/openid-vci-api/src/main/java/org/geant/shibboleth/plugin/openidvci/credential/CredentialDefinition.java b/openid-vci-api/src/main/java/org/geant/shibboleth/plugin/openidvci/credential/CredentialDefinition.java
index 3e4f9ec..ee210ec 100644
--- a/openid-vci-api/src/main/java/org/geant/shibboleth/plugin/openidvci/credential/CredentialDefinition.java
+++ b/openid-vci-api/src/main/java/org/geant/shibboleth/plugin/openidvci/credential/CredentialDefinition.java
@@ -16,12 +16,16 @@
 
 package org.geant.shibboleth.plugin.openidvci.credential;
 
+import java.util.Collections;
+import java.util.LinkedHashMap;
 import java.util.List;
+import java.util.Map;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
 import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnore;
 import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
 import com.fasterxml.jackson.annotation.JsonProperty;
 import com.fasterxml.jackson.core.JsonProcessingException;
@@ -39,6 +43,11 @@ import com.fasterxml.jackson.databind.ObjectMapper;
 @JsonIgnoreProperties(ignoreUnknown = true)
 public final class CredentialDefinition {
 
+    /** Names of the members copied into the credential and not published. */
+    @Nonnull
+    public static final List<String> CREDENTIAL_MEMBERS = List.of("name", "description", "credentialSchema",
+            "termsOfUse", "relatedResource", "evidence");
+
     /** Context of credential definition. */
     @Nonnull
     @JsonProperty("@context")
@@ -49,18 +58,26 @@ public final class CredentialDefinition {
     @JsonProperty("type")
     private final List<String> type;
 
+    /** Credential members copied into the credential and not published. */
+    @Nonnull
+    @JsonIgnore
+    private final Map<String, Object> credentialMembers;
+
     /**
      * Constructor.
      *
-     * @param context Context of credential definition
-     * @param type    Type of credential definition
+     * @param context           Context of credential definition
+     * @param type              Type of credential definition
+     * @param credentialMembers Credential members copied into the credential
      */
-    private CredentialDefinition(@JsonProperty("@context") @Nonnull final List<String> context,
-            @JsonProperty("type") @Nonnull final List<String> type) {
+    private CredentialDefinition(@Nonnull final List<String> context, @Nonnull final List<String> type,
+            @Nonnull final Map<String, Object> credentialMembers) {
         assert context != null;
         assert type != null;
+        assert credentialMembers != null;
         this.context = context;
         this.type = type;
+        this.credentialMembers = Collections.unmodifiableMap(credentialMembers);
     }
 
     /**
@@ -83,6 +100,16 @@ public final class CredentialDefinition {
         return type;
     }
 
+    /**
+     * Get credential members copied into the credential.
+     * 
+     * @return credential members by name
+     */
+    @Nonnull
+    public Map<String, Object> getCredentialMembers() {
+        return credentialMembers;
+    }
+
     /**
      * Serialize object to json.
      * 
@@ -108,17 +135,50 @@ public final class CredentialDefinition {
     /**
      * Create a Credential Definition, or null when the mandatory members are absent.
      *
-     * @param context  value of the @context member
-     * @param id       value of the id member
-     * @param type     value of the type member
+     * @param context          value of the @context member
+     * @param id               value of the id member
+     * @param type             value of the type member
+     * @param name             value of the name member
+     * @param description      value of the description member
+     * @param credentialSchema value of the credentialSchema member
+     * @param termsOfUse       value of the termsOfUse member
+     * @param relatedResource  value of the relatedResource member
+     * @param evidence         value of the evidence member
      * @return Credential Definition, or null
      */
     @JsonCreator
     public static CredentialDefinition of(@JsonProperty("@context") @Nullable final List<String> context,
-            @JsonProperty("id") @Nullable final String id, @JsonProperty("type") @Nullable final List<String> type) {
+            @JsonProperty("id") @Nullable final String id, @JsonProperty("type") @Nullable final List<String> type,
+            @JsonProperty("name") @Nullable final Object name,
+            @JsonProperty("description") @Nullable final Object description,
+            @JsonProperty("credentialSchema") @Nullable final Object credentialSchema,
+            @JsonProperty("termsOfUse") @Nullable final Object termsOfUse,
+            @JsonProperty("relatedResource") @Nullable final Object relatedResource,
+            @JsonProperty("evidence") @Nullable final Object evidence) {
         if (context == null || type == null) {
             return null;
         }
-        return new CredentialDefinition(context, type);
+        final Map<String, Object> members = new LinkedHashMap<>();
+        putIfPresent(members, "name", name);
+        putIfPresent(members, "description", description);
+        putIfPresent(members, "credentialSchema", credentialSchema);
+        putIfPresent(members, "termsOfUse", termsOfUse);
+        putIfPresent(members, "relatedResource", relatedResource);
+        putIfPresent(members, "evidence", evidence);
+        return new CredentialDefinition(context, type, members);
+    }
+
+    /**
+     * Put value to map if not null.
+     *
+     * @param members map to put to
+     * @param name    member name
+     * @param value   member value
+     */
+    private static void putIfPresent(@Nonnull final Map<String, Object> members, @Nonnull final String name,
+            @Nullable final Object value) {
+        if (value != null) {
+            members.put(name, value);
+        }
     }
 }
diff --git a/openid-vci-api/src/test/java/org/geant/shibboleth/plugin/openidvci/credential/CredentialConfigurationsTest.java b/openid-vci-api/src/test/java/org/geant/shibboleth/plugin/openidvci/credential/CredentialConfigurationsTest.java
index a7cc88b..359cfb7 100644
--- a/openid-vci-api/src/test/java/org/geant/shibboleth/plugin/openidvci/credential/CredentialConfigurationsTest.java
+++ b/openid-vci-api/src/test/java/org/geant/shibboleth/plugin/openidvci/credential/CredentialConfigurationsTest.java
@@ -19,6 +19,7 @@ package org.geant.shibboleth.plugin.openidvci.credential;
 import java.io.IOException;
 import java.nio.charset.StandardCharsets;
 import java.time.Duration;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
@@ -105,6 +106,32 @@ public class CredentialConfigurationsTest {
         Assert.assertEquals(card.keySet().size(), 2);
     }
 
+    @Test
+    public void parseCredentialMembers() throws IOException {
+
+        final CredentialConfigurations configurations = CredentialConfigurations
+                .parse(("{\"credential_configurations_supported\":{"
+                        + "\"Diploma\":{\"format\":\"vc+sd-jwt\",\"credential_definition\":{"
+                        + "\"@context\":[\"https://www.w3.org/ns/credentials/v2\"],"
+                        + "\"type\":[\"VerifiableCredential\",\"Diploma\"],"
+                        + "\"name\":\"Diploma\",\"description\":{\"en\":\"A diploma\"},"
+                        + "\"credentialSchema\":{\"id\":\"https://example.org/schema\",\"type\":\"JsonSchema\"},"
+                        + "\"termsOfUse\":{\"type\":\"ExampleTermsOfUse\"},"
+                        + "\"relatedResource\":[{\"id\":\"https://example.org/schema\"}],"
+                        + "\"evidence\":[{\"type\":\"Evidence\"}]}}}}")
+                        .getBytes(StandardCharsets.UTF_8));
+
+        Assert.assertEquals(List.copyOf(configurations.getCredentialConfigurations().get("Diploma")
+                .getCredentialDefinition().getCredentialMembers().keySet()), CredentialDefinition.CREDENTIAL_MEMBERS);
+
+        @SuppressWarnings("unchecked")
+        final Map<String, Object> published = (Map<String, Object>) configurations.getSupported().get("Diploma");
+        Assert.assertEquals(published.get("format"), "vc+sd-jwt");
+        @SuppressWarnings("unchecked")
+        final Map<String, Object> definition = (Map<String, Object>) published.get("credential_definition");
+        Assert.assertEquals(definition.keySet(), Set.of("@context", "type"));
+    }
+
     @Test
     public void parseUnparsableCredentialConfiguration() {
         Assert.assertThrows(IOException.class,
diff --git a/openid-vci-api/src/test/java/org/geant/shibboleth/plugin/openidvci/credential/CredentialDefinitionTest.java b/openid-vci-api/src/test/java/org/geant/shibboleth/plugin/openidvci/credential/CredentialDefinitionTest.java
index e55aecd..12f5100 100644
--- a/openid-vci-api/src/test/java/org/geant/shibboleth/plugin/openidvci/credential/CredentialDefinitionTest.java
+++ b/openid-vci-api/src/test/java/org/geant/shibboleth/plugin/openidvci/credential/CredentialDefinitionTest.java
@@ -16,6 +16,9 @@
 
 package org.geant.shibboleth.plugin.openidvci.credential;
 
+import java.util.List;
+import java.util.Map;
+
 import org.springframework.core.io.ClassPathResource;
 import org.testng.Assert;
 import org.testng.annotations.BeforeMethod;
@@ -33,10 +36,15 @@ public class CredentialDefinitionTest {
 
     private String definition;
 
+    private String definitionWithMembers;
+
     @BeforeMethod
     protected void setUp() throws Exception {
         definition = new String(ResourceHelper.of(new ClassPathResource("credentials/CredentialDefinition.json"))
                 .getInputStream().readAllBytes());
+        definitionWithMembers = new String(
+                ResourceHelper.of(new ClassPathResource("credentials/CredentialDefinitionWithMembers.json"))
+                        .getInputStream().readAllBytes());
     }
 
     @Test
@@ -50,7 +58,22 @@ public class CredentialDefinitionTest {
         Assert.assertEquals(credentialDefinition.getType().get(1), "UniversityDegreeCredential");
         Assert.assertEquals(credentialDefinition.serialize(),
                 "{\"@context\":[\"https://www.w3.org/2018/credentials/v1\",\"https://www.w3.org/2018/credentials/examples/v1\"],\"type\":[\"VerifiableCredential\",\"UniversityDegreeCredential\"]}");
+        Assert.assertTrue(credentialDefinition.getCredentialMembers().isEmpty());
+    }
 
+    @Test
+    public void parseStringWithMembers() throws ParseException, JsonProcessingException {
+
+        CredentialDefinition credentialDefinition = CredentialDefinition.parse(definitionWithMembers);
+        Assert.assertEquals(List.copyOf(credentialDefinition.getCredentialMembers().keySet()), List.of("name", "description",
+                "credentialSchema", "termsOfUse", "relatedResource", "evidence"));
+        Assert.assertEquals(credentialDefinition.getCredentialMembers().get("name"), "Example Credential");
+        Assert.assertEquals(((Map<?, ?>) credentialDefinition.getCredentialMembers().get("description")).get("en"),
+                "An example");
+        Assert.assertEquals(((Map<?, ?>) ((List<?>) credentialDefinition.getCredentialMembers()
+                .get("credentialSchema")).get(0)).get("type"), "JsonSchema");
+        Assert.assertEquals(credentialDefinition.serialize(),
+                "{\"@context\":[\"https://www.w3.org/ns/credentials/v2\"],\"type\":[\"VerifiableCredential\",\"ExampleCredential\"]}");
     }
 
 }
\ No newline at end of file
diff --git a/openid-vci-api/src/test/resources/credentials/CredentialDefinitionWithMembers.json b/openid-vci-api/src/test/resources/credentials/CredentialDefinitionWithMembers.json
new file mode 100644
index 0000000..323863c
--- /dev/null
+++ b/openid-vci-api/src/test/resources/credentials/CredentialDefinitionWithMembers.json
@@ -0,0 +1,35 @@
+{
+	"@context": [
+		"https://www.w3.org/ns/credentials/v2"
+	],
+	"type": [
+		"VerifiableCredential",
+		"ExampleCredential"
+	],
+	"name": "Example Credential",
+	"description": {
+		"en": "An example"
+	},
+	"credentialSchema": [
+		{
+			"id": "https://example.com/schemas/example.json",
+			"type": "JsonSchema"
+		}
+	],
+	"termsOfUse": {
+		"type": "ExampleTermsOfUse",
+		"id": "https://example.com/policies/1"
+	},
+	"relatedResource": [
+		{
+			"id": "https://example.com/schemas/example.json",
+			"digestSRI": "sha384-S57yQDg1MTzF56Oi9DbSQ14u7jBy0RDdx0YbeV7shwhCS88G8SCXeFq82PafhCrW"
+		}
+	],
+	"evidence": [
+		{
+			"type": "Evidence",
+			"description": "Identity verified in person"
+		}
+	]
+}
diff --git a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/FormJsonLdSelectiveDisclosureJWTCredential.java b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/FormJsonLdSelectiveDisclosureJWTCredential.java
index 9e64f19..1c24046 100644
--- a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/FormJsonLdSelectiveDisclosureJWTCredential.java
+++ b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/FormJsonLdSelectiveDisclosureJWTCredential.java
@@ -22,7 +22,9 @@ import java.util.UUID;
 import java.util.function.Function;
 
 import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
 
+import org.geant.shibboleth.plugin.openidvci.credential.CredentialDefinition;
 import org.geant.shibboleth.plugin.openidvci.profile.logic.CredentialIssuerLookupFunction;
 import org.geant.shibboleth.plugin.openidvci.messaging.context.CredentialsContext;
 import org.geant.shibboleth.plugin.openidvci.messaging.impl.CredentialOfferRequestedCredential;
@@ -144,11 +146,14 @@ public class FormJsonLdSelectiveDisclosureJWTCredential extends AbstractProfileA
             }
             try {
                 // Claims of this format are inside 'credentialSubject'.
-                credentials.add(new JWTClaimsSet.Builder(cred.toJWTClaimsSet())
-                        .claim("@context", ctx.getCredentialConfiguration().getCredentialDefinition().getContext())
+                final CredentialDefinition definition = ctx.getCredentialConfiguration().getCredentialDefinition();
+                final JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder(cred.toJWTClaimsSet())
+                        .claim("@context", definition.getContext())
                         .claim("id", "urn:uuid:" + UUID.randomUUID())
-                        .claim("type", ctx.getCredentialConfiguration().getCredentialDefinition().getType())
-                        .claim("issuer", issuer).claim("credentialSubject", tree.getClaims())
+                        .claim("type", definition.getType());
+                definition.getCredentialMembers()
+                        .forEach((name, value) -> builder.claim(name, combine(cred.getClaim(name), value)));
+                credentials.add(builder.claim("issuer", issuer).claim("credentialSubject", tree.getClaims())
                         .claim("_sd_alg", tree.getAlgorithm()).build());
                 disclosures.add(tree.getFormattedDisclosures());
             } catch (final ParseException e) {
@@ -160,4 +165,36 @@ public class FormJsonLdSelectiveDisclosureJWTCredential extends AbstractProfileA
         ctx.setJWTCredentials(credentials);
         ctx.setDisclosures(disclosures);
     }
+
+    /**
+     * Combine a member set by the plugin with the configured one.
+     *
+     * @param added      value set by the plugin, or null
+     * @param configured configured value
+     * @return configured value alone, or both as one array
+     */
+    @Nonnull
+    private static Object combine(@Nullable final Object added, @Nonnull final Object configured) {
+        if (added == null) {
+            return configured;
+        }
+        final List<Object> combined = new ArrayList<>();
+        appendTo(combined, added);
+        appendTo(combined, configured);
+        return combined;
+    }
+
+    /**
+     * Append a value, or each of its entries if it is an array.
+     *
+     * @param list  list to append to
+     * @param value value to append
+     */
+    private static void appendTo(@Nonnull final List<Object> list, @Nonnull final Object value) {
+        if (value instanceof List<?> entries) {
+            list.addAll(entries);
+        } else {
+            list.add(value);
+        }
+    }
 }
\ No newline at end of file

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


More information about the commits mailing list