[java-idp-plugin-vci] branch main updated: Support the attestation proof type, EUDI Reference Wallet as HAIP example
Codeberg
noreply at shibboleth.net
Tue Oct 6 14:25:48 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/a45c82270ce05149d8e543af062c86c4bc672bb4
The following commit(s) were added to refs/heads/main by this push:
new a45c822 Support the attestation proof type, EUDI Reference Wallet as HAIP example
a45c822 is described below
commit a45c82270ce05149d8e543af062c86c4bc672bb4
Author: Janne Lauros <janne.lauros at csc.fi>
AuthorDate: Tue Oct 6 17:25:34 2026 +0300
Support the attestation proof type, EUDI Reference Wallet as HAIP example
---
README.md | 81 +++++--
docs/example/step-5/conf/attribute-filter.xml | 2 +-
docs/example/step-5/conf/global.xml | 29 +++
docs/example/step-5/conf/relying-party.xml | 2 +-
.../step-5/credentials/eudiw-pid-issuer-ca02.crt | 18 ++
docs/example/step-5/metadata/oidc-client.json | 6 +-
.../step-5/metadata/verifiable-credentials.json | 14 ++
.../messaging/context/CredentialsContext.java | 24 ++
.../openidvci/profile/impl/AddCredentialShell.java | 10 +-
.../profile/impl/AddMobileSecurityObjectShell.java | 22 +-
.../plugin/openidvci/profile/impl/ParseProof.java | 77 ++++++-
.../logic/CredentialCountLookupFunction.java | 5 +-
.../profile/impl/ParseAttestationProofTest.java | 253 +++++++++++++++++++++
13 files changed, 509 insertions(+), 34 deletions(-)
diff --git a/README.md b/README.md
index 26d9204..efb6a84 100644
--- a/README.md
+++ b/README.md
@@ -138,7 +138,7 @@ Optional currently not supported features.
| Credential Offer, same device and cross device | Yes |
| Wallet Attestation at PAR and token endpoint | Yes |
| Key attestation in a `jwt` proof, `key_attestation` | Yes |
-| Proof type `attestation` | No |
+| Proof type `attestation` | Yes |
| Batch issuance advertised by `batch_credential_issuance` | Yes |
| Refresh tokens | No |
| SD-JWT VC with `exp` and `status` | Yes |
@@ -257,7 +257,8 @@ of the OIDC OP wiki page. A normal installation of the OP plugin has one,
The wallet of steps 1 and 2 is [Paradym](https://paradym.id). Paradym accepts only did and x5c
issuers, so the url credential of steps 3 and 4 goes to the TOPPAN wallet. The HAIP credentials of
-Step 5 are tested with the OpenID conformance suite. At the end the installation
+Step 5 go to the Hopae EUDI Wallet, and they are also tested with the OpenID conformance suite.
+At the end the installation
releases did:jwk, did:web, plain url, OpenID Federation and HAIP credentials.
`https://issuer.example.org` is your IdP's OIDC issuer everywhere below.
@@ -2733,6 +2734,11 @@ Last two credentials are for a HAIP wallet. HAIP takes a credential as SD-JWT VC
see [HAIP 1.0](https://openid.net/specs/openid4vc-high-assurance-interoperability-profile-1_0-final.html#vc-profiles).
This step adds one of each.
+The wallet of this step is [HopaeEUDIWallet](https://apps.apple.com/app/id6759562486) on iPhone,
+Hopae's build of the [EUDI Reference Wallet](https://github.com/eu-digital-identity-wallet), source
+in [hopae-eudi-ref-wallet](https://github.com/hopae-official/hopae-eudi-ref-wallet). The wallet
+needs no configuration for your issuer, it takes the Credential Offer QR code as it is.
+
What you configure:
- a signing key with a certificate, used first
@@ -2740,7 +2746,7 @@ What you configure:
- a url credential
- an mdoc credential
- key attestation required for the credentials, and the attester CA you trust
-- the wallet as a client with `attest_jwt_client_auth`
+- the Hopae EUDI Wallet as a client with `attest_jwt_client_auth`
- signed Credential Issuer metadata
#### Certificate chain
@@ -2983,6 +2989,13 @@ attestation required:
+ "key_storage": ["iso_18045_high"],
+ "user_authentication": ["iso_18045_high"]
+ }
++ },
++ "attestation": {
++ "proof_signing_alg_values_supported": ["ES256", "ES384", "ES512"],
++ "key_attestations_required": {
++ "key_storage": ["iso_18045_high"],
++ "user_authentication": ["iso_18045_high"]
++ }
+ }
+ },
+ "_issuance_configuration": {
@@ -3014,6 +3027,13 @@ Full file: [metadata/verifiable-credentials.json](docs/example/step-5/metadata/v
For the values of `key_attestations_required`, see
[attack potential resistance](https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-attack-potential-resistance).
+A wallet sends the key attestation in one of two ways,
+see [HAIP key attestation](https://openid.net/specs/openid4vc-high-assurance-interoperability-profile-1_0-final.html#key-attestation).
+With proof type `jwt` the attestation is in the `key_attestation` header of the proof. With proof
+type `attestation` the attestation is the proof, and each of its `attested_keys` gets a credential.
+Advertise both. The OpenID conformance suite uses `jwt`, the Hopae EUDI Wallet only
+`attestation`.
+
Add `<AttributeEncoder xsi:type="oidc:OIDCString" name="GeantIncubatorDiploma_HAIP" />` to the
attribute definition of Step 1 and release the attribute also to the HAIP wallet in
*conf/attribute-filter.xml*.
@@ -3174,6 +3194,13 @@ Issuer identifier is not used.
"key_storage": ["iso_18045_high"],
"user_authentication": ["iso_18045_high"]
}
+ },
+ "attestation": {
+ "proof_signing_alg_values_supported": ["ES256", "ES384", "ES512"],
+ "key_attestations_required": {
+ "key_storage": ["iso_18045_high"],
+ "user_authentication": ["iso_18045_high"]
+ }
}
},
"_issuance_configuration": {
@@ -3207,6 +3234,13 @@ Issuer identifier is not used.
+ "key_storage": ["iso_18045_high"],
+ "user_authentication": ["iso_18045_high"]
+ }
++ },
++ "attestation": {
++ "proof_signing_alg_values_supported": ["ES256", "ES384", "ES512"],
++ "key_attestations_required": {
++ "key_storage": ["iso_18045_high"],
++ "user_authentication": ["iso_18045_high"]
++ }
+ }
+ },
+ "_issuance_configuration": {
@@ -3320,7 +3354,7 @@ release it to the HAIP wallet in *conf/attribute-filter.xml*.
+ <AttributeFilterPolicy id="GeantIncubatorDiplomaToHAIPWallets">
+ <PolicyRequirementRule xsi:type="Requester"
-+ value="haip-wallet" />
++ value="eudiw-abca" />
+ <AttributeRule attributeID="GeantIncubatorDiploma">
+ <PermitValueRule xsi:type="ANY" />
+ </AttributeRule>
@@ -3342,24 +3376,35 @@ The Status List needs nothing more for the mdoc, it is also served as CWT, see
#### Attestations
Key attestations and Wallet Attestations are signed by an attester, the wallet provider. You
-trust the certificate of the attester CA of the wallet provider, in *conf/global.xml*:
+trust the certificate of the attester CA of the wallet provider. The Hopae EUDI Wallet gets
+its attestations from the EUDI wallet provider, https://wallet-provider.eudiw.dev. Its CA is
+*PID Issuer CA 02, EUDI Wallet Reference Implementation, EU*, a root, with SHA-256 fingerprint
+`3B:0A:22:3E:87:48:F3:C4:22:17:29:8A:D9:05:EC:F1:A1:04:E7:3C:D2:C5:4F:F2:1D:A1:6F:DA:80:70:B2:97`.
+Copy [credentials/eudiw-pid-issuer-ca02.crt](docs/example/step-5/credentials/eudiw-pid-issuer-ca02.crt)
+to *credentials/*. The same certificate is in the wallet sources as
+[Wallet/Certificate/pidissuerca02_eu.der](https://github.com/hopae-official/hopae-eudi-ref-wallet/tree/main/Wallet/Certificate)
+and at https://preprod.pki.eudiw.dev/aia/PIDIssuerCA02-EU.cacert.pem. Trust it in
+*conf/global.xml*:
```xml
<util:list id="openidvci.KeyAttestationTrustAnchors">
<bean parent="shibboleth.BasicX509CredentialFactoryBean"
- p:entity="%{idp.home}/credentials/attester-ca.crt"
- p:certificates="#{{'%{idp.home}/credentials/attester-ca.crt'}}" />
+ p:entity="%{idp.home}/credentials/eudiw-pid-issuer-ca02.crt"
+ p:certificates="#{{'%{idp.home}/credentials/eudiw-pid-issuer-ca02.crt'}}" />
</util:list>
<util:list id="openidvci.ClientAttestationTrustAnchors">
<bean parent="shibboleth.BasicX509CredentialFactoryBean"
- p:entity="%{idp.home}/credentials/attester-ca.crt"
- p:certificates="#{{'%{idp.home}/credentials/attester-ca.crt'}}" />
+ p:entity="%{idp.home}/credentials/eudiw-pid-issuer-ca02.crt"
+ p:certificates="#{{'%{idp.home}/credentials/eudiw-pid-issuer-ca02.crt'}}" />
</util:list>
```
+Full file: [conf/global.xml](docs/example/step-5/conf/global.xml)
+
Key attestation is checked for every credential that has `key_attestations_required`. Without
-anchors such a request is refused.
+anchors such a request is refused. For the conformance suite add the certificate of its attester CA
+to both lists the same way.
Wallet Attestation is a client authentication method, so it is added to the login flow of
clients in *conf/authn/oauth2client-authn-config.xml*. It has to be before the validator of
@@ -3387,7 +3432,8 @@ idp.oauth2.authn.flows = OpenIDVCIClient
#### The HAIP wallet as a client
-The client id is the `sub` of the Wallet Attestation of the wallet. In *metadata/oidc-client.json*:
+The client id is the `sub` of the Wallet Attestation of the wallet. The EUDI wallet provider gives
+the Hopae EUDI Wallet `eudiw-abca`. In *metadata/oidc-client.json*:
<details>
<summary>metadata/oidc-client.json</summary>
@@ -3435,9 +3481,9 @@ The client id is the `sub` of the Wallet Attestation of the wallet. In *metadata
"grant_types": ["authorization_code"]
+ },
+ {
-+ "client_id": "haip-wallet",
-+ "client_name": "HAIP Wallet",
-+ "redirect_uris": ["https://wallet.example.org/callback"],
++ "client_id": "eudiw-abca",
++ "client_name": "Hopae EUDI Wallet",
++ "redirect_uris": ["eu.europa.ec.euidi://authorization"],
+ "scope": "openid GeantIncubatorDiploma_HAIP GeantIncubatorDiploma_MDOC",
+ "audience": "credentials",
+ "token_endpoint_auth_method": "attest_jwt_client_auth",
@@ -3574,7 +3620,7 @@ and both token and PAR endpoints take the Wallet Attestation:
</bean>
+ <bean parent="RelyingPartyByName"
-+ c:relyingPartyIds="#{{'haip-wallet'}}">
++ c:relyingPartyIds="#{{'eudiw-abca'}}">
+ <property name="profileConfigurations">
+ <list>
+ <bean parent="OIDC.SSO"
@@ -3611,7 +3657,7 @@ DPoP, see [RFC 9449 10.1](https://www.rfc-editor.org/rfc/rfc9449#section-10.1).
`token_endpoint_auth_methods_supported` of *static/oauth-authorization-server.json* lists the
methods your `tokenEndpointAuthMethods` allow on the token and PAR endpoints, here `none` for
-Paradym and `attest_jwt_client_auth` for the HAIP wallet:
+Paradym and `attest_jwt_client_auth` for the Hopae EUDI Wallet:
<details>
<summary>static/oauth-authorization-server.json</summary>
@@ -3785,8 +3831,7 @@ Check that your wallets support this before you configure several datasets. The
`credential_identifiers` from the token response and request each credential by
`credential_identifier`. A wallet that requests by `credential_configuration_id` only gets
`invalid_credential_request` and no credential. Support for this varies between wallets and
-wallet versions. In our own tests the EUDI reference wallet libraries handled it and some other
-wallets did not.
+wallet versions. In our own tests the Hopae EUDI Wallet, built on EudiWalletKit 0.34.4, did not.
One Credential Configuration may be issued more than once for the same user. A user with a
Master of Science in Computer Science and another in Mathematics gets two credentials of one
diff --git a/docs/example/step-5/conf/attribute-filter.xml b/docs/example/step-5/conf/attribute-filter.xml
index 5dc959d..d3bb945 100644
--- a/docs/example/step-5/conf/attribute-filter.xml
+++ b/docs/example/step-5/conf/attribute-filter.xml
@@ -22,7 +22,7 @@
<AttributeFilterPolicy id="GeantIncubatorDiplomaToHAIPWallets">
<PolicyRequirementRule xsi:type="Requester"
- value="haip-wallet" />
+ value="eudiw-abca" />
<AttributeRule attributeID="GeantIncubatorDiploma">
<PermitValueRule xsi:type="ANY" />
</AttributeRule>
diff --git a/docs/example/step-5/conf/global.xml b/docs/example/step-5/conf/global.xml
new file mode 100644
index 0000000..931367d
--- /dev/null
+++ b/docs/example/step-5/conf/global.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans"
+ xmlns:context="http://www.springframework.org/schema/context"
+ xmlns:util="http://www.springframework.org/schema/util"
+ xmlns:p="http://www.springframework.org/schema/p"
+ xmlns:c="http://www.springframework.org/schema/c"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
+ http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
+ http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"
+ default-init-method="initialize"
+ default-destroy-method="destroy">
+
+ <import resource="openid-vci-credentials.xml"/>
+ <import resource="openid-vci-oidfed.xml"/>
+
+ <util:list id="openidvci.KeyAttestationTrustAnchors">
+ <bean parent="shibboleth.BasicX509CredentialFactoryBean"
+ p:entity="%{idp.home}/credentials/eudiw-pid-issuer-ca02.crt"
+ p:certificates="#{{'%{idp.home}/credentials/eudiw-pid-issuer-ca02.crt'}}" />
+ </util:list>
+
+ <util:list id="openidvci.ClientAttestationTrustAnchors">
+ <bean parent="shibboleth.BasicX509CredentialFactoryBean"
+ p:entity="%{idp.home}/credentials/eudiw-pid-issuer-ca02.crt"
+ p:certificates="#{{'%{idp.home}/credentials/eudiw-pid-issuer-ca02.crt'}}" />
+ </util:list>
+
+</beans>
diff --git a/docs/example/step-5/conf/relying-party.xml b/docs/example/step-5/conf/relying-party.xml
index f66bdb3..afd991c 100644
--- a/docs/example/step-5/conf/relying-party.xml
+++ b/docs/example/step-5/conf/relying-party.xml
@@ -112,7 +112,7 @@
</bean>
<bean parent="RelyingPartyByName"
- c:relyingPartyIds="#{{'haip-wallet'}}">
+ c:relyingPartyIds="#{{'eudiw-abca'}}">
<property name="profileConfigurations">
<list>
<bean parent="OIDC.SSO"
diff --git a/docs/example/step-5/credentials/eudiw-pid-issuer-ca02.crt b/docs/example/step-5/credentials/eudiw-pid-issuer-ca02.crt
new file mode 100644
index 0000000..afcd0cd
--- /dev/null
+++ b/docs/example/step-5/credentials/eudiw-pid-issuer-ca02.crt
@@ -0,0 +1,18 @@
+-----BEGIN CERTIFICATE-----
+MIIC0zCCAnmgAwIBAgIUXRXxkLbUM6+njr/XT0IIw/HA/uowCgYIKoZIzj0EAwMw
+VzEZMBcGA1UEAwwQUElEIElzc3VlciBDQSAwMjEtMCsGA1UECgwkRVVESSBXYWxs
+ZXQgUmVmZXJlbmNlIEltcGxlbWVudGF0aW9uMQswCQYDVQQGEwJFVTAeFw0yNTA0
+MDkwMDAzMzBaFw0zNDA3MDYwMDAzMjlaMFcxGTAXBgNVBAMMEFBJRCBJc3N1ZXIg
+Q0EgMDIxLTArBgNVBAoMJEVVREkgV2FsbGV0IFJlZmVyZW5jZSBJbXBsZW1lbnRh
+dGlvbjELMAkGA1UEBhMCRVUwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARkqdLm
+wIlv+SSWr00tAIrt7EAMztgd3w9qA6qEm16yVfsLcyx2f4oIWuH45wa37J9GoNWp
+deo27VoSoNMCzxOYo4IBITCCAR0wEgYDVR0TAQH/BAgwBgEB/wIBADAfBgNVHSME
+GDAWgBRCUFC+ELgQ8J1EXI2/qxAI7ifcSTATBgNVHSUEDDAKBggrgQICAAABBzBD
+BgNVHR8EPDA6MDigNqA0hjJodHRwczovL3ByZXByb2QucGtpLmV1ZGl3LmRldi9j
+cmwvcGlkX0NBX0VVXzAyLmNybDAdBgNVHQ4EFgQUQlBQvhC4EPCdRFyNv6sQCO4n
+3EkwDgYDVR0PAQH/BAQDAgEGMF0GA1UdEgRWMFSGUmh0dHBzOi8vZ2l0aHViLmNv
+bS9ldS1kaWdpdGFsLWlkZW50aXR5LXdhbGxldC9hcmNoaXRlY3R1cmUtYW5kLXJl
+ZmVyZW5jZS1mcmFtZXdvcmswCgYIKoZIzj0EAwMDSAAwRQIhAIavYfC5o0VVLKfg
+TKkzzWgc09hzDMsCl3O2le2sQfG7AiA2soqAN5gtUOLQKWK00DUz22EW79rvaV+V
+JPvfdQeokA==
+-----END CERTIFICATE-----
diff --git a/docs/example/step-5/metadata/oidc-client.json b/docs/example/step-5/metadata/oidc-client.json
index 5fb670e..5604e55 100644
--- a/docs/example/step-5/metadata/oidc-client.json
+++ b/docs/example/step-5/metadata/oidc-client.json
@@ -40,9 +40,9 @@
"grant_types": ["authorization_code"]
},
{
- "client_id": "haip-wallet",
- "client_name": "HAIP Wallet",
- "redirect_uris": ["https://wallet.example.org/callback"],
+ "client_id": "eudiw-abca",
+ "client_name": "Hopae EUDI Wallet",
+ "redirect_uris": ["eu.europa.ec.euidi://authorization"],
"scope": "openid GeantIncubatorDiploma_HAIP GeantIncubatorDiploma_MDOC",
"audience": "credentials",
"token_endpoint_auth_method": "attest_jwt_client_auth",
diff --git a/docs/example/step-5/metadata/verifiable-credentials.json b/docs/example/step-5/metadata/verifiable-credentials.json
index a963b2b..5e54bd6 100644
--- a/docs/example/step-5/metadata/verifiable-credentials.json
+++ b/docs/example/step-5/metadata/verifiable-credentials.json
@@ -143,6 +143,13 @@
"key_storage": ["iso_18045_high"],
"user_authentication": ["iso_18045_high"]
}
+ },
+ "attestation": {
+ "proof_signing_alg_values_supported": ["ES256", "ES384", "ES512"],
+ "key_attestations_required": {
+ "key_storage": ["iso_18045_high"],
+ "user_authentication": ["iso_18045_high"]
+ }
}
},
"_issuance_configuration": {
@@ -176,6 +183,13 @@
"key_storage": ["iso_18045_high"],
"user_authentication": ["iso_18045_high"]
}
+ },
+ "attestation": {
+ "proof_signing_alg_values_supported": ["ES256", "ES384", "ES512"],
+ "key_attestations_required": {
+ "key_storage": ["iso_18045_high"],
+ "user_authentication": ["iso_18045_high"]
+ }
}
},
"_issuance_configuration": {
diff --git a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/messaging/context/CredentialsContext.java b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/messaging/context/CredentialsContext.java
index 9d79ef7..5bb7185 100644
--- a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/messaging/context/CredentialsContext.java
+++ b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/messaging/context/CredentialsContext.java
@@ -27,6 +27,7 @@ import org.geant.shibboleth.plugin.openidvci.credential.impl.MobileSecurityObjec
import org.geant.shibboleth.plugin.openidvci.messaging.impl.CredentialOfferRequestedCredential;
import org.opensaml.messaging.context.BaseContext;
+import com.nimbusds.jose.jwk.JWK;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;
import com.nimbusds.openid.connect.sdk.claims.ClaimsSet;
@@ -59,6 +60,10 @@ public class CredentialsContext extends BaseContext {
@Nullable
private List<SignedJWT> proofs;
+ /** Holder keys of a validated proof of type 'attestation'. */
+ @Nullable
+ private List<JWK> attestedKeys;
+
/** Issuance instant shared by all credentials of this request. */
@Nullable
private Instant issuedAt;
@@ -110,6 +115,25 @@ public class CredentialsContext extends BaseContext {
this.proofs = proofs;
}
+ /**
+ * Get holder keys of a validated proof of type 'attestation'.
+ *
+ * @return holder keys of a validated proof of type 'attestation'
+ */
+ @Nullable
+ public List<JWK> getAttestedKeys() {
+ return attestedKeys;
+ }
+
+ /**
+ * Set holder keys of a validated proof of type 'attestation'.
+ *
+ * @param keys holder keys of a validated proof of type 'attestation'
+ */
+ public void setAttestedKeys(@Nullable final List<JWK> keys) {
+ attestedKeys = keys;
+ }
+
/**
* Get issuance instant shared by all credentials of this request.
*
diff --git a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/AddCredentialShell.java b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/AddCredentialShell.java
index f5d5e8d..547c7ef 100644
--- a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/AddCredentialShell.java
+++ b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/AddCredentialShell.java
@@ -51,6 +51,7 @@ import org.opensaml.profile.context.ProfileRequestContext;
import org.slf4j.Logger;
import com.fasterxml.jackson.core.JsonProcessingException;
+import com.nimbusds.jose.jwk.JWK;
import com.nimbusds.jwt.SignedJWT;
import com.nimbusds.oauth2.sdk.id.Issuer;
import com.nimbusds.openid.connect.sdk.claims.ClaimsSet;
@@ -277,7 +278,14 @@ public class AddCredentialShell extends AbstractProfileAction {
protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
final List<ClaimsSet> shells = new ArrayList<>();
- if (ctx.getProofs() != null && !ctx.getProofs().isEmpty()) {
+ if (ctx.getAttestedKeys() != null && !ctx.getAttestedKeys().isEmpty()) {
+ final List<JWK> keys = ctx.getAttestedKeys();
+ for (int ordinal = 0; ordinal < keys.size(); ordinal++) {
+ final ClaimsSet shell = createShell(ordinal);
+ shell.setClaim("cnf", Map.of("jwk", keys.get(ordinal).toPublicJWK().toJSONObject()));
+ shells.add(shell);
+ }
+ } else if (ctx.getProofs() != null && !ctx.getProofs().isEmpty()) {
final List<SignedJWT> proofs = ctx.getProofs();
for (int ordinal = 0; ordinal < proofs.size(); ordinal++) {
final ClaimsSet shell = createShell(ordinal);
diff --git a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/AddMobileSecurityObjectShell.java b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/AddMobileSecurityObjectShell.java
index 73b4aa6..b75542c 100644
--- a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/AddMobileSecurityObjectShell.java
+++ b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/AddMobileSecurityObjectShell.java
@@ -110,7 +110,8 @@ public class AddMobileSecurityObjectShell extends AbstractProfileAction {
ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_PROFILE_CONFIG);
return false;
}
- if (ctx.getProofs() == null || ctx.getProofs().isEmpty()) {
+ if ((ctx.getProofs() == null || ctx.getProofs().isEmpty())
+ && (ctx.getAttestedKeys() == null || ctx.getAttestedKeys().isEmpty())) {
log.error("{} An mdoc is bound to a device key, the request carries no proof", getLogPrefix());
ActionSupport.buildEvent(profileRequestContext, OpenIDVCIEventIds.INVALID_PROOF);
return false;
@@ -124,11 +125,14 @@ public class AddMobileSecurityObjectShell extends AbstractProfileAction {
final List<MobileSecurityObject> objects = new ArrayList<>();
final List<SignedJWT> proofs = ctx.getProofs();
- for (int ordinal = 0; ordinal < proofs.size(); ordinal++) {
+ final List<JWK> attestedKeys = ctx.getAttestedKeys();
+ final boolean attested = attestedKeys != null && !attestedKeys.isEmpty();
+ final int count = attested ? attestedKeys.size() : proofs.size();
+ for (int ordinal = 0; ordinal < count; ordinal++) {
final ECPublicKey deviceKey;
final MobileSecurityObject mso;
try {
- deviceKey = deviceKey(proofs.get(ordinal));
+ deviceKey = attested ? ecPublicKey(attestedKeys.get(ordinal)) : deviceKey(proofs.get(ordinal));
if (deviceKey == null) {
log.error("{} Proof carries no EC holder key this issuer binds to", getLogPrefix());
ActionSupport.buildEvent(profileRequestContext, OpenIDVCIEventIds.INVALID_PROOF);
@@ -181,6 +185,18 @@ public class AddMobileSecurityObjectShell extends AbstractProfileAction {
key = JWK.parse(new Base64URL(kid.substring(DidSupport.DID_JWK_PREFIX.length(), kid.length() - 2))
.decodeToString());
}
+ return ecPublicKey(key);
+ }
+
+ /**
+ * Read an EC public key from a JWK.
+ *
+ * @param key holder key
+ * @return EC public key, or null if the key is no EC key
+ * @throws JOSEException if the key cannot be converted
+ */
+ @Nullable
+ private static ECPublicKey ecPublicKey(@Nullable final JWK key) throws JOSEException {
return key instanceof ECKey ecKey ? ecKey.toECPublicKey() : null;
}
diff --git a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/ParseProof.java b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/ParseProof.java
index 5813604..c69af72 100644
--- a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/ParseProof.java
+++ b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/impl/ParseProof.java
@@ -65,10 +65,14 @@ import net.shibboleth.shared.resolver.ResolverException;
/** Action that parses the key proofs of a Credential request. */
public class ParseProof extends AbstractProfileAction {
- /** Proof type this action reads. */
+ /** Proof type of a signed key proof. */
@Nonnull
private static final String PROOF_TYPE_JWT = "jwt";
+ /** Proof type of a key attestation used as the proof. */
+ @Nonnull
+ private static final String PROOF_TYPE_ATTESTATION = "attestation";
+
/** Header of a key proof carrying a key attestation. */
@Nonnull
private static final String KEY_ATTESTATION_HEADER = "key_attestation";
@@ -218,7 +222,7 @@ public class ParseProof extends AbstractProfileAction {
.getMessage() instanceof OpenIDVCICredentialsRequest request) {
proof = request.getProofs();
}
- final ProofTypeSupported jwtProofs = resolveJwtProofs(profileRequestContext);
+ final ProofTypeSupported jwtProofs = resolveProofType(profileRequestContext, PROOF_TYPE_JWT);
keyAttestationsRequired = jwtProofs != null ? jwtProofs.getKeyAttestationsRequired() : null;
proofSigningAlgValuesSupported = jwtProofs != null ? jwtProofs.getProofSigningAlgValuesSupported() : null;
if (keyAttestationsRequired != null && keyAttestationValidator == null) {
@@ -244,8 +248,12 @@ public class ParseProof extends AbstractProfileAction {
@Override
protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
final Object proofToken = proof.get(PROOF_TYPE_JWT);
+ if (proofToken == null && proof.get(PROOF_TYPE_ATTESTATION) != null) {
+ parseAttestationProof(profileRequestContext, proof.get(PROOF_TYPE_ATTESTATION));
+ return;
+ }
if (proofToken == null) {
- log.error("{} Only proofs of type 'jwt' are supported {}", getLogPrefix(), proof);
+ log.error("{} Only proofs of type 'jwt' and 'attestation' are supported {}", getLogPrefix(), proof);
ActionSupport.buildEvent(profileRequestContext, OpenIDVCIEventIds.PROOF_TYPE_UNSUPPORTED);
return;
}
@@ -288,6 +296,61 @@ public class ParseProof extends AbstractProfileAction {
}
+ /**
+ * Parse a proof of type 'attestation', a key attestation whose attested keys are the holder keys.
+ *
+ * <p>See https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#name-attestation-proof-type</p>
+ *
+ * @param profileRequestContext profile request context
+ * @param proofToken value of the 'attestation' member of the proofs
+ */
+ private void parseAttestationProof(@Nonnull final ProfileRequestContext profileRequestContext,
+ @Nonnull final Object proofToken) {
+ final ProofTypeSupported attestationProofs = resolveProofType(profileRequestContext, PROOF_TYPE_ATTESTATION);
+ if (attestationProofs == null) {
+ log.error("{} Requested credential configuration does not support proofs of type 'attestation'",
+ getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, OpenIDVCIEventIds.PROOF_TYPE_UNSUPPORTED);
+ return;
+ }
+ if (keyAttestationValidator == null) {
+ log.error("{} Proofs of type 'attestation' need a key attestation validator, none is configured",
+ getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, IdPEventIds.INVALID_PROFILE_CONFIG);
+ return;
+ }
+ if (!(proofToken instanceof List<?> tokens) || tokens.size() != 1
+ || !(tokens.get(0) instanceof String attestation)) {
+ log.error("{} Key proofs of type 'attestation' are not an array of exactly one string", getLogPrefix());
+ ActionSupport.buildEvent(profileRequestContext, OpenIDVCIEventIds.INVALID_PROOF);
+ return;
+ }
+ final List<JWK> attested;
+ try {
+ final SignedJWT parsed = SignedJWT.parse(attestation);
+ proofSigningAlgValuesSupported = attestationProofs.getProofSigningAlgValuesSupported();
+ validateAlgorithm(parsed.getHeader().getAlgorithm());
+ attested = keyAttestationValidator.validate(profileRequestContext, parsed,
+ attestationProofs.getKeyAttestationsRequired(), keyAttestationClaimsValidator);
+ } catch (final DPoPProofNonceJWTValidationException e) {
+ log.error("{} Key attestation carries an invalid nonce.", getLogPrefix(), e);
+ ActionSupport.buildEvent(profileRequestContext, OpenIDVCIEventIds.INVALID_NONCE);
+ return;
+ } catch (final Exception e) {
+ log.error("{} Key attestation parsing failed.", getLogPrefix(), e);
+ ActionSupport.buildEvent(profileRequestContext, OpenIDVCIEventIds.INVALID_PROOF);
+ return;
+ }
+ if (attested.size() > batchSize) {
+ log.error("{} Key attestation attests {} keys, at most {} are accepted", getLogPrefix(), attested.size(),
+ batchSize);
+ ActionSupport.buildEvent(profileRequestContext, OpenIDVCIEventIds.INVALID_PROOF);
+ return;
+ }
+ profileRequestContext.getInboundMessageContext().ensureSubcontext(CredentialsContext.class)
+ .setAttestedKeys(attested);
+ }
+
/**
* Validate a jwt proof per the OpenID4VCI specification.
*
@@ -404,14 +467,16 @@ public class ParseProof extends AbstractProfileAction {
}
/**
- * Read what the requested credential configuration says of 'jwt' key proofs.
+ * Read what the requested credential configuration says of key proofs of a proof type.
*
* @param profileRequestContext profile request context
+ * @param proofType proof type
*
* @return the proof type, or null when the configuration declares none
*/
@Nullable
- private ProofTypeSupported resolveJwtProofs(@Nonnull final ProfileRequestContext profileRequestContext) {
+ private ProofTypeSupported resolveProofType(@Nonnull final ProfileRequestContext profileRequestContext,
+ @Nonnull final String proofType) {
final CredentialsContext credentialsContext = profileRequestContext.getInboundMessageContext() != null
? profileRequestContext.getInboundMessageContext().getSubcontext(CredentialsContext.class)
@@ -422,7 +487,7 @@ public class ParseProof extends AbstractProfileAction {
if (configuration == null || configuration.getProofTypesSupported() == null) {
return null;
}
- return configuration.getProofTypesSupported().get(PROOF_TYPE_JWT);
+ return configuration.getProofTypesSupported().get(proofType);
}
/**
diff --git a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/logic/CredentialCountLookupFunction.java b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/logic/CredentialCountLookupFunction.java
index be790ab..fe192b9 100644
--- a/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/logic/CredentialCountLookupFunction.java
+++ b/openid-vci-impl/src/main/java/org/geant/shibboleth/plugin/openidvci/profile/logic/CredentialCountLookupFunction.java
@@ -29,7 +29,7 @@ import com.nimbusds.jwt.SignedJWT;
/**
* Supplies the number of credentials a request will produce, being one per
- * validated proof in {@link CredentialsContext}.
+ * validated proof or attested key in {@link CredentialsContext}.
*
* A request without proofs gives one credential without key binding, so the
* count is one.
@@ -50,6 +50,9 @@ public class CredentialCountLookupFunction implements Function<ProfileRequestCon
return 1;
}
+ if (ctx.getAttestedKeys() != null && !ctx.getAttestedKeys().isEmpty()) {
+ return ctx.getAttestedKeys().size();
+ }
final List<SignedJWT> proofs = ctx.getProofs();
return proofs == null || proofs.isEmpty() ? 1 : proofs.size();
diff --git a/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/profile/impl/ParseAttestationProofTest.java b/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/profile/impl/ParseAttestationProofTest.java
new file mode 100644
index 0000000..6aa7f6c
--- /dev/null
+++ b/openid-vci-impl/src/test/java/org/geant/shibboleth/plugin/openidvci/profile/impl/ParseAttestationProofTest.java
@@ -0,0 +1,253 @@
+/*
+ * Copyright (c) 2025, GÉANT
+ *
+ * Licensed under the Apache License, Version 2.0 (the “License”); you may not
+ * use this file except in compliance with the License. You may obtain a copy
+ * of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an “AS IS” BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.geant.shibboleth.plugin.openidvci.profile.impl;
+
+import java.io.InputStream;
+import java.net.URI;
+import java.security.PrivateKey;
+import java.security.cert.X509Certificate;
+import java.time.Instant;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+
+import org.geant.shibboleth.plugin.openidvci.credential.CredentialConfiguration;
+import org.geant.shibboleth.plugin.openidvci.messaging.context.CredentialsContext;
+import org.geant.shibboleth.plugin.openidvci.messaging.impl.OpenIDVCICredentialsRequest;
+import org.geant.shibboleth.plugin.openidvci.profile.OpenIDVCIEventIds;
+import org.geant.shibboleth.plugin.openidvci.profile.config.impl.DefaultOpenIDVCICredentialsConfiguration;
+import org.geant.shibboleth.plugin.openidvci.security.impl.KeyAttestationValidator;
+import org.opensaml.messaging.context.MessageContext;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.security.crypto.KeySupport;
+import org.opensaml.security.x509.BasicX509Credential;
+import org.opensaml.security.x509.X509Support;
+import org.springframework.webflow.execution.RequestContext;
+import org.testng.Assert;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.nimbusds.jose.JOSEObjectType;
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jose.JWSHeader;
+import com.nimbusds.jose.crypto.ECDSASigner;
+import com.nimbusds.jose.jwk.Curve;
+import com.nimbusds.jose.jwk.ECKey;
+import com.nimbusds.jose.jwk.gen.ECKeyGenerator;
+import com.nimbusds.jose.util.Base64;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.SignedJWT;
+import com.nimbusds.oauth2.sdk.http.HTTPRequest;
+
+import net.shibboleth.idp.profile.IdPEventIds;
+import net.shibboleth.idp.profile.context.navigate.WebflowRequestContextProfileRequestContextLookup;
+import net.shibboleth.idp.profile.testing.ActionTestingSupport;
+import net.shibboleth.idp.profile.testing.RequestContextBuilder;
+import net.shibboleth.oidc.jwt.claims.ClaimsValidator;
+import net.shibboleth.oidc.jwt.claims.JWTValidationException;
+import net.shibboleth.profile.context.RelyingPartyContext;
+import net.shibboleth.shared.collection.CollectionSupport;
+
+/**
+ * Unit tests for {@link ParseProof} reading proofs of type 'attestation'.
+ */
+public class ParseAttestationProofTest {
+
+ private static final String ATTESTATION_CONFIGURATION = "{\"format\":\"dc+sd-jwt\",\"vct\":\"TestCredential\","
+ + "\"proof_types_supported\":{\"attestation\":{\"proof_signing_alg_values_supported\":[\"ES256\"],"
+ + "\"key_attestations_required\":{\"key_storage\":[\"iso_18045_high\"]}}}}";
+
+ private static final String JWT_CONFIGURATION = "{\"format\":\"dc+sd-jwt\",\"vct\":\"TestCredential\","
+ + "\"proof_types_supported\":{\"jwt\":{\"proof_signing_alg_values_supported\":[\"ES256\"]}}}";
+
+ private RequestContext requestCtx;
+
+ private ProfileRequestContext profileRequestCtx;
+
+ private DefaultOpenIDVCICredentialsConfiguration configuration;
+
+ private ParseProof action;
+
+ private X509Certificate signerCertificate;
+
+ private PrivateKey signerKey;
+
+ private ECKey firstKey;
+
+ private ECKey secondKey;
+
+ private X509Certificate read(final String name) throws Exception {
+ try (InputStream in = getClass().getResourceAsStream("/credentials/" + name)) {
+ return X509Support.decodeCertificate(in.readAllBytes());
+ }
+ }
+
+ @BeforeMethod
+ protected void setUp() throws Exception {
+ signerCertificate = read("attestation-signer.crt");
+ try (InputStream in = getClass().getResourceAsStream("/credentials/attestation-signer.key")) {
+ signerKey = KeySupport.decodePrivateKey(in.readAllBytes(), null);
+ }
+ firstKey = new ECKeyGenerator(Curve.P_256).generate();
+ secondKey = new ECKeyGenerator(Curve.P_256).generate();
+ requestCtx = new RequestContextBuilder().buildRequestContext();
+ profileRequestCtx = new WebflowRequestContextProfileRequestContextLookup().apply(requestCtx);
+ configuration = new DefaultOpenIDVCICredentialsConfiguration();
+ profileRequestCtx.ensureSubcontext(RelyingPartyContext.class).setProfileConfig(configuration);
+ profileRequestCtx.setInboundMessageContext(new MessageContext());
+ action = actionTrusting("attestation-ca.crt");
+ }
+
+ private ParseProof actionTrusting(final String anchorName) throws Exception {
+ final KeyAttestationValidator validator = new KeyAttestationValidator();
+ validator.setTrustAnchors(CollectionSupport.listOf(new BasicX509Credential(read(anchorName))));
+ validator.initialize();
+ final ParseProof parse = new ParseProof();
+ parse.setKeyAttestationValidator(validator);
+ parse.initialize();
+ return parse;
+ }
+
+ private String attestation(final JWSAlgorithm alg, final List<ECKey> keys, final List<String> keyStorage)
+ throws Exception {
+ final JWSHeader header = new JWSHeader.Builder(alg).type(KeyAttestationValidator.TYPE)
+ .x509CertChain(Arrays.asList(Base64.encode(signerCertificate.getEncoded()))).build();
+ final JWTClaimsSet claims = new JWTClaimsSet.Builder().issueTime(new Date())
+ .expirationTime(Date.from(Instant.now().plusSeconds(300))).claim("nonce", "test-nonce")
+ .claim(KeyAttestationValidator.ATTESTED_KEYS_CLAIM,
+ keys.stream().map(key -> key.toPublicJWK().toJSONObject()).toList())
+ .claim(KeyAttestationValidator.KEY_STORAGE_CLAIM, keyStorage).build();
+ final SignedJWT jwt = new SignedJWT(header, claims);
+ jwt.sign(new ECDSASigner(signerKey, Curve.P_256));
+ return jwt.serialize();
+ }
+
+ private void request(final String configurationJson, final String proofs) throws Exception {
+ final HTTPRequest httpRequest = new HTTPRequest(HTTPRequest.Method.POST, new URI("http://example.com"));
+ httpRequest.setAuthorization("Bearer dGVzdDp0ZXN0");
+ httpRequest.setContentType("application/json");
+ httpRequest.setBody("{\"credential_configuration_id\":\"TestCredential\",\"proofs\":" + proofs + "}");
+ profileRequestCtx.getInboundMessageContext().setMessage(OpenIDVCICredentialsRequest.parse(httpRequest));
+ profileRequestCtx.getInboundMessageContext().ensureSubcontext(CredentialsContext.class)
+ .setCredentialConfiguration(
+ new ObjectMapper().readValue(configurationJson, CredentialConfiguration.class));
+ }
+
+ private void requestAttestation(final String attestation) throws Exception {
+ request(ATTESTATION_CONFIGURATION, "{\"attestation\":[\"" + attestation + "\"]}");
+ }
+
+ private CredentialsContext credentialsContext() {
+ return profileRequestCtx.getInboundMessageContext().getSubcontext(CredentialsContext.class);
+ }
+
+ @Test
+ public void testSuccess() throws Exception {
+ requestAttestation(attestation(JWSAlgorithm.ES256, List.of(firstKey), List.of("iso_18045_high")));
+ ActionTestingSupport.assertProceedEvent(action.execute(requestCtx));
+ Assert.assertNull(credentialsContext().getProofs());
+ Assert.assertEquals(credentialsContext().getAttestedKeys().size(), 1);
+ Assert.assertEquals(credentialsContext().getAttestedKeys().get(0).computeThumbprint(),
+ firstKey.computeThumbprint());
+ }
+
+ @Test
+ public void testBatch() throws Exception {
+ requestAttestation(attestation(JWSAlgorithm.ES256, List.of(firstKey, secondKey), List.of("iso_18045_high")));
+ ActionTestingSupport.assertProceedEvent(action.execute(requestCtx));
+ Assert.assertEquals(credentialsContext().getAttestedKeys().size(), 2);
+ }
+
+ @Test
+ public void testTooManyAttestedKeys() throws Exception {
+ configuration.setBatchSize(1);
+ requestAttestation(attestation(JWSAlgorithm.ES256, List.of(firstKey, secondKey), List.of("iso_18045_high")));
+ ActionTestingSupport.assertEvent(action.execute(requestCtx), OpenIDVCIEventIds.INVALID_PROOF);
+ Assert.assertNull(credentialsContext().getAttestedKeys());
+ }
+
+ @Test
+ public void testNotAdvertised() throws Exception {
+ request(JWT_CONFIGURATION, "{\"attestation\":[\""
+ + attestation(JWSAlgorithm.ES256, List.of(firstKey), List.of("iso_18045_high")) + "\"]}");
+ ActionTestingSupport.assertEvent(action.execute(requestCtx), OpenIDVCIEventIds.PROOF_TYPE_UNSUPPORTED);
+ }
+
+ @Test
+ public void testNoValidator() throws Exception {
+ final ParseProof unvalidated = new ParseProof();
+ unvalidated.initialize();
+ requestAttestation(attestation(JWSAlgorithm.ES256, List.of(firstKey), List.of("iso_18045_high")));
+ ActionTestingSupport.assertEvent(unvalidated.execute(requestCtx), IdPEventIds.INVALID_PROFILE_CONFIG);
+ }
+
+ @Test
+ public void testMoreThanOne() throws Exception {
+ final String attestation = attestation(JWSAlgorithm.ES256, List.of(firstKey), List.of("iso_18045_high"));
+ request(ATTESTATION_CONFIGURATION, "{\"attestation\":[\"" + attestation + "\",\"" + attestation + "\"]}");
+ ActionTestingSupport.assertEvent(action.execute(requestCtx), OpenIDVCIEventIds.INVALID_PROOF);
+ }
+
+ @Test
+ public void testUntrustedChain() throws Exception {
+ requestAttestation(attestation(JWSAlgorithm.ES256, List.of(firstKey), List.of("iso_18045_high")));
+ ActionTestingSupport.assertEvent(actionTrusting("attestation-other-ca.crt").execute(requestCtx),
+ OpenIDVCIEventIds.INVALID_PROOF);
+ }
+
+ @Test
+ public void testKeyStorageNotAccepted() throws Exception {
+ requestAttestation(attestation(JWSAlgorithm.ES256, List.of(firstKey), List.of("iso_18045_basic")));
+ ActionTestingSupport.assertEvent(action.execute(requestCtx), OpenIDVCIEventIds.INVALID_PROOF);
+ }
+
+ @Test
+ public void testNoAttestedKeys() throws Exception {
+ requestAttestation(attestation(JWSAlgorithm.ES256, List.of(), List.of("iso_18045_high")));
+ ActionTestingSupport.assertEvent(action.execute(requestCtx), OpenIDVCIEventIds.INVALID_PROOF);
+ }
+
+ @Test
+ public void testClaimsRejected() throws Exception {
+ configuration.setKeyAttestationClaimsValidator(new ClaimsValidator() {
+ @Override
+ public void validate(final JWTClaimsSet claims, final ProfileRequestContext context)
+ throws JWTValidationException {
+ throw new JWTValidationException("nonce mismatch");
+ }
+
+ @Override
+ public String getId() {
+ return "rejecting";
+ }
+
+ @Override
+ public void setId(final String componentId) {
+ }
+ });
+ requestAttestation(attestation(JWSAlgorithm.ES256, List.of(firstKey), List.of("iso_18045_high")));
+ ActionTestingSupport.assertEvent(action.execute(requestCtx), OpenIDVCIEventIds.INVALID_PROOF);
+ }
+
+ @Test
+ public void testNotAProofArray() throws Exception {
+ request(ATTESTATION_CONFIGURATION, "{\"attestation\":\"not-an-array\"}");
+ ActionTestingSupport.assertEvent(action.execute(requestCtx), OpenIDVCIEventIds.INVALID_PROOF);
+ }
+
+}
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list