8000 [OID4VCI] Review support for proofs, attestations and key binding by mposolda · Pull Request #52637 · keycloak/keycloak · GitHub
[go: up one dir, main page]

Skip to content

Navigation Menu

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[[_oid4vci_example_projects]]
=== Example projects

For the illustration purpose, it is possible to use some example projects, which might be useful for quickstart OID4VCI setup and understand the concepts.

* https://github.com/keycloak/keycloak-oauth-sig/tree/main/oid4vci-deployment[OID4VCI deployment]

* https://github.com/keycloak/keycloak-playground/tree/main/fapi-playground#oid4vci-demo[Keycloak playground]

WARNING: Those are community projects used just for the illustration purposes and not for production environment. They are not officially supported by the {project_name}
project team. Use them at your own discretion. Eventually, feel free to create github issues or pull requests in the corresponding github repositories if you find any issues
related to those example projects.
121 changes: 82 additions & 39 deletions docs/documentation/server_admin/topics/oid4vci/proofs.adoc
Original file line number Diff line number Diff line change
@@ -1,7 +1,57 @@
[[_oid4vci_proofs]]
=== Proof Types and Key Binding in OID4VCI

This section documents the proof types supported by OpenID for Verifiable Credential Issuance (OID4VCI), how to configure trusted attester keys, and how key binding works in the resulting credentials.
A credential proof in OID4VCI is a cryptographic proof-of-possession that a wallet sends to an issuer ({project_name}) to show it controls the private key meant to be
bound to the new credential.

Purposes of the proof are:

* **Anti-replay** - the nonce in the proof prevents a captured credential request from being replayed
* **Key binding** - the public proof key embedded in the VC ties the credential to the Wallet's key pair. When credential is later presented by the wallet to the
credential verifier, verifier is able to cryptographically verify issuer as well as holder/wallet. Verifying of the holder is possible in case that key binding was used.

[[_oid4vci_proofs_use_cases]]
==== Example use cases

===== Use Case 1: Credential request without proof

.Credential request without proof
image::images/oid4vci/proof_part1-no-proof.drawio.svg[Credential request without proof]

. Wallet sends credential request without a `proofs` parameter
. Issuer/{project_name} issues the credential but has no key to bind (for instance, in case of SD-JWT credential format, there is no `cnf` claim)
. Wallet creates the "verifiable credential presentation" and sends it to the verifier. However that presentation does not have key binding (for instance, in case of SD-JWT credential format, presentation ends with `~`)
. Verifier is able to verify issuer signature and eventually disclosed claims digests. However verifier cannot verify holder key binding and hence cannot distinguish the presenter from anyone else who stole the credential.
Risk is, that credential can be replayed or transferred to a different party.

===== Use Case 2: Credential request with non-attested proof

.Credential request with proof
image::images/oid4vci/proof_part2-jwt-proof.drawio.svg[Credential request with proof]

. Wallet fetches `c_nonce` from Issuer's nonce endpoint
. Wallet generates a key pair before credential request (if it does not have existing key pair already). Wallet signs `openid4vci-proof+jwt` (in case that `jwt` proof was used, which is most typical for this use case). JWT header carries public proof key inline (typically in `jwk` claim). The proofs is sent within credential request to the issuer ({project_name})
. Issuer validates proof JWT, extracts public key, embeds into the credential (for instance, in issued SD-JWT format, it is embedded into `cnf.jwk` claim)
. At presentation, Wallet appends a Key-Binding JWT signed with private proof key.
. Thanks to the key binding, verifier is able to verify Key-Binding JWT signature against `cnf.jwk` from issuer signed JWT. This allows to prevent bearer/replay attacks, which is main benefit against use case 1. For more details, see the <<_oid4vci_proofs_key_binding, Key Binding section>>

===== Use Case 3: Credential request with attested proof

Attestation is additional (optional) part of credential proof to add additional security. The purpose is to ensure that the proof key lives in a trusted environment (e.g. hardware secure element). It includes another entity called **Attester**. Attester is an external, trusted party. It participates in the creation of attested credential proofs for the credential wallets.
Attester has trust relationship with the issuer ({project_name}) as issuer should be able to verify attestation.

.Credential request with attested proof
image::images/oid4vci/proof_part3-attested-proof.drawio.svg[Credential request with attested proof]

. Wallet sends its public proof key to the Attester
. Attester checks the key is in a trusted environment (e.g. hardware-backed). Then issues key-attestation JWT signed by attestation private key
. Wallet either sends the key-attestation JWT as an `attestation` proof, or embeds it in the `key_attestation` header of a separate `jwt` proof.
. Issuer validates the attestation JWT against the Attester's public key (registered in Trusted Key IDP); for a `jwt` proof, it also validates the enclosing proof JWT. The public key (or certificate) of the Attester must be configured in {project_name} to establish trust.
. Issuer issues the credential including key-binding and returns to the wallet. It includes the wallet/holder key for the key binding (not the attestation key or attestation certificate)
. Same like step 4 for use case 2
. Same like step 5 for use case 2

The following sections document the proof types supported by OpenID for Verifiable Credential Issuance (OID4VCI), how to configure trusted attester keys, and how key binding works in the resulting credentials.

==== Supported Proof Types

Expand All @@ -15,17 +65,42 @@ This section documents the proof types supported by OpenID for Verifiable Creden

| JWT Proof
| `jwt`
| A signed JWT containing holder binding information. The key used to sign the JWT becomes the holder binding key in the credential.
| A signed JWT containing holder binding information. The key used to sign the JWT becomes the holder binding key in the credential. Supported for both <<_oid4vci_proofs_use_cases,use case 2 and use case 3>>.

| Attestation Proof
| `attestation`
| Proof containing key attestations from a trusted attestation authority.
| Proof containing key attestations from a trusted attestation authority. Supported for <<_oid4vci_proofs_use_cases,use case 3>>.
|===

When a credential requires cryptographic holder binding, the client must provide a proof of an appropriate type. The issuer validates the proof and extracts the binding key, which is then included in the issued credential.

[[_oid4vci_proofs_client_scope_config]]
==== Client Scope Configuration for Proof Types

To require key binding for a credential, configure the OID4VCI client scope with the appropriate attributes. These settings are advertised in the issuer metadata as `cryptographic_binding_methods_supported` and `proof_types_supported`, and they determine which proof types the wallet can use.

.Client scope configuration for proofs
image::images/oid4vci/proof_client_scope_config.png[Client scope configuration for proofs]

Most relevant options of the client scope related to proofs and key binding:

* **Cryptographic binding required** - When OFF, proofs are not required in credential request and key binding will not be used for verifiable credentials. This is <<_oid4vci_proofs_use_cases, use case 1 from examples above>>.
When this is ON, proofs are required in credential request and key binding will be used as specified in the <<_oid4vci_proofs_use_cases, use cases 2 and 3 from examples above>>.
If **Cryptographic binding required** is not enabled, the issuer metadata omits `cryptographic_binding_methods_supported` and `proof_types_supported`.

* **Supported proof types** - The attribute accepts a comma-separated list of supported proof types, currently `jwt` and `attestation`.

* **Cryptographic binding methods** - Comma separated list of key binding methods. Typical value is `jwk` for the supported credential formats like SD-JWT format, but might be different according to what particular format supports.

* **Key attestations required** - If ON, then proofs must be always attested as specified in the <<_oid4vci_proofs_use_cases, use case 3 above>>

* **Key storage resistance levels** and **User authentication resistance levels** - If this is configured, the corresponding values must be included in the attestation proofs. Those will be included in the OID4VCI credential metadata. Details about this are in the https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html#appendix-D.2[section D.2 of the OID4VCI specification].

NOTE: The `proof_signing_alg_values_supported` values in the metadata are automatically included in the issuer metadata based on the realm's supported asymmetric signing algorithms.

==== Configuring Trusted Attesters

This is needed to be configured for the <<_oid4vci_proofs_use_cases, use case 3 from examples above>>.
{project_name} supports JWT proofs that can optionally include key attestations, and the `attestation` proof type. To validate these attestations, configure the trusted attester keys through trust-material identity providers and link those identity providers to the OID4VCI client.

Trusted attester keys are configured per client because different wallets can rely on different attestation authorities. The former realm-level options **Trusted Key IDs** and **Trusted Keys (JSON)** are no longer used for OID4VCI proof validation.
Expand Down Expand Up @@ -118,7 +193,7 @@ Do not place public Web PKI roots or the general {project_name} TLS truststore i

==== Creating and Sending JWT Proofs

JWT proofs are the most common proof type. To create a JWT proof:
JWT proofs are the most common proof type. They support the <<_oid4vci_proofs_use_cases, use cases 2 and 3 from examples above>>. The following steps describe creation of a JWT proof without attestation (use case 2):

===== 1. Obtain a Nonce

Expand Down Expand Up @@ -253,6 +328,7 @@ Include the JWT proof in the credential request:
==== Using Attestation Proofs

Attestation proofs allow the holder to prove key ownership through an attestation authority. This is useful for hardware-backed keys or keys managed by secure enclaves.
The example is described in the <<_oid4vci_proofs_use_cases, use case 3 from examples above>>.

===== Structure of an Attestation Proof

Expand Down Expand Up @@ -323,6 +399,7 @@ An attestation proof is a JWT containing:
}
----

[[_oid4vci_proofs_key_binding]]
==== Key Binding in Credentials

The binding key from the proof is included in the issued credential to ensure the holder possesses the corresponding private key.
Expand Down Expand Up @@ -403,41 +480,7 @@ curl -X POST "https://localhost:8443/realms/myrealm/protocol/oid4vc/credential"
}'
----

==== Client Scope Configuration for Proof Types

To require key binding for a credential, configure the OID4VCI client scope with the appropriate attributes. These settings are advertised in the issuer metadata as `cryptographic_binding_methods_supported` and `proof_types_supported`, and they determine which proof types the wallet can use.

[source,json]
----
{
"name": "my-vc-scope",
"protocol": "oid4vc",
"attributes": {
"include.in.token.scope": "true",
"vc.format": "jwt_vc_json",
"vc.credential_configuration_id": "my-credential-config",
"vc.binding_required": "true",
"vc.cryptographic_binding_methods_supported": "jwk",
"vc.binding_required_proof_types": "jwt,attestation",
"vc.credential_signing_alg": "ES256"
},
"protocolMappers": [
{
"name": "givenName",
"protocol": "oid4vc",
"protocolMapper": "oid4vc-user-attribute-mapper",
"config": {
"claim.name": "given_name",
"userAttribute": "firstName"
}
}
]
}
----

The `vc.binding_required_proof_types` attribute accepts a comma-separated list of supported proof types, currently `jwt` and `attestation`. If `vc.binding_required` is not enabled, the credential configuration does not require holder binding and the issuer metadata omits `cryptographic_binding_methods_supported` and `proof_types_supported`.

The `proof_signing_alg_values_supported` values are automatically included in the issuer metadata based on the realm's supported asymmetric sig 8 ning alg 10BC8 orithms.
Additional working examples on how to setup and use proofs can be seen from the https://github.com/keycloak/keycloak-playground/blob/main/fapi-playground/README.md#oid4vci-demo[Keycloak playground repository] (not officially supported repository).

==== Related Configuration

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ OpenID4VCI supports multiple credential formats for encoding and issuing credent
- https://github.com/decentralized-identity/jwt-vc-presentation-profile[JWT VC] – JSON Web Token Verifiable Credential
- https://www.iso.org/standard/69084.html[mDL/mdoc] – Mobile driver's license / mobile document format

{project_name} currently supports both SD-JWT VC and JWT VC for issuance. mDL/mdoc support is planned for a future release.
{project_name} currently supports both SD-JWT VC and JWT VC for issuance. mDL/mdoc format has just experimental support in {project_name} and should not be used in production environments. It is planned for a proper support in the future releases.

=== The Issuer-Holder-Verifier Model

Expand Down Expand Up @@ -211,7 +211,7 @@ Do not use a generated realm key to sign SD-JWT VCs. A generated key either has

Elliptic Curve cryptography with the P-256 curve and `ES256` algorithm is recommended for OpenID4VC. To configure a suitable signing key:

. Obtain a P-256 private key and a corresponding leaf certificate issued by a certificate authority. Include intermediate certificates when applicable.
. Obtain a P-256 private key and a corresponding leaf certificate issued by a certificate authority. Include intermediate certificates when applicable. The example how to create valid ES256 key is in the https://github.com/keycloak/keycloak-playground/blob/main/fapi-playground/keystores/oid4vci-ssl-test/README-generate-keystore.md[Keycloak playground repository] (this is just for the development purposes and not officially supported).
. Store the private key and its certificate chain in a supported Java keystore. The private-key entry must contain the leaf certificate first, followed by any intermediate certificates. You may append a self-signed root certificate, but you must not append a non-self-signed certificate that is intended to be the trust anchor.
. In the Admin Console, go to *Realm Settings* > *Keys* > *Providers*.
. Add a *java-keystore* provider and configure the keystore path, type, passwords, and key alias.
Expand Down Expand Up @@ -394,7 +394,7 @@ After selecting the OID4VCI protocol, the following configuration fields become

| Supported Format
| SD-JWT VC (`dc+sd-jwt`)
| The credential format. Supported options: *SD-JWT VC* (`dc+sd-jwt`) and *JWT VC* (`jwt_vc_json`).
| The credential format. Supported options: *SD-JWT VC* (`dc+sd-jwt`) and *JWT VC* (`jwt_vc_json`). There is also experimental support for the mdoc format, which is available just if the {project_name} server is started with the feature `oid4vc-mdoc` (Not recommended to use in the production environments).
Comment thread
mposolda marked this conversation as resolved.
Resolved

| Token JWS Type
| (auto)
Expand All @@ -412,13 +412,13 @@ After selecting the OID4VCI protocol, the following configuration fields become
| `sha-256`
| Hashing algorithm used before signing the credential.

| Binding Required
| Cryptographic Binding Required
| Off
| When enabled, the credential must be cryptographically bound to the holder's wallet.
| When enabled, the credential must be cryptographically bound to the holder's wallet. More details about this option are in the <<_oid4vci_proofs_client_scope_config,proofs section>>.

| Cryptographic Binding Methods
| (when binding enabled)
| Comma-separated cryptographic binding methods. Allowed value: `jwk`.
| Comma-separated cryptographic binding methods. The default is `jwk` for SD-JWT VC and JWT VC, while mdoc uses `cose_key`.

| Binding Supported Proof Types
| (when binding enabled)
Expand Down Expand Up @@ -653,3 +653,7 @@ include::managing-credentials.adoc[]
include::wallet-integration.adoc[]

include::proofs.adoc[]

ifeval::[{project_community}==true]
include::example-projects.adoc[]
endif::[]
Original file line number Diff line number Diff line change
Expand Up @@ -1925,6 +1925,11 @@ private void validateProofs(VCIssuanceContext vcIssuanceContext, String proofTyp
} catch (VCIssuerException e) {
eventBuilder.detail(Details.REASON, e.getMessage())
.error(e.getErrorType().getValue());

if (LOGGER.isTraceEnabled()) {
LOGGER.trace(e.getMessage(), e);
}

switch (e.getErrorType()) {
case INVALID_NONCE:
throw new ErrorResponseException(INVALID_NONCE.getValue(), e.getMessage(), Response.Status.BAD_REQUEST);
Expand All @@ -1934,6 +1939,13 @@ private void validateProofs(VCIssuanceContext vcIssuanceContext, String proofTyp
throw new BadRequestException("Could not validate provided proof", e);
}
} catch (CredentialBuilderException e) {
eventBuilder.detail(Details.REASON, e.getMessage())
.error(INVALID_PROOF.getValue());
Comment thread
mposolda marked this conversation as resolved.
Resolved

if (LOGGER.isTraceEnabled()) {
LOGGER.trace(e.getMessage(), e);
}

// A proof key that cannot be bound to the credential, such as an mDoc COSE_Key conversion that rejects
// the key curve, is an invalid proof rather than a server error.
throw new ErrorResponseException(INVALID_PROOF.getValue(), e.getMessage(), Response.Status.BAD_REQUEST);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,10 @@ public Response update(IdentityProviderRepresentation providerRep) {
message = "Invalid request";
}

if (logger.isDebugEnabled()) {
logger.debug(message, e);
}

throw ErrorResponse.error(message, BAD_REQUEST);
} catch (ModelDuplicateException e) {
throw ErrorResponse.exists("Identity Provider " + providerRep.getAlias() + " already exists");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
import org.eclipse.microprofile.openapi.annotations.responses.APIResponses;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
import org.jboss.logging.Logger;
import org.jboss.resteasy.reactive.NoCache;

import static jakarta.ws.rs.core.Response.Status.BAD_REQUEST;
Expand All @@ -86,6 +87,8 @@
@Extension(name = KeycloakOpenAPI.Profiles.ADMIN, value = "")
public class IdentityProvidersResource {

protected static final Logger logger = Logger.getLogger(IdentityProvidersResource.class);

private final RealmModel realm;
private final KeycloakSession session;
private final AdminPermissionEvaluator auth;
Expand Down Expand Up @@ -293,6 +296,10 @@ public Response create(@Parameter(description = "JSON body") IdentityProviderRep
message = "Invalid request";
}

if (logger.isDebugEnabled()) {
logger.debug(message, e);
}

throw ErrorResponse.error(message, BAD_REQUEST);
} catch (ModelDuplicateException e) {
throw ErrorResponse.exists("Identity Provider " + representation.getAlias() + " already exists");
Expand Down
Loading
0