vck
VC-K is a comprehensive Kotlin Multiplatform library for implementing digital identity solutions, with full support for modern credential standards and protocols. It enables developers to build wallet applications, verifier systems, and issuer services using a single, consistent API across multiple platforms.
Designed with developers in mind, VC-K provides a flexible, modular architecture that simplifies the implementation of complex identity workflows while maintaining compatibility with the broader digital identity ecosystem, including the EU Digital Identity Wallet (EUDI Wallet).
Architecture
VC-K is split into published Kotlin Multiplatform modules that separate wire models, credential behavior, OpenID protocol behavior, and Ktor transport integration.
For a contributor-oriented guide to module boundaries, implementation entry points, and refactoring strategy, see ARCHITECTURE.md. For setup, building, testing, and publishing, see DEVELOPMENT.md.
Features
VC-K implements multiple credential formats to ensure maximum interoperability:
- W3C Verifiable Credentials Data Model: Rudimentary implementation of the W3C VC Data Model (skipping everything around DIDs)
- SD-JWT VC: Privacy-preserving credential format with selective disclosure capabilities, see SD-JWT-based Verifiable Credentials (including type metadata and JWT VC issuer metadata), built on Selective Disclosure for JSON Web Tokens (RFC 9901) with key binding JWTs and nested disclosures.
- ISO 18013-5 and 18013-7: ISO standard defining Mobile Driving Licence and its generalization mDoc credentials as a CBOR-based credential format
When using the plain JWT representation, the W3C VC credentialSubject is handled as JsonElement. For ISO mDoc claims see IssuerSignedItems and related classes like Document and MobileSecurityObject. For SD-JWT claims see SelectiveDisclosureItem and SdJwtSigned.
Other libraries implementing credential schemes may call LibraryInitializer.registerExtensionLibrary() to register with this library. See our implementation of the EU PID credential and our implementation of the Mobile Driving Licence for examples. We also maintain a comprehensive list of all credentials powered by this library.
OpenID Protocol Implementations
VC-K provides full implementations of the OpenID protocol family for credential issuance and presentation:
OpenID4VCI (OpenID for Verifiable Credential Issuance): Standards-compliant credential issuance flows (OpenID for VCI), including:
- Pre-authorized code grants
- Authorization code flow
- Credential selection with authorization details and scopes
- Pushed authorization requests, DPoP, and attestation-based client authentication, e.g. with a Wallet Instance Attestation
- Proofs of possession with key attestations, and encrypted credential requests and responses
- Signed credential issuer metadata, validated against the access and registration certificates of the provider
- See classes
OpenId4VciClient,OpenId4VciProtocolClient,OpenId4VciKtorClientandOpenId4VciServer
OpenID4VP (OpenID for Verifiable Presentations): Complete holder and verifier implementation (OpenID for VP), supporting:
- Same device and cross-device flows
- Response modes:
direct_post,direct_post.jwt,dc_apianddc_api.jwt - Request objects by value or reference, signed and/or encrypted
- Verifier attestations
verifier_infoattestations with profile-defined formats (for exampleregistration_cert)- Signed and/or encrypted responses
- Digital Credential Query Language (DCQL)
- ISO DeviceRequest presentation for direct ISO 18013-5 and DC API transports
- See classes
OpenId4VpVerifier,OpenId4VpHolder,OpenId4VpProtocolClientandOpenId4VpKtorHolder
The protocol clients in vck-openid (OAuth2ProtocolClient, OpenId4VciProtocolClient, OpenId4VpProtocolClient)
never send requests themselves: each call returns an HttpExchange to send with any HTTP stack, while the clients in
vck-openid-ktor send them with Ktor. Server-side, results convert to a PreparedHttpResponse to write out unchanged.
Validation and Trust
Credentials, presentations, relying parties, and credential issuers are validated by one API in the package
at.asitplus.wallet.lib.validation: CredentialValidator, RelyingPartyValidator, and IssuerMetadataValidator
return a ValidationReport with every check (passed, failed with its cause, blocked, or not applicable), whose
decision follows from the ValidationPolicy you choose:
- Trust: issuers, status list signers, and relying parties are trusted through anchors configured per credential
type, e.g. filtered from the EU Lists of Trusted Entities (LoTE, ETSI TS 119 602) with
LoTEFilterService - Status: Token Status List entries are resolved with a
StatusValidatorand yourStatusListTokenResolver - Timeliness, holder binding, and required claims, e.g. the claims the type metadata of a credential mark as mandatory
- Registration: access certificates (WRPAC) and registration certificates (WRPRC, ETSI TS 119 475) of relying parties and credential issuers, including whether a relying party is registered for the data it requests
A relaxed policy lets a wallet show a report instead of rejecting, e.g. to warn about an untrusted relying party, or to store a credential of an untrusted issuer after the user accepts it.
EUDI Wallet Compatibility
VC-K is built for the EU Digital Identity Wallet (EUDI Wallet) ecosystem:
- Implements the credential formats (SD-JWT VC, ISO mDoc) and protocols (OpenID4VCI, OpenID4VP, ISO/IEC 18013-5 and 18013-7, Digital Credentials API) of the ARF (Architecture and Reference Framework)
- Validates trust and registration as the EUDI Wallet ecosystem defines it: Lists of Trusted Entities (ETSI TS 119 602), access and registration certificates of relying parties and credential issuers (ETSI TS 119 475), and Token Status Lists
- Compatible with the EUDI Wallet Reference Implementation
Interoperability
VC-K has been tested against many wallet, issuer, and verifier implementations across the digital identity ecosystem:
- EUDI Wallets Launchpad 2026 (October 2026, Brussels): With VC-K as their base, A-SIT Plus' wallet, issuer, and relying party passed a large number of test cases.
- EUDI Wallets Launchpad 2025 (December 2025, Brussels): With VC-K as their base, A-SIT Plus' wallet, issuer, and relying party passed a large number of test cases at the European Commission's first interoperability testing event for Member State wallets.
- Interop Events of POTENTIAL: VC-K has been successfully tested with multiple wallet implementations, issuer systems, and verifier platforms in cross-vendor credential exchange scenarios.
Usage
VC-K uses a modular structure to separate concerns. Hence, depending on the use cases you want to cover, you will need different artifacts:
| Artefact | Info |
|---|---|
vck |
VC-K base functionality. Contains business logic for creating, issuing, presenting, and verifying credentials, and the validation API for credentials, presentations, relying parties, and credential issuers. |
vck-openid |
OAuth 2.0, OpenID4VCI, and OpenID4VP for wallets, issuers, and verifiers, with protocol clients returning HttpExchanges for any HTTP stack, and servers returning PreparedHttpResponses. |
vck-openid-ktor |
Contains ktor-based OAuth 2.0 and OpenID4VCI client and OpenID4VP holder implementations. |
vck-longfellow |
Implementation of a registerable backend for ISO mDoc zero-knowledge proofs using LongfellowZk |
openid-data-classes |
OpenID data classes. Only depends on csc-data-classes, etsi-data-classes and rfc3986-uri-syntax and can hence be used independently of VC-K! |
csc-data-classes |
CSC API and data model data classes. Does not depend on any other vck artefact and can hence be used independently of VC-K! |
etsi-data-classes |
ETSI TS 119 602 (LoTE) and ETSI TS 119 475 (WRPAC, WRPRC) data classes. Only depends on rfc3986-uri-syntax and can hence be used independently of VC-K! |
rfc3986-uri-syntax |
RFC 3986 URI Syntax data classes. Does not depend on any other vck artefact and can hence be used independently of VC-K! |
sd-jwt-type-metadata |
SD-JWT VC type metadata data classes. Only depends on rfc3986-uri-syntax and can hence be used independently of VC-K! |
Simply declare the desired dependency to get going. This will usually be one of:
implementation("at.asitplus.wallet:vck:$version")
implementation("at.asitplus.wallet:vck-openid:$version")
implementation("at.asitplus.wallet:vck-openid-ktor:$version")
Everything else (serialization, crypto through Signum, …) will be taken care of.
Therefore, do not manually add serialization dependencies! In case you are using this project in a codebase with dependencies on kotlinx-serialization, please use the vck-versionCatalog artefact to keep versions in sync.
As discovered in #226, using the deprecated io.spring.dependency-management will cause issues.
The actual credentials are provided as discrete artefacts and are maintained separately over here. It is fine to add credentials and VC-K to as project dependencies, e. g., to use a version of VC-K that is more recent than the one a certain credentials depends on.
OpenID4VP presentation
Use OpenId4VpVerifier in the verifier/relying party and OpenId4VpHolder in the wallet. The verifier creates the
authorization request, sends the resulting URL to the wallet as a QR code or deep link, then validates the URL or POST
body returned by the wallet.
The verifier validates every presentation with a CredentialValidator: configure it with the trust anchors per
credential type, e.g. from the trust lists of the EUDI Wallet ecosystem, and a status list token resolver. By default,
issuers have to be trusted, a status is checked, and credentials have to be valid, so without anchors every
presentation is rejected.
val credentialValidator = CredentialValidator(
trustAnchors = CredentialTrustScopes(
loteFilterService.credentialTrustScope(
credentialIdentifiers = setOf(EuPidSdJwtScheme.sdJwtType),
profile = LoteProfile.PID,
source = "PID providers",
lists = { authenticatedPidProviderLists() },
),
),
statusValidator = StatusValidator(StatusListTokenResolver { uri -> fetchStatusListToken(uri) }),
)
val verifier = OpenId4VpVerifier(
keyMaterial = verifierKeyMaterial,
clientIdScheme = ClientIdScheme.RedirectUri("https://rp.example/callback"),
credentialValidator = credentialValidator,
)
val request = verifier.createAuthnRequest(
requestOptions = OpenId4VpRequestOptions(
presentationRequest = CredentialPresentationRequestBuilder(
credentials = setOf(
RequestOptionsCredential(
credentialScheme = EuPidSdJwtScheme,
representation = SD_JWT,
attributePaths = setOf(
DCQLClaimsPathPointer("given_name"),
DCQLClaimsPathPointer("family_name"),
),
)
)
).toDCQLRequest(),
),
creationOptions = OpenId4VpVerifier.CreationOptions.Query("openid4vp://authorize"),
).getOrThrow()
// Show request.url as QR code or open it as a wallet deep link.
// For RequestByReference/SignedRequestByReference also serve the request object at your request_uri, with the
// parameters the wallet may have posted: request.loadRequestObjectHttpResponse(walletParameters).getOrThrow()
val response = verifier.validateAuthnResponse(walletRedirectUrlOrDirectPostBody).getOrElse {
// Not processed, e.g. malformed or for an unknown state: answer the wallet's POST with a non-success status
return badRequest(it)
}
when (response) {
// The wallet presented: the report tells whether the response is accepted, with every check of every presentation
is AuthnResponseResult.Success -> if (response.validationReport.decision == ValidationDecision.ACCEPTED) {
response.presentationValidations.values.flatten().mapNotNull { it.presentation }
} else showWhy(response.validationReport)
// The wallet declined, e.g. with access_denied; escape the wallet's error_description when displaying it
is AuthnResponseResult.Error -> response.error
}
// Either one ends the request. For direct_post and direct_post.jwt, answer the wallet's POST of a processed response,
// optionally with a redirect_uri for the wallet:
val answer = directPostHttpResponse(redirectUri = "https://rp.example/result#response_code=$freshSecret")
Each PreparedHttpResponse is written out unchanged: its status, every header in headers, and body.
On the wallet side, use the two-step API when the user must review and choose credentials. To establish trust in
the relying party sending a request, pass a relyingPartyValidator with RelyingPartyTrustAnchors: WRPAC anchors,
also for x509_san_dns and x509_hash, WRPRC anchors, trusted attesters for verifier_attestation, and a registry
of known clients for pre-registered. startAuthorizationResponsePreparation(input, policy) returns the report of
the request next to the preparation state, also for a rejected request, so the wallet can show why. With
ValidationPolicy.registration set to RegistrationPolicy.ValidateIfPresent, relying parties without a WRPRC, e.g.
mdoc readers outside the EUDI Wallet ecosystem, are accepted through their access certificate alone.
OpenId4VpKtorHolder from vck-openid-ktor sends the requests to the verifier with Ktor, i.e. it fetches a request
object passed by reference and posts the response. It uses a copy of your HttpClient that doesn't follow redirects.
Its plugins must not re-send requests or react to error statuses, because a re-sent response submits the presentation
twice. All other parameters are those of OpenId4VpHolder:
val wallet = OpenId4VpKtorHolder(
httpClient = httpClient, // your app's Ktor HttpClient
keyMaterial = holderKeyMaterial,
holder = holderAgent,
relyingPartyValidator = RelyingPartyValidator(
RelyingPartyTrustAnchors(accessCertificates = wrpacAnchors, registrationCertificates = wrprcAnchors),
),
)
// There is no default policy: choose which checks have to pass
val relyingPartyPolicy = ValidationPolicy(
trust = TrustPolicy.RequireAuthorizedSigner,
status = StatusPolicy.ValidateIfPresent(signerTrust = TrustPolicy.RequireAuthorizedSigner),
timeLeeway = 5.minutes,
requireTimeliness = true,
)
val validation = wallet.startAuthorizationResponsePreparation(requestUrlFromQrOrDeepLink, relyingPartyPolicy)
.getOrThrow()
if (validation.validationReport.decision == ValidationDecision.REJECTED) {
// Show why, e.g. validation.validationReport.firstRejection(), and what the relying party claims to be, i.e.
// validation.relyingParty?.untrustedRegistrations; continue only if the user decides to
}
val preparation = validation.preparationState
val matches = wallet.getMatchingCredentials(preparation).getOrThrow()
// Show preparation.verifierInfo and matches to the user, then continue after consent.
when (val result = wallet.finalizeAuthorizationResponse(preparation).getOrThrow()) {
// The URL carrying the response, or the verifier's redirect_uri after posting it (always an absolute https URI)
is OpenId4VpKtorHolder.AuthenticationSuccess -> result.redirectUri?.let { openBrowser(it) }
is OpenId4VpKtorHolder.AuthenticationForward -> returnToBrowserDcApi(result.authenticationResponseResult)
}
Without Ktor, OpenId4VpHolder creates the response, and OpenId4VpProtocolClient returns the HttpExchanges to
fetch the request object and to post the response. Send their requests with your HTTP stack, as execute shows for
OpenID4VCI:
val holder = OpenId4VpHolder(keyMaterial = holderKeyMaterial, holder = holderAgent)
val client = OpenId4VpProtocolClient(holder)
val preparation = execute(client.prepareAuthorizationResponse(requestUrlFromQrOrDeepLink))
val matches = holder.getMatchingCredentials(preparation).getOrThrow()
// Show preparation.verifierInfo and matches to the user, then continue after consent.
when (val authnResponse = holder.finalizeAuthorizationResponse(preparation).getOrThrow()) {
is AuthenticationResponseResult.Redirect -> openBrowser(authnResponse.url)
is AuthenticationResponseResult.Post -> execute(client.sendAuthorizationResponse(authnResponse))
?.let { openBrowser(it) }
is AuthenticationResponseResult.DcApi -> returnToBrowserDcApi(authnResponse)
}
OpenID4VCI credential issuance
Use OpenId4VciServer on the issuer service. Your HTTP framework only needs to expose the metadata, nonce, and
credential endpoints, forward request data into the protocol object, and write out the PreparedHttpResponse it
gets back unchanged (status, headers, body). Status codes and headers, e.g. Cache-Control: no-store, DPoP-Nonce
and WWW-Authenticate, are set by VC-K.
val credentialIssuer = OpenId4VciServer(
publicContext = "https://issuer.example",
credentialSchemes = setOf(EuPidSdJwtScheme),
authorizationService = authorizationServer,
issuer = issuerAgent,
keyMaterial = setOf(issuerKeyMaterial),
credentialEndpointPath = "/credential",
nonceEndpointPath = "/nonce",
)
// GET /.well-known/openid-credential-issuer, signed metadata if the Accept header asks for application/jwt
suspend fun issuerMetadata(acceptHeader: String?): PreparedHttpResponse =
credentialIssuer.metadataHttpResponse(acceptHeader).getOrThrow()
// POST /nonce
suspend fun nonce(): PreparedHttpResponse = credentialIssuer.nonceWithDpopNonce().getOrThrow().toHttpResponse()
// POST /credential
suspend fun credential(
authorizationHeader: String,
requestBody: String,
requestInfo: RequestInfo,
): PreparedHttpResponse = credentialIssuer.credential(
authorizationHeader = authorizationHeader,
params = OpenId4VciClient.CredentialRequest.parse(requestBody).getOrThrow(),
request = requestInfo,
credentialDataProvider = credentialDataProvider,
).fold(
onSuccess = { it.toHttpResponse() },
// 401 with WWW-Authenticate for token and DPoP errors, 400 for credential errors; other errors are yours
onFailure = { (it as? OAuth2Exception)?.toResourceServerHttpResponse(authorizationHeader) ?: throw it },
)
The authorization server works the same way: convert the results of SimpleAuthorizationService, e.g. of
parWithDpopNonce and tokenWithDpopNonce, with toHttpResponse(), and its errors with
OAuth2Exception.toHttpResponse().
On the wallet side, three layers build on each other:
OpenId4VciClientcreates credential requests (proofs with itsProofCreators, encryption) and decodes credential responses.OpenId4VciProtocolClientandOAuth2ProtocolClientadd everything HTTP, i.e. DPoP, client attestation, nonces, and retries, but never send requests themselves: each call returns anHttpExchange.OpenId4VciKtorClientsends those exchanges with Ktor and runs the complete flows. It uses a copy of yourHttpClientthat doesn't follow redirects. Its plugins must not re-send requests or react to error statuses, because DPoP proofs are single-use.
With Ktor, load the offer (or, without one, start with loadIssuerMetadata(issuerUrl, policy) and
startProvisioningWithAuthRequestReturningResult). Signed issuer metadata are preferred and validated with the
issuerMetadataValidator; received credentials are validated by the HolderAgent, configured with a
CredentialValidator, before they are stored:
val client = OpenId4VciKtorClient(
httpClient = httpClient, // your app's Ktor HttpClient, ideally with a persistent cookie storage
oauth2Client = OAuth2Client(clientId = walletClientId, redirectUrl = walletRedirectUrl),
vciClient = OpenId4VciClient(proofCreators = listOf(JwtProofCreator(holderKeyMaterial))),
// Optional: attestation-based client authentication, e.g. with a WIA; its key is also the DPoP key
clientAttestation = ClientAttestation(attestedKeyMaterial) { loadWalletInstanceAttestation(it) },
// Anchors of the providers of access certificates (WRPAC) and registration certificates (WRPRC)
issuerMetadataValidator = IssuerMetadataValidator(
accessCertificates = wrpacAnchors,
registrationCertificates = wrprcAnchors,
),
)
val offer = client.loadCredentialOffer(credentialOfferUrl).getOrThrow()
// LoadedIssuerMetadata.Signed carries the report of rejected metadata, Unsigned ones are not authenticated at all
val metadata = client.loadIssuerMetadata(offer.credentialIssuer, policy).getOrThrow()
val credentials = client.parseCredentialMetadata(metadata).getOrThrow()
val selectedCredential = credentials.first { it.credentialIdentifier in offer.configurationIds }
when (val result = client.loadCredentialWithOfferReturningResult(offer, selectedCredential).getOrThrow()) {
is CredentialIssuanceResult.OpenUrlForAuthnRequest -> {
storeProvisioningContext(result.context)
openBrowser(result.url)
}
is CredentialIssuanceResult.Success -> store(result)
}
// After the browser redirects back to the wallet app in the authorization-code flow:
store(client.resumeWithAuthCode(redirectUrl, loadProvisioningContext()).getOrThrow())
suspend fun store(result: CredentialIssuanceResult.Success) =
result.prepareCredentialStorage(holderAgent, policy).getOrThrow().forEach { prepared ->
if (holderAgent.storeAccepted(prepared).getOrThrow().storedEntry == null) {
// Rejected: show prepared.validation.report, e.g. of an untrusted issuer or missing required claims, and
// store it only after the user accepts it, as long as it is intact
if (prepared.validation.unacceptedCredential != null && userAccepts(prepared.validation.report))
holderAgent.storeAfterUserConsent(prepared).getOrThrow()
}
}
Without Ktor, send the requests of each exchange with your HTTP stack and pass the responses back; the KDoc of
OpenId4VciProtocolClient lists the order of calls in each flow:
suspend fun <T> execute(exchange: HttpExchange<T>): T {
var step = exchange.next().getOrThrow()
// send(): returns the response for every status code, without following redirects
while (step is HttpStep.Send) step = exchange.next(send(step.request.http)).getOrThrow()
return (step as HttpStep.Done).value
}
val vci = OpenId4VciProtocolClient(
vciClient = OpenId4VciClient(proofCreators = listOf(JwtProofCreator(holderKeyMaterial))),
oauth2Client = OAuth2ProtocolClient(oAuth2Client = OAuth2Client(clientId = walletClientId)),
issuerMetadataValidator = issuerMetadataValidator,
)
val offer = execute(vci.loadCredentialOffer(credentialOfferUrl))
val issuerMetadata = execute(vci.loadIssuerMetadata(offer.credentialIssuer, policy))
Decode credential responses with OpenId4VciClient.decodeCredentialResponse and validate each ReceivedCredential
with prepareCredentialStorage(holderAgent, scheme, policy, renewalInfo), then store it as above.
Credential validation
The ValidationPolicy has no defaults or presets: choose whether signers have to be authorized by trust anchors,
whether a status is checked, and whether an expired or not yet valid credential is rejected. Configure the trust
anchors per credential type and the status list token resolver once, in a CredentialValidator, and pass it to the
HolderAgent, OpenId4VpVerifier, or DcApiVerifier:
val credentialValidator = CredentialValidator(
trustAnchors = CredentialTrustScopes(/* one CredentialTrustScope per credential type, e.g. from a LoTE */),
statusValidator = StatusValidator(StatusListTokenResolver { uri -> fetchStatusListToken(uri) }),
)
val holderAgent = HolderAgent(keyMaterial = holderKeyMaterial, credentialValidator = credentialValidator)
val policy = ValidationPolicy(
trust = TrustPolicy.RequireAuthorizedSigner,
status = StatusPolicy.ValidateIfPresent(signerTrust = TrustPolicy.RequireAuthorizedSigner),
timeLeeway = 5.minutes,
requireTimeliness = true,
requiredClaims = RequiredClaimsPolicy.RequireMandatory,
)
Each ValidationReport lists its namedChecks with the Requirement the policy put on each, so that a report can be
displayed as it has been decided, including the checks a relaxed policy tolerates; firstRejection() tells why a
report has been rejected. The validators and agents that predate this API, e.g. Validator, VerifierAgent,
WrprcValidator, and TokenStatusValidator, are deprecated, see CHANGELOG.md.
Registering credential schemes
Credential schemes are derived from SD-JWT Type Metadata
documents and resolved through AttributeIndex. Register one or more CredentialMetadataRegistry instances once at
startup; on a lookup miss AttributeIndex consults them, builds the scheme, and caches it. Two registries coexist:
a StaticCredentialMetadataRegistry for documents bundled in code (offline, authoritative; preloaded so they win
on lookup), and a RemoteCredentialMetadataRegistry that fetches documents over HTTP for everything else. The
documents are hosted in credentials-collection.
val base = "https://raw.githubusercontent.com/a-sit-plus/credentials-collection/main"
// Bundled in code: EU PID (ISO), EU PID SD-JWT, mDL. The URL is the document's hosted copy (becomes schemaUri).
LibraryInitializer.registerCredentialMetadataRegistry(
StaticCredentialMetadataRegistry(
documentRegistry = SdJwtTypeMetadataDocumentRegistry(
EuPidSdJwtMetadataDocument, EuPidMetadataDocument, MobileDrivingLicenceMetadataDocument,
),
documentUrls = mapOf(
EuPidSdJwtMetadataDocument.first to EU_PID_SD_JWT_METADATA_URL,
EuPidMetadataDocument.first to EU_PID_METADATA_URL,
MobileDrivingLicenceMetadataDocument.first to MDL_METADATA_URL,
),
)
)
// Fetched on demand: add one `vct -> URL` entry per published document. SD-JWT resolves directly (identifier == vct);
// ISO mDoc has no direct vct fallback, so its docType must be aliased to the document's vct.
LibraryInitializer.registerCredentialMetadataRegistry(
RemoteCredentialMetadataRegistry(
httpClient = httpClient, // your app's Ktor HttpClient
clock = Clock.System,
documentUrls = mutableMapOf(
SdJwtVcType("urn:eudi:ehic:1") to "$base/ehic.json",
SdJwtVcType("eu.europa.ec.av.1") to "$base/age-verification.json",
),
aliases = mapOf(
CredentialMetadataLookup(ISO_MDOC, "eu.europa.ec.av.1") to SdJwtVcType("eu.europa.ec.av.1"),
),
)
)
ISO mDoc credentials with non-primitive values additionally need their CBOR/JSON value serializers registered from
code (e.g. LibraryInitializer.registerCredentialSerializers(EuPidJsonValueEncoder, EuPidItemValueSerializerMap));
schemes whose values are all primitive (such as the all-boolean age verification) need none.
Digital Credentials API (DC API)
DC API Wallet integration
The browser's Digital Credentials API can carry either OpenID4VP or ISO/IEC 18013-7 Annex C. Use DcApiHolder for
both. It returns a DcApiPreparationState that preserves the selected protocol across matching, consent, and
finalization, and a platform-independent DigitalCredentialInterface response.
The platform integration must first convert the selected request into RequestParametersFrom.DcApiRequest. For
serialized DigitalCredentialRequestOptions, use decodeDigitalCredentialRequestOptions() followed by
toRequestParametersFrom(...), supplying the protocol and trusted metadata returned by the platform matcher.
Platform object conversion, including Android Bundle conversion, remains in the wallet application.
val options = requestOptionsJson.decodeDigitalCredentialRequestOptions()
val request = options.toRequestParametersFrom(
selectedProtocol = platformSelection.protocol,
credentialIds = platformSelection.credentialIds,
callingOrigin = platformSelection.callingOrigin,
callingPackageName = platformSelection.callingPackageName,
)
val dcApiHolder = DcApiHolder(
keyMaterial = holderKeyMaterial,
holder = holderAgent,
relyingPartyValidator = relyingPartyValidator,
)
// Validates the relying party, e.g. its WRPAC and WRPRCs, see validation.validationReport and validation.relyingParty
val validation = dcApiHolder.startAuthorizationResponsePreparation(request, relyingPartyPolicy).getOrThrow()
val preparation = validation.preparationState
val matches = dcApiHolder.getMatchingCredentials(preparation).getOrThrow()
// Render preparation.presentationRequest and matches, then build the selected CredentialPresentation after consent.
val response = dcApiHolder.finalizeAuthorizationResponse(preparation, selectedPresentation).getOrThrow()
val androidResponseJson = response.toAndroidDcApiResponseJson()
// Annex C only:
val iosResponseBytes = response.toIosIsoMdocResponseBytes()
On iOS, IosDcApiMdocPreRequestSummary represents the system's pre-request disclosure summary without depending on
Apple frameworks. It can be converted for early matching with toDeviceRequest(). Once the full Annex C
request arrives, require summary.isConsistentWith(request.parameters.isoMdocRequest) before continuing so that the
final request cannot ask for different data than the system showed. The iOS response encoder accepts Annex C
responses only; OpenID4VP responses use a different platform return path.
Registering a Zero-Knowledge Proof backend for ISO mDoc
On the wallet side use IsoMdocZkBackendRegistry to register a zero-knowledge backend capable of generating
IsoMdocZkProofs/ZkDocuments.
Example: Register LongfellowBackend:
Ensure the longfellow-zk dependency is added to the project:
implementation("at.asitplus.wallet:vck-longfellow:$version")
Then register the backend (usually with the default registry):
IsoMdocZkBackendRegistry.Default.register(LongfellowBackend()).fold(
onSuccess = { Napier.i { "Hooray!" } },
onFailure = { Napier.e { "Registration failed with ${it.message}" }}
)
Limitations
- Several parts of the W3C VC Data Model have not been fully implemented, i.e. everything around resolving cryptographic key material.
- Anything related to ledgers (e.g. resolving DID documents) is out of scope.
- JSON-LD is not supported for W3C credentials.
- Trust in certificates is evaluated by walking the transported chain, without RFC 5280 path building,
pathLenConstraintorkeyUsage, and without certificate revocation (CRL, OCSP). - ISO mDoc zero-knowledge proof verification is not yet implemented: ZK mdoc presentations are validated like plain mdocs, their proofs are not verified.
Contributing
External contributions are greatly appreciated! Be sure to observe the contribution guidelines (see CONTRIBUTING.md). In particular, external contributions to this project are subject to the A-SIT Plus Contributor License Agreement (see also CONTRIBUTING.md).
| This project has received funding from the European Union’s Horizon 2020 research and innovation programme under grant agreement No 959072. |
|---|
Co‑Funded by the European Union |
This project has received funding from the European Union’s Digital Europe Programme (DIGITAL), Project 101102655 — POTENTIAL. |
|---|
The Apache License does not apply to the logos, (including the A-SIT logo) and the project/module name(s), as these are the sole property of A-SIT/A-SIT Plus GmbH and may not be used in derivative works without explicit permission!
