[java-idp-plugin-vci] 01/01: Copy VCDM members of credential_definition into vc+sd-jwt credentials
Codeberg
noreply at shibboleth.net
Fri Oct 9 13:37:22 UTC 2026
This is an automated email from the git hooks/post-receive script.
codeberg pushed a commit to branch VCDMImprovements
in repository java-idp-plugin-vci.
View the commit online:
https://codeberg.org/Shibboleth/java-idp-plugin-vci/commit/97f304b133eb9d26371654354ea27aaf45a8be99
commit 97f304b133eb9d26371654354ea27aaf45a8be99
Author: Janne Lauros <janne.lauros at csc.fi>
AuthorDate: Fri Oct 9 16:36:29 2026 +0300
Copy VCDM members of credential_definition into vc+sd-jwt credentials
---
README.md | 284 ++++++++++++++++++++-
.../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 ++
.../credential/CredentialConfigurations.java | 27 +-
.../openidvci/credential/CredentialDefinition.java | 78 +++++-
.../credential/CredentialConfigurationsTest.java | 27 ++
.../credential/CredentialDefinitionTest.java | 23 ++
.../CredentialDefinitionWithMembers.json | 35 +++
...FormJsonLdSelectiveDisclosureJWTCredential.java | 45 +++-
15 files changed, 717 insertions(+), 30 deletions(-)
diff --git a/README.md b/README.md
index 1634c7d..ffffad3 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": "IssuerPolicy"
+ },
+ "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,9 @@ 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 [`@context`](https://www.w3.org/TR/vc-data-model-2.0/#contexts) of a `vc+sd-jwt` credential
+has to resolve. The first entry is the W3C one and the second one is yours, you publish it
+yourself as a static file. The terms of your claims are in it:
<details>
<summary>webapps/root/credentials/GeantIncubatorDiploma_W3C.json</summary>
@@ -398,6 +429,74 @@ Full file: [webapps/root/credentials/GeantIncubatorDiploma_W3C.json](docs/exampl
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.
+`@context` and [`type`](https://www.w3.org/TR/vc-data-model-2.0/#types) are required, they are published and added to the
+credential. The other members of `credential_definition` below are optional, use any of them or
+none. They are added to the credential only, not published, because OpenID4VCI does not define
+them there. Their content is purely yours: the plugin does not check them and copies them to
+every credential as they are, not selectively disclosable.
+
+| 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, the same way |
+| [`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) | An issuer policy |
+| [`relatedResource`](https://www.w3.org/TR/vc-data-model-2.0/#integrity-of-related-resources) | Digests of the context and the schema, so that a verifier sees if they change |
+| [`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 describes the credential as a verifier has it after the disclosures. Nothing in
+`credentialSubject` is required, the holder may leave any of it out:
+
+<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>
+
+Jetty serves it as it is, the url has the file name. The `digestSRI` values are
+[Subresource Integrity](https://www.w3.org/TR/SRI/) digests of the two files exactly as
+published. Change either file and its digest changes, compute it again:
+
+```bash
+echo "sha384-$(openssl dgst -sha384 -binary GeantIncubatorDiploma_W3C.json | openssl base64 -A)"
+```
+
#### Settings of your own, per credential
Member `_issuance_configuration` is not published. It states how long a credential is valid,
@@ -452,7 +551,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 +1478,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": "IssuerPolicy"
+ },
+ "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 +2002,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": "IssuerPolicy"
+ },
+ "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 +2563,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": "IssuerPolicy"
+ },
+ "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 +2905,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 +2916,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": "IssuerPolicy"
+ }
+ ]
+}
+```
+
+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 +3153,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": "IssuerPolicy"
+- },
+- "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 +4140,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..de9f4c9 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": "IssuerPolicy"
+ },
+ "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..75c2b46 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": "IssuerPolicy"
+ },
+ "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..d230f72 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": "IssuerPolicy"
+ },
+ "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..2d49fe2 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": "IssuerPolicy"
+ },
+ "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/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..46224f6 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\":\"IssuerPolicy\"},"
+ + "\"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..8457e28
--- /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": "IssuerPolicy",
+ "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