[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