Skip to content

France Identité OID4VCI Android Integration

Table of Contents


Overview

This document is addressed to teams implementing an Android wallet that integrates with the France Identité app to receive an age verification credential using the OpenID for Verifiable Credential Issuance (OpenID4VCI) 1.0 protocol and Android Credential Manager.

The calling wallet plays two consecutive roles in this flow:

  1. Initiator — it launches the France Identité app via a deeplink, which triggers authentication and credential offer retrieval on the France Identité side.
  2. Credential Manager provider — it is registered as a Digital Credential provider in Android Credential Manager. Once France Identité passes the credential request to Credential Manager and the user selects this wallet in the chooser, the wallet receives the request and performs the OID4VCI network exchanges.

The wallet performs four sequential network exchanges with the issuer: Challenge, Token, Nonce, and Credential. The Token and Credential exchanges use DPoP (Demonstrating Proof of Possession, RFC 9449). The Token exchange additionally requires a Client Attestation proving the wallet's identity.


Architecture Overview

Actors

Calling Wallet Application Calling Wallet Backend Android Credential Manager France Identité App France Identité Backend (Credential Issuer)
Description The Android wallet application implementing this guide. Plays two roles: initiates the flow via deeplink, then receives the credential request as a registered Credential Manager provider and performs the OID4VCI exchanges. The wallet's own backend, responsible for generating and signing the OAuth-Client-Attestation JWT. Android system component that mediates the handoff of the credential request from France Identité to the wallet. Authenticates the user, retrieves the credential offer, and triggers Credential Manager. Issues OAuth2 access tokens, validates proofs, and generates signed credentials.

Cryptographic Material Reference

A single issuance session involves multiple key pairs and nonces. This section maps each piece of cryptographic material to where it originates and where it is used, to prevent confusion before reading the step-by-step detail.

Key Inventory

Key Algorithm Lifetime Storage Purpose
Ephemeral Session Key EC P-256 Per issuance session In memory only Signs DPoP JWTs (token and credential requests); embedded in Client Attestation cnf.jwk; signs the Client Attestation PoP
Credential Binding Key EC P-256 / COSE_Key Long-lived (per credential) Android Keystore (hardware-backed) Bound to the issued credential as the device key in the MSO; used as the signing key in the jwt proof type and the android_keystore_attestation proof type
Credential Response Encryption Key EC P-256 Per issuance session (ephemeral) In memory only; discard after decrypting the response Wallet provides the public half in the credential request so the issuer can encrypt the response; private half decrypts it
Issuer Credential Request Encryption Key EC P-256 Long-lived (issuer-managed) Embedded in signed issuer metadata at credential_request_encryption.jwks Used by the wallet to encrypt the credential request body

The Ephemeral Session Key and the Credential Binding Key are distinct key pairs and must never be the same key. The Ephemeral Session Key is represented as a JWK in DPoP and Client Attestation material, then discarded after the session. The Credential Binding Key persists and is bound to the mdoc credential as a COSE_Key. For the attestation and android_keystore_attestation proof types, the attested key is the Credential Binding Key, not the Ephemeral Session Key.


Nonce Reference

Four distinct nonce values flow through a session. Each originates from a different response and is consumed in a different place.

Nonce Type Origin Delivered via Consumed in Consumed as
attestation_challenge Server-generated Challenge response Response body Client Attestation PoP JWT challenge claim
DPoP-Nonce (1st) Server-generated Challenge response DPoP-Nonce response header or equivalent response metadata Token request DPoP JWT nonce claim
c_nonce Server-generated Nonce response Response body Proof JWT (inside credential request body) nonce claim
DPoP-Nonce (2nd) Server-generated Nonce response DPoP-Nonce response header or equivalent response metadata Credential request DPoP JWT nonce claim

attestation_challenge and the first DPoP nonce are returned for the same stage but serve entirely different purposes. Do not derive one from the other or reuse one in place of the other.


Issuer Metadata

Before initiating any exchange, the wallet retrieves the issuer's metadata from the well-known discovery endpoint. This document contains the URLs for all subsequent requests and the embedded JWKS needed to encrypt the credential request.

Discovery Endpoint URL Construction

In OID4VCI 1.0, when the issuer identifier contains a path component, the /.well-known/openid-credential-issuer segment is inserted between the host and the path, not appended at the end.

https://{fqdn}/.well-known/openid-credential-issuer/{path}

Request

GET /.well-known/openid-credential-issuer/{path} HTTP/1.1

Response Format

The server returns a signed JWT (Content-Type: application/jwt). The wallet MUST verify the JWT signature and claims before trusting the payload.

HTTP/1.1 200 OK
Content-Type: application/jwt

<signed_jwt>

The JWT payload contains the metadata claims. Example decoded payload:

{
  "iss": "{credential_issuer}",
  "sub": "{credential_issuer}",
  "iat": <unix_timestamp>,
  "credential_issuer": "{credential_issuer}",
  "credential_endpoint": "{credential_issuer}/credential",
  "token_endpoint": "{credential_issuer}/token",
  "challenge_endpoint": "{credential_issuer}/challenge",
  "nonce_endpoint": "{credential_issuer}/nonce",
  "credential_request_encryption": {
    "encryption_required": true,
    "jwks": {
      "keys": [
        {
          "kty": "EC",
          "crv": "P-256",
          "alg": "ECDH-ES",
          "use": "enc",
          "kid": "<kid>",
          "x": "<x_coordinate>",
          "y": "<y_coordinate>"
        }
      ]
    },
    "enc_values_supported": ["A256GCM"]
  },
  "credential_response_encryption": {
    "encryption_required": false,
    "alg_values_supported": ["ECDH-ES"],
    "enc_values_supported": ["A256GCM"]
  },
  "credential_configurations_supported": {
    "eu.europa.ec.av.1": {
      "format": "mso_mdoc",
      "doctype": "eu.europa.ec.av.1",
      "cryptographic_binding_methods_supported": ["cose_key"],
      "credential_signing_alg_values_supported": [-9],
      "proof_types_supported": {
        "android_keystore_attestation": {
          "attestations_required": {},
          "x509_signature_algorithm_oids_supported": [
            "1.2.840.10045.4.3.2",
            "1.2.840.113549.1.1.11"
          ]
        },
        "attestation": {
          "key_attestations_required": {
            "key_storage": [
              "iso_18045_high",
              "iso_18045_moderate",
              "iso_18045_enhanced",
              "iso_18045_basic"
            ]
          },
          "proof_signing_alg_values_supported": [
            "ES256"
          ]
        },
        "jwt": {
          "key_attestations_required": {},
          "proof_signing_alg_values_supported": [
            "ES256"
          ]
        }
      }
    }
  }
}

Metadata Notes

x509_signature_algorithm_oids_supported. The two OIDs in the android_keystore_attestation proof type describe which signature algorithms are accepted in the attestation certificate chain: 1.2.840.10045.4.3.2 is ecdsa-with-SHA256, 1.2.840.113549.1.1.11 is sha256WithRSAEncryption.

Metadata JWT Validation

Before using the decoded metadata, the wallet MUST:

  1. Verify the metadata JWT header is explicitly typed as openidvci-issuer-metadata+jwt.
  2. Validate the JWT signature using the x5c certificate chain in the JWT header and the configured trust anchor for the France Identité environment.
  3. Verify iss, sub, and credential_issuer match the expected issuer identifier.
  4. Verify the metadata is fresh enough for local policy using iat and any cache policy.

Relevant Fields

Field Description
credential_issuer Issuer identifier — used as the aud in the proof JWT
challenge_endpoint URL of the challenge endpoint (Step 3)
token_endpoint URL of the token endpoint (Step 4)
nonce_endpoint URL of the nonce endpoint (Step 5)
credential_endpoint URL of the credential endpoint (Step 6)
credential_request_encryption.jwks Embedded JSON Web Key Set containing the issuer encryption key for credential request JWE encryption
credential_request_encryption.encryption_required true in this integration — the credential request body MUST be encrypted as a compact JWE
credential_response_encryption Algorithms the issuer accepts when encrypting the credential response to the wallet
credential_response_encryption.encryption_required false in this integration — response encryption is optional; providing credential_response_encryption in the credential request is recommended to keep the issued credential confidential in transit
credential_configurations_supported Object describing each supported credential type and its format

Base URL and Environments

All network exchanges in Steps 3–6 are relative to a common base URL.

Environment Base URL
Playground https://api.playground.france-identite.gouv.fr/poc/idakto/openid4vci/v2
Partenaires TBD
Production TBD

All request examples in this document use the placeholder {issuer_base_url}. Substitute the appropriate environment value before making requests.


Flow Overview

Sequence Diagram

sequenceDiagram
    participant FIA as France Identité App
    participant ACM as Android CredentialManager
    participant CWA as Calling Wallet App
    participant CWB as Calling Wallet Backend
    participant FI as France Identité Backend / Issuer

    CWA->>FIA: Deeplink
    FIA->>ACM: CredentialManager request
    ACM->>CWA: Wallet chooser + credential request data
    CWA->>FI: Challenge Request
    FI->>CWA: Challenge Response (attestation_challenge + DPoP nonce)
    CWA->>CWB: Client Attestation Request (with attestation_challenge)
    CWB->>CWA: Client Attestation Response (client_attestation JWT)
    CWA->>CWA: Build DPoP JWT (nonce = DPoP nonce) + Client Attestation PoP JWT (challenge = attestation_challenge)
    CWA->>FI: Token Request (DPoP + OAuth-Client-Attestation + OAuth-Client-Attestation-PoP + pre-authorized_code)
    FI->>CWA: Token Response (access_token, token_type: DPoP)
    CWA->>FI: Nonce Request
    FI->>CWA: Nonce Response (c_nonce + DPoP nonce)
    CWA->>CWA: Build DPoP JWT (nonce = DPoP nonce) + credential proof (nonce = c_nonce) + encrypt request JWE
    CWA->>FI: Credential Request (Authorization: DPoP + DPoP header + encrypted JWE body)
    FI->>CWA: Credential Response (encrypted JWE)
    CWA->>CWA: Decrypt credential response JWE

Step Summary

Step Actor Endpoint Method Key Inputs Key Outputs
1 — Deeplink Calling Wallet App France Identité App Intent credential_type, wallet_name
2 — Credential Manager France Identité App Android Credential Manager API pre-authorized_code, credential_issuer
3 — Challenge Wallet {issuer_base_url}/challenge POST attestation_challenge, DPoP nonce
4 — Token Wallet {issuer_base_url}/token POST DPoP, Client Attestation, Attestation PoP, pre-authorized_code access_token
5 — Nonce Wallet {issuer_base_url}/nonce POST c_nonce, DPoP nonce
6 — Credential Wallet {issuer_base_url}/credential POST DPoP, Authorization: DPoP, encrypted JWE body Encrypted JWE credential

The calling wallet application launches the France Identité app using a deeplink. The flow is never initiated from the France Identité app itself.

https://idp.france-identite.gouv.fr/credentials-request#<parameters>
Parameter Type Required Description Example
wallet_name string YES Display name of the calling wallet application MyWallet
credential_type string YES Identifier of the credential type being requested eu.europa.ec.av.1
credential_format string YES Format of the credential being requested (mso_mdoc or sd+jwt) mso_mdoc

Example

https://idp.france-identite.gouv.fr/credentials-request#wallet_name=MyWallet&credential_type=eu.europa.ec.av.1&credential_format=mso_mdoc

Step 2 — Credential Manager Request

After receiving the deeplink, the France Identité app guides the user through an authentication process, obtains a credential offer from the backend, and passes the issuance request to Android Credential Manager.

User Flow



The user is presented with an explanation of the process.


The user authenticates and authorises the France Identité backend to provision the requested credential.


The France Identité app receives the credential offer URL, retrieves the offer, and extracts the pre-authorized_code.


The France Identité app passes the credential request to Android Credential Manager. The selected wallet receives this payload and begins the OID4VCI exchanges.

Credential Manager Provider Registration

To appear in the wallet chooser and receive the credential request, the wallet must register as a Digital Credential provider in Android Credential Manager. Refer to the official Android documentation for implementation details:

Android Credential Manager — Digital Credentials provider guide

Once registered, the wallet receives the credential request payload described below when the user selects it in the chooser. The OID4VCI flow (Steps 3–6) is then initiated from within the provider callback.


Credential Request Structure

The payload delivered to the wallet through Credential Manager is assembled by the France Identité app. The wallet receives it as-is and must not attempt to modify it.

{
  "requests": [
    {
      "protocol": "openid4vci1.0",
      "data": {
        "credential_issuer": "<credential_issuer>",
        "credential_configuration_ids": [
          "eu.europa.ec.av.1"
        ],
        "grants": {
          "urn:ietf:params:oauth:grant-type:pre-authorized_code": {
            "pre-authorized_code": "<pre-authorized_code>"
          }
        }
      }
    }
  ]
}

Payload Fields

Field Type Description
requests array\ List of credential issuance requests. Currently always contains exactly one entry.
requests[].protocol string Issuance protocol identifier. Always openid4vci1.0.
requests[].data.credential_issuer string Credential issuer URL. This value MUST match the credential_issuer identifier from the signed issuer metadata.
requests[].data.credential_configuration_ids array\ Identifiers of the requested credential types (e.g. ["eu.europa.ec.av.1"]).
requests[].data.grants[...].pre-authorized_code string Short-lived, single-use authorization code used to obtain an access token at the token endpoint. Treat as a secret.

Step 3 — Challenge Request

The wallet calls the challenge endpoint to obtain the values it needs before building the token request: an attestation_challenge for the Client Attestation PoP and a server-provided DPoP nonce for the token request's DPoP JWT.

Extension: The challenge endpoint is not part of the core OID4VCI 1.0 flow. It exists specifically to support OAuth 2.0 Attestation-Based Client Authentication: the attestation_challenge it returns is the server-generated nonce bound into the OAuth-Client-Attestation-PoP JWT at the token request step.

Request

POST {issuer_base_url}/challenge HTTP/1.1

No request body or authentication headers are required.

Response

Response Headers

HTTP/1.1 200 OK
Content-Type: application/json
DPoP-Nonce: <dpop_nonce>
Header Description
DPoP-Nonce Server-issued nonce to include in the nonce claim of the token request DPoP JWT

In some integration traces, intermediary logging does not expose the DPoP-Nonce response header even though the next DPoP proof contains a server-provided nonce. Treat the nonce value as opaque response metadata. Do not reuse attestation_challenge as the DPoP nonce.

Response Body

{
  "attestation_challenge": "hEOhASagV4JQ0Uqc0Q5gryefi5ScjgJKWxpqGG-RWEACBR8szWiB9fya-KkGAyEbiWBnxo5XSumw3Zj3eZZpek8j7-DWMOZfPYkAsHFczNzjoikLN3tSc2Lwa0-VEndA"
}
Field Type Description
attestation_challenge string Server-generated nonce to include in the challenge claim of the Client Attestation PoP JWT

Two nonces, two purposes. attestation_challenge (response body) goes into the Client Attestation PoP. The DPoP nonce goes into the DPoP JWT for the token request. They are independent values.


Step 4 — Token Request

The wallet exchanges the pre-authorized_code for a DPoP-bound access token. Beyond the standard OAuth2 parameters, the request carries three security headers: a DPoP proof, a Client Attestation, and a Client Attestation PoP.

Request

POST {issuer_base_url}/token HTTP/1.1
Content-Type: application/x-www-form-urlencoded
DPoP: <dpop_jwt>
OAuth-Client-Attestation: <client_attestation_jwt>
OAuth-Client-Attestation-PoP: <client_attestation_pop_jwt>

grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code
&pre-authorized_code=w9HRltH_WKYtHHf2RttR_7U2-YcQ5fX7iokppzyZxZQ

Body Parameters

Parameter Type Description
grant_type string Always urn:ietf:params:oauth:grant-type:pre-authorized_code
pre-authorized_code string The pre-authorized code received in the Credential Manager payload (Step 2)

DPoP Header

The DPoP header is a signed JWT that binds the token request to the wallet's key pair and to the target endpoint. A fresh DPoP JWT MUST be generated for each request.

JWT Header

{
  "typ": "dpop+jwt",
  "alg": "ES256",
  "jwk": {
    "kty": "EC",
    "crv": "P-256",
    "kid": "<wallet_key_id>",
    "x": "<x_coordinate>",
    "y": "<y_coordinate>"
  }
}

The jwk field contains the Ephemeral Session Key public key. The corresponding private key signs the JWT.

JWT Payload

Standard DPoP proof JWT claims (htm, htu, iat, jti) are defined in RFC 9449 §4.2. The integration-specific claim is:

{
  "htm": "POST",
  "htu": "{issuer_base_url}/token",
  "iat": <unix_timestamp>,
  "nonce": "<dpop_nonce>",
  "jti": "<unique_identifier>"
}
Claim Description
htm HTTP method: POST
htu Full URI of the token endpoint
iat Issuance timestamp
nonce The DPoP nonce value supplied for the token request in Step 3
jti Unique identifier preventing DPoP JWT reuse

OAuth-Client-Attestation Header

The OAuth-Client-Attestation header contains a JWT issued by the wallet backend. It attests to the wallet application's identity and binds the attestation to the Ephemeral Session Key via cnf.jwk. The wallet presents this JWT as received from its backend — it does not generate it itself.

JWT Header

{
  "typ": "oauth-client-attestation+jwt",
  "alg": "ES256",
  "x5c": ["<leaf_certificate>", "<intermediate_certificate>", "<root_certificate>"]
}

The x5c array contains the certificate chain used to verify the attestation signature, from leaf to root.

JWT Payload

{
  "sub": "<client_id>",
  "wallet_name": "<wallet_display_name>",
  "wallet_link": "<wallet_store_or_info_url>",
  "iss": "Android Keystore",
  "cnf": {
    "jwk": {
      "kty": "EC",
      "crv": "P-256",
      "kid": "<wallet_key_id>",
      "x": "<x_coordinate>",
      "y": "<y_coordinate>"
    }
  },
  "iat": <unix_timestamp>,
  "exp": <unix_timestamp>
}
Claim Description
sub The wallet application's registered client identifier
wallet_name Display name of the wallet
wallet_link URL pointing to the wallet's store listing or information page
iss Attestation issuer identifier. The value "Android Keystore" is used when the wallet's attestation signing key is hardware-backed by Android Keystore. Per draft-ietf-oauth-attestation-based-client-auth, this claim would normally be a URI identifying the attestation issuer service; "Android Keystore" is the convention used in this integration for hardware-signed attestations.
cnf.jwk The Ephemeral Session Key public key — identical to the key embedded in the DPoP JWT
iat / exp Validity window of the attestation

OAuth-Client-Attestation-PoP Header

The OAuth-Client-Attestation-PoP header is a JWT signed by the Ephemeral Session Key private key. It proves possession of the key referenced in the Client Attestation (cnf.jwk) and ties this attestation to the current issuance session via the attestation_challenge.

JWT Header

{
  "typ": "oauth-client-attestation-pop+jwt",
  "alg": "ES256",
  "jwk": {
    "kty": "EC",
    "crv": "P-256",
    "kid": "<wallet_key_id>",
    "x": "<x_coordinate>",
    "y": "<y_coordinate>"
  }
}

JWT Payload

{
  "iss": "<client_id>",
  "aud": "{credential_issuer}",
  "challenge": "<attestation_challenge>",
  "iat": <unix_timestamp>,
  "exp": <unix_timestamp>,
  "jti": "<unique_identifier>"
}
Claim Description
iss The wallet application's client identifier
aud The credential_issuer value from the issuer metadata
challenge The attestation_challenge value from the challenge response body (Step 3)
iat / exp Validity window
jti Unique identifier preventing replay

Response

{
  "access_token": "yRP5dc9cdfx4c_Ne8DZP9Uj4htaxUWJ05EFMFRWHIxA",
  "expires_in": 896,
  "token_type": "DPoP"
}
Field Type Description
access_token string The DPoP-bound access token used in the credential request
token_type string Always DPoP — the token MUST be used with a DPoP proof, not as a plain Bearer token
expires_in integer Token lifetime in seconds

Note: The token response does not contain a c_nonce. The credential nonce is fetched separately in Step 5.


Step 5 — Nonce Request

The wallet fetches a fresh c_nonce from the nonce endpoint. This stage also provides a DPoP nonce for the credential request's DPoP JWT.

Request

POST {issuer_base_url}/nonce HTTP/1.1

No request body or authentication headers are required.

Response

Response Headers

HTTP/1.1 200 OK
Content-Type: application/json
DPoP-Nonce: <dpop_nonce>
Header Description
DPoP-Nonce Server-issued nonce. Include in the nonce claim of the credential request DPoP JWT.

As with Step 3, do not derive the DPoP nonce from c_nonce. They are separate opaque values even if a trace or proxy records only the JSON body.

Response Body

{
  "c_nonce": "hEOhASagV4JQ_PV8-80xj556CJFQG4QEpBpqGG-RWECI21HpF8kSeufcFhLckHU7i7eE65ixQvgnTDjLPhnXA1n6KjLLiF1VtIwUqiLWrSXSBnDLOnz17UfC5nwwqPfV"
}
Field Type Description
c_nonce string Server-generated nonce to include in the nonce claim of the proof JWT inside the credential request body

Two nonces, two purposes. c_nonce (response body) binds the credential proof. The DPoP nonce binds the DPoP JWT. They are independent values, same pattern as Step 3.


Step 6 — Credential Request

The wallet submits the credential request to the issuer. Two features distinguish this from a standard OID4VCI credential request:

  1. DPoP-bound authorization — uses Authorization: DPoP (not Bearer), with a DPoP JWT that additionally carries an ath (access token hash) claim.
  2. Encrypted request body — the credential request JSON is encrypted as a compact JWE using the issuer's public encryption key, and sent with Content-Type: application/jwt.

Request

Headers

POST {issuer_base_url}/credential HTTP/1.1
Authorization: DPoP <access_token>
DPoP: <dpop_jwt>
Content-Type: application/jwt
Header Description
Authorization DPoP <access_token> — DPoP scheme, not Bearer
DPoP Fresh DPoP JWT bound to the credential endpoint and access token
Content-Type application/jwt — body is a compact JWE

DPoP JWT

The DPoP JWT for the credential request follows the same structure as the token request, with two differences: the htu points to the credential endpoint, and it includes an additional ath claim.

Same structure as the token request DPoP header (see Step 4).

Payload

Standard DPoP proof JWT claims (htm, htu, iat, jti) are defined in RFC 9449 §4.2. The integration-specific claims are nonce (Step 5 server-supplied value) and ath (access token binding defined in RFC 9449 §4.3):

{
  "htm": "POST",
  "htu": "{issuer_base_url}/credential",
  "ath": "<base64url(SHA-256(access_token))>",
  "iat": <unix_timestamp>,
  "nonce": "<dpop_nonce>",
  "jti": "<unique_identifier>"
}
Claim Description
htm HTTP method: POST
htu Full URI of the credential endpoint
ath Base64url-encoded SHA-256 hash of the raw access_token string, binding this DPoP proof to the specific token
iat Issuance timestamp
nonce The DPoP nonce value supplied for the credential request in Step 5
jti Unique identifier preventing DPoP JWT reuse

Request Body — Encrypted JWE

The credential request JSON is encrypted as a compact JWE before transmission. The encryption uses the issuer's public key from credential_request_encryption.jwks in the signed issuer metadata (see Issuer Metadata).

Why encryption? The credential request body contains the wallet's binding public key and the proof JWT. Encrypting it prevents any network intermediary from observing or replaying these values.

Compact JWE Structure

Compact serialization is defined in RFC 7516 §3.1:

With ECDH-ES, the Content Encryption Key is derived directly from the key agreement and is never transmitted. The <base64url(Encrypted Key)> segment is always an empty string in the serialized compact JWE.

JWE Header
{
  "alg": "ECDH-ES",
  "enc": "A256GCM",
  "kid": "<kid>",
  "cty": "application/json",
  "epk": {
    "kty": "EC",
    "crv": "P-256",
    "x": "<sender_ephemeral_x>",
    "y": "<sender_ephemeral_y>"
  }
}
Field Description
alg Key agreement algorithm: ECDH-ES
enc Content encryption algorithm: A256GCM
kid Identifies the issuer encryption key to use
cty Content type of the plaintext: application/json
epk Sender's ephemeral public key for the ECDH-ES shared secret derivation
Issuer Encryption Public Key

Read the key from the signed issuer metadata at credential_request_encryption.jwks.keys. Look for the entry with "use": "enc":

{
  "kty": "EC",
  "crv": "P-256",
  "alg": "ECDH-ES",
  "use": "enc",
  "kid": "<kid>",
  "x": "<x_coordinate>",
  "y": "<y_coordinate>"
}
Decrypted Payload — Credential Request JSON

The plaintext inside the JWE is an OID4VCI credential request extended with credential_response_encryption. The current OID4VCI 1.0 structure uses proofs, where each proof type maps to an array of proof values:

{
  "format": "mso_mdoc",
  "doctype": "eu.europa.ec.av.1",
  "proofs": {
    "<proof_type>": [
      "<proof_value>"
    ]
  },
  "credential_response_encryption": {
    "jwk": {
      "kty": "EC",
      "crv": "P-256",
      "alg": "ECDH-ES",
      "use": "enc",
      "kid": "<response_encryption_key_id>",
      "x": "<x_coordinate>",
      "y": "<y_coordinate>"
    },
    "alg": "ECDH-ES",
    "enc": "A256GCM"
  }
}
Field Description
format Requested credential format: mso_mdoc
doctype mdoc document type: eu.europa.ec.av.1
proofs Proof-of-possession object demonstrating control of the binding key. Each selected proof type maps to one or more proof values. See Proof Types below.
credential_response_encryption Instructs the issuer to encrypt the response using the provided public key

Proof Types

The issuer accepts three proof types for eu.europa.ec.av.1. The wallet selects one and includes the corresponding key and value in the proofs object.

proof_type Standard Signing key Description
jwt OID4VCI 1.0 Credential Binding Key JWT signed by the Credential Binding Key; the issuer binds the credential to this key
attestation OID4VCI 1.0 Credential Binding Key Attestation JWT from a trusted service that attests the Credential Binding Key
android_keystore_attestation Google / Android Digital Credentials proof type Credential Binding Key Android Keystore attestation chain proving the Credential Binding Key is hardware-backed

Proof Type: jwt

Defined in OID4VCI 1.0 Appendix F.1. Refer to the specification for the full structure.

Integration-specific values:

Claim / Field Required value in this integration
aud The credential_issuer value from the issuer metadata
nonce The c_nonce value from the nonce response body (Step 5)

Proof Type: attestation

Defined in OID4VCI 1.0 Appendix F.3. Refer to the specification for the full structure.

Integration-specific values:

Claim / Field Required value in this integration
aud The credential_issuer value from the issuer metadata
nonce The c_nonce value from the nonce response body (Step 5)

Proof Type: android_keystore_attestation

This proof type is defined by Google's Android Digital Credentials guidance and is accepted by France Identité for this integration. For the official Android structure and validation guidance, see Android credential issuer — Keystore attestation.

{
  "proofs": {
    "android_keystore_attestation": [
      "<android_keystore_attestation_jwt>"
    ]
  }
}
Credential Response Encryption Key

Including credential_response_encryption in the request is optional per the issuer metadata (encryption_required: false), but strongly recommended — omitting it means the credential is returned in plaintext, exposing the bound key material to any intermediary.

The wallet generates a fresh EC P-256 key pair for each issuance session. The public key is sent in credential_response_encryption.jwk; the private key is retained in memory to decrypt the response. Discard the private key after decryption.

Example public key:

{
  "kty": "EC",
  "crv": "P-256",
  "alg": "ECDH-ES",
  "use": "enc",
  "kid": "response_encryption_test_key",
  "x": "TtTXFg3Dx0-TdOktImr31-bF12Ev9JIGszduZODq5g8",
  "y": "714EGSZv5ToUKVwq90UTmU8eja8xJI-u9OfU97k4wq4"
}

Response

The issuer returns the credential as a compact JWE encrypted with the wallet's credential_response_encryption public key.

HTTP/1.1 200 OK
Content-Type: application/jwt

<compact_jwe_serialization>

Credential Response JWE Header

{
  "alg": "ECDH-ES",
  "enc": "A256GCM",
  "kid": "<response_encryption_key_id>",
  "epk": {
    "kty": "EC",
    "crv": "P-256",
    "x": "<issuer_ephemeral_x>",
    "y": "<issuer_ephemeral_y>"
  }
}

The kid matches the key ID the wallet provided in credential_response_encryption.jwk. Decrypt using the corresponding private key.

Decrypted Credential

After decryption, the payload contains the issued mso_mdoc credential. For OID4VCI mso_mdoc, the credential value is the base64url-encoded CBOR IssuerSigned structure defined by ISO 18013-5. At issuance time there is no presentation-time DeviceSigned component; the wallet produces DeviceSigned only later during presentation.

Credential Response Payload
{
  "credentials": [
    {
      "credential": "<base64url(CBOR IssuerSigned)>"
    }
  ]
}
IssuerSigned Structure
IssuerSigned {
  nameSpaces: {
    "eu.europa.ec.av.1": [
      IssuerSignedItem { digestID, random, elementIdentifier: "age_over_18", elementValue: true },
      IssuerSignedItem { digestID, random, elementIdentifier: "age_over_21", elementValue: true },
      IssuerSignedItem { digestID, random, elementIdentifier: "issuing_country", elementValue: "FR" },
      IssuerSignedItem { digestID, random, elementIdentifier: "expiry_date", elementValue: <tdate> }
    ]
  }
  issuerAuth: <COSE_Sign1 containing the MSO>
}
Mobile Security Object (MSO)

The MSO is the issuer-signed CBOR structure embedded in issuerAuth. It contains the digest of each data element (allowing selective disclosure during presentation) and the device key binding.

MSO Field Description
version MSO version string (e.g. "1.0")
digestAlgorithm Digest algorithm used for element hashes: SHA-256
valueDigests Map from namespace to map of digestID → SHA-256(random \|\| elementValue) — one entry per data element
deviceKeyInfo.deviceKey The Credential Binding Key public key as a COSE_Key, extracted by the issuer from the proof submitted in the credential request
docType Document type: eu.europa.ec.av.1
validityInfo.signed Timestamp when the MSO was signed
validityInfo.validFrom Credential validity start
validityInfo.validUntil Credential validity end
Data Elements

The eu.europa.ec.av.1 credential type defines the following data elements:

Element Identifier Type Example Value Description
age_over_18 boolean true Whether the holder is 18 years of age or older
age_over_21 boolean true Whether the holder is 21 years of age or older
issuing_country string "FR" ISO 3166-1 alpha-2 country code of the issuing authority
issuing_jurisdiction string "Gironde" Jurisdiction within the issuing country (e.g. department or region)
issuing_authority string "France" Name of the authority that issued the credential
issue_date date/time "2018-08-09T00:00:00Z" Date the credential was issued
expiry_date date/time issuance date + 3 months Credential expiry date

The wallet MUST parse the actual IssuerSigned.nameSpaces content rather than assuming a fixed set — future versions of the credential profile may add or remove elements.

Each element is individually selectively disclosable during presentation — the wallet can reveal a subset without exposing the others.

The wallet verifies the issuer's COSE_Sign1 signature over the MSO before storing the credential, validates the issuer certificate chain against the France Identité IACA for the current environment, and verifies that deviceKeyInfo.deviceKey matches the submitted Credential Binding Key.


Error Handling

Error Response Format

All error responses follow the standard OAuth2 JSON envelope (RFC 6749 §5.2):

HTTP/1.1 <status_code>
Content-Type: application/json

{
  "error": "<error_code>",
  "error_description": "<human_readable_description>"
}

Error Codes

HTTP Status Error Code Likely Cause Recommended Action
400 invalid_request Malformed request body or missing required parameter Inspect the request and fix the parameter
400 invalid_grant pre-authorized_code is expired, already used, or invalid Restart the flow from Step 1
400 invalid_proof Proof is invalid — wrong nonce, wrong audience, bad signature, or invalid certificate chain Verify the c_nonce and re-generate the proof
400 use_dpop_nonce The DPoP JWT nonce claim is missing or stale See DPoP Nonce Retry
401 invalid_token Access token is invalid or was not issued for this client Request a new token (Step 4)
401 expired_token Access token has expired Request a new token (Step 4)
400 unsupported_credential_type The requested credential_configuration_id is not supported Check the issuer metadata for supported configurations
400 unsupported_format The requested credential format is not supported Use mso_mdoc

DPoP Nonce Retry

If the server rejects a request with use_dpop_nonce, it means the DPoP JWT's nonce claim is missing or no longer valid. The server includes a fresh DPoP-Nonce header in the error response.

HTTP/1.1 400 Bad Request
Content-Type: application/json
DPoP-Nonce: <fresh_dpop_nonce>

{
  "error": "use_dpop_nonce",
  "error_description": "Authorization server requires nonce in DPoP proof"
}

The wallet MUST: 1. Extract the new DPoP-Nonce from the error response header. 2. Regenerate the DPoP JWT using the new nonce value. 3. Retry the original request once with the updated DPoP JWT.

Do not restart the entire flow — the pre-authorized_code and access_token remain valid.


Security Considerations

The calling wallet is the same application throughout the flow — it sends the deeplink, registers as a Credential Manager provider, and performs the VCI exchanges. All obligations below apply to the single wallet implementation.

The wallet MUST:

  • Validate the origin of the incoming Credential Manager request and reject requests from unexpected callers
  • Retrieve and validate issuer metadata before initiating any network exchange
  • Use HTTPS for all network exchanges
  • Treat pre-authorized_code and access_token as secrets — never log them in plaintext
  • Generate a fresh key pair for credential_response_encryption per issuance session and discard the private key after decryption
  • Bind the credential to the wallet's key (via the device key in the MSO) and validate this binding after decryption
  • Verify the issuer's COSE_Sign1 signature on the issued MSO and validate the document signer certificate chain against the France Identité IACA for the active environment
  • Prevent replay attacks — validate c_nonce freshness and enforce jti uniqueness in DPoP JWTs
  • Validate certificate chains in x5c before trusting a Client Attestation
  • Validate Android Keystore attestation chains, roots, revocation status, and hardware/StrongBox security-level claims before relying on android_keystore_attestation
  • Store issued credentials in encrypted storage

The wallet SHOULD:

  • Verify that the France Identité app is installed and authentic before firing the deeplink
  • Avoid leaking pre-authorized_code values through logs, logcat, or IPC
  • Store the Credential Binding Key in the Android Keystore with hardware-backed protection; use StrongBox if available
  • Discard the Ephemeral Session Key after each issuance session completes or fails

Android Integration Notes

Client Registration and Onboarding

Registration and onboarding for third-party wallet implementations is TBD. Contact the France Identité team to initiate the process.

The following information will be required or assigned:

Item Direction Description
client_id Assigned by issuer Stable identifier used as sub in the Client Attestation and iss in the Attestation PoP
Certificate chain (x5c) Issued to wallet Used to sign the OAuth-Client-Attestation JWT
wallet_name Provided by wallet Display name shown to users
wallet_link Provided by wallet URL to the wallet's store listing or information page

Credential Manager Integration

See Step 2 — Credential Manager Request for the provider registration link and payload structure. The wallet does not need a custom intent filter to receive the credential request — delivery is handled through the Credential Manager service binding.

The wallet is the sender of the deeplink. No intent filter is required on the wallet side. The wallet constructs the France Identité deeplink URL and fires an ACTION_VIEW intent as described in Step 1 — Deeplink Invocation.


Out of Scope

The following OID4VCI features are not part of the current integration:

  • Multiple credential configurations in a single request
  • Deferred issuance
  • Authorization code flow
  • Batch credential issuance
  • DID-based subject identifiers
  • Credential refresh
  • Credential status validation

Playground Trace Examples

The following examples are extracted from a playground issuance session on 2026-05-28. They are intended for implementers who need to compare their own traces against a known-good flow.

Opaque bearer values, compact JWTs, and compact JWEs are shortened where they are not useful to read inline. The credential response encryption private key is included only because it belongs to this expired playground trace and was used to decrypt that session response. Do not reuse it in an implementation.

Annotated VCI flow trace
{
  "challenge": {
    "request": {
      "timestamp": "2026-05-28T16:23:41.488Z",
      "method": "POST",
      "url": "/france-titres/oid4vci-16-2-13/challenge"
    },
    "response": {
      "timestamp": "2026-05-28T16:23:41.510Z",
      "status": 200,
      "body": {
        "attestation_challenge": "hEOhASagV4JQ0Uqc0Q5gryefi5ScjgJKWxpqGG-RWEACBR8szWiB9fya-KkGAyEbiWBnxo5XSumw3Zj3eZZpek8j7-DWMOZfPYkAsHFczNzjoikLN3tSc2Lwa0-VEndA"
      }
    }
  },
  "token": {
    "request": {
      "timestamp": "2026-05-28T16:23:41.726Z",
      "method": "POST",
      "url": "/france-titres/oid4vci-16-2-13/token",
      "headers": {
        "content-type": "application/x-www-form-urlencoded",
        "dpop": "<compact dpop+jwt>",
        "oauth-client-attestation": "<compact oauth-client-attestation+jwt>",
        "oauth-client-attestation-pop": "<compact oauth-client-attestation-pop+jwt>"
      },
      "body": {
        "grant_type": "urn:ietf:params:oauth:grant-type:pre-authorized_code",
        "pre-authorized_code": "w9HRltH_WKYtHHf2RttR_7U2-YcQ5fX7iokppzyZxZQ"
      }
    },
    "decoded_dpop": {
      "header": {
        "typ": "dpop+jwt",
        "alg": "ES256",
        "jwk": {
          "kty": "EC",
          "crv": "P-256",
          "kid": "1_6d99a524-75a4-489e-adfa-5a9bee92d049_fb0724f5-a5da-407c-ba6a-d84ecef67f20",
          "x": "tAHecxu06n_un6zR1gXJ0jasgUVRbpB-c0El3RUCqDs",
          "y": "NqQBi7iXlA1BmPjKZLxa28moF68e_6RUEXbCCIxTvQU"
        }
      },
      "payload": {
        "htm": "POST",
        "htu": "https://api.playground.france-identite.gouv.fr/poc/idakto/openid4vci/v2/token",
        "iat": 1779985421,
        "nonce": "hEOhASagV4JQNCbK1OXwxcPsLnHIULpEBxpqGG-RWEDuvKrd7iq53h9LqCXarknGzYioV-Me46vk54bnSpdPRXV5hnbIzF1IdTzj5O1SpGQaCkROqOrToNVK9vuseVQa",
        "jti": "40821509-1715-4b9a-b49c-8fd632dc5bcf"
      }
    },
    "response": {
      "timestamp": "2026-05-28T16:23:41.752Z",
      "status": 200,
      "body": {
        "access_token": "yRP5dc9cdfx4c_Ne8DZP9Uj4htaxUWJ05EFMFRWHIxA",
        "expires_in": 896,
        "token_type": "DPoP"
      }
    }
  },
  "nonce": {
    "request": {
      "timestamp": "2026-05-28T16:23:41.853Z",
      "method": "POST",
      "url": "/france-titres/oid4vci-16-2-13/nonce"
    },
    "response": {
      "timestamp": "2026-05-28T16:23:41.861Z",
      "status": 200,
      "body": {
        "c_nonce": "hEOhASagV4JQ_PV8-80xj556CJFQG4QEpBpqGG-RWECI21HpF8kSeufcFhLckHU7i7eE65ixQvgnTDjLPhnXA1n6KjLLiF1VtIwUqiLWrSXSBnDLOnz17UfC5nwwqPfV"
      }
    }
  },
  "credential": {
    "request": {
      "timestamp": "2026-05-28T16:23:42.191Z",
      "method": "POST",
      "url": "/france-titres/oid4vci-16-2-13/credential",
      "headers": {
        "authorization": "DPoP yRP5dc9cdfx4c_Ne8DZP9Uj4htaxUWJ05EFMFRWHIxA",
        "content-type": "application/jwt; charset=utf-8",
        "dpop": "<compact dpop+jwt>"
      },
      "body": "<encrypted compact JWE credential request>"
    },
    "decoded_dpop": {
      "payload": {
        "htm": "POST",
        "htu": "https://api.playground.france-identite.gouv.fr/poc/idakto/openid4vci/v2/credential",
        "ath": "8_VZ0r-vYFbfUwe79g0V6xhXXLmJps0D0lT6H8V8758",
        "iat": 1779985421,
        "jti": "7ffb5b21-7900-45aa-afa3-2e7799f137ac"
      }
    },
    "request_jwe_header": {
      "alg": "ECDH-ES",
      "enc": "A256GCM",
      "cty": "application/json",
      "kid": "ecdsap256",
      "epk": {
        "kty": "EC",
        "crv": "P-256",
        "x": "dACJxQoZdqqHW7Ngj-tFiNM33sCNbXy9EUjV-YnTHCo",
        "y": "rr8mXFB9ie0MmbMAsBhAbJV-H2QcbZNk4BHAKaySot0"
      }
    },
    "response": {
      "timestamp": "2026-05-28T16:23:42.267Z",
      "status": 200,
      "headers": {
        "content-type": "application/jwt"
      },
      "body": "<encrypted compact JWE credential response>"
    },
    "response_jwe_header": {
      "alg": "ECDH-ES",
      "enc": "A256GCM",
      "kid": "response_encryption_test_key",
      "epk": {
        "kty": "EC",
        "crv": "P-256",
        "x": "cLoReAr4360JdDZEDevgNTpEfr3HW7rlWbGk7nTPJ1I",
        "y": "LcxsJtrj20JcIcHZmP3WaLVuLTYOCn09nH85C_CAFdw"
      }
    }
  },
  "credential_response_encryption_public_key": {
    "kty": "EC",
    "crv": "P-256",
    "x": "TtTXFg3Dx0-TdOktImr31-bF12Ev9JIGszduZODq5g8",
    "y": "714EGSZv5ToUKVwq90UTmU8eja8xJI-u9OfU97k4wq4",
    "alg": "ECDH-ES",
    "kid": "response_encryption_test_key",
    "use": "enc"
  },
  "credential_response_encryption_private_key": {
    "kty": "EC",
    "crv": "P-256",
    "x": "TtTXFg3Dx0-TdOktImr31-bF12Ev9JIGszduZODq5g8",
    "y": "714EGSZv5ToUKVwq90UTmU8eja8xJI-u9OfU97k4wq4",
    "d": "2_uG1rJ3UgV6EhJHhqpIfKER3Fr2QECOIuNe6Uzdcdo"
  },
  "credential_request_encryption_public_key": {
    "kty": "EC",
    "crv": "P-256",
    "x": "cN9gxxCvgLIU7mmNxE1U4nj2mW6dLgGv8txj5NVvRB8",
    "y": "K_ATXIVyzRNAa9hCzJ5icXmotjXMLPjrN66OOMr9C9A",
    "alg": "ECDH-ES",
    "kid": "ecdsap256",
    "use": "enc"
  }
}
Decoded playground eu.europa.ec.av.1 credential
{
  "docType": "eu.europa.ec.av.1",
  "version": "1.0",
  "nameSpaces": {
    "eu.europa.ec.av.1": {
      "issuing_jurisdiction": "Gironde",
      "age_over_18": true,
      "age_over_21": true,
      "expiry_date": "2026-01-24T00:00:00Z",
      "issue_date": "2018-08-09T00:00:00Z",
      "issuing_authority": "France",
      "issuing_country": "FR"
    }
  },
  "issuerAuth": {
    "digestAlgorithm": "SHA-256",
    "validityInfo": {
      "signed": "2026-05-26T11:25:41Z",
      "validFrom": "2026-05-26T11:25:41Z",
      "validUntil": "2026-08-25T17:25:41Z"
    },
    "documentSignerCertificate": {
      "subject": "CN=DS, OU=0002 1300032621, O=Agence Nationale des Titres Sécurisés, C=FR",
      "issuer": "CN=Autorité de Certification France Identité IACA, OU=POUR QUALIFICATION UNIQUEMENT, OU=0002 1300032621, O=Agence Nationale des Titres Sécurisés, C=FR",
      "notBefore": "2026-03-04T13:58:07Z",
      "notAfter": "2027-06-04T12:58:07Z"
    },
    "deviceKeyInfo": {
      "deviceKey": {
        "kty": "EC2",
        "crv": "P-256",
        "x": "n7N9arHtxn5qvVwaRd1VIGUJ0JvxVNgERpEtO_Ey-TA",
        "y": "4jbQbUwrAoujuEQqqx6TQi8V1G0j5YrHiJDVGfzZZns"
      }
    }
  },
  "rawNameSpaceItems": {
    "format": "CBOR tag 24 encoded issuer-signed items",
    "count": 7
  }
}

Playground Certificates

Obtain the playground IACA certificate from the France Identité EUDIW Unfold Playground documentation. The wallet must configure this certificate as the trust anchor for MSO signature validation before verifying any issued credential.


References

Reference Description
OpenID for Verifiable Credential Issuance 1.0 Core protocol specification
OAuth 2.0 (RFC 6749) Authorization framework
RFC 7516 — JWE JSON Web Encryption — compact serialization format used for credential request and response encryption
RFC 7517 — JWK JSON Web Key — format used for DPoP jwk, Client Attestation cnf.jwk, and encryption keys
RFC 9449 — DPoP Demonstrating Proof of Possession
OAuth 2.0 Attestation-Based Client Authentication OAuth-Client-Attestation and PoP header specification
ISO 18013-5 — mDL / mso_mdoc CBOR-based mobile document format used for the issued credential
Android Credential Manager — Digital Credentials provider guide Android API for credential issuance mediation — provider implementation guide
Android credential issuer — Keystore attestation Official Android issuer guidance for android_keystore_attestation
France Identité — EUDIW Unfold Playground France Identité playground profile, age credential data elements, and environment certificates
EUDI Architecture and Reference Framework EUDI ARF — defines eu.europa.ec.av.1 credential type