Skip to content

Integration Guide

This page walks through what it takes to integrate fido4vc-jcs-2026 verification into an existing verifier stack. The reference deployment uses walt.id — most of the worked examples below are walt.id-specific. The general patterns apply to any verifier.

You have three architectural choices for how to add fido4vc-jcs-2026 verification to a verifier stack:

ModelProsCons
HTTP sidecar (run fido-vc-verifier-sidecar next to your verifier)Language-agnostic; one impl serves any backend; cleanest deployment for non-Node stacksExtra deployment artifact; network hop adds latency; sidecar must be reachable
Native in-process library (depend on fido-vc-cryptosuite-ts directly)No network hop; no sidecar to operate; lowest latencyLanguage must match the cryptosuite port; only TS is published today, so non-Node verifier stacks must use the sidecar model
Hybrid (use native where you can, sidecar as fallback)Best of bothMore moving parts; need a strategy interface

The walt.id reference deployment uses the sidecar model.

walt.id’s waltid-verification-policies library provides a pluggable policy framework. Verification policies are registered by name; the verifier API applies policies declaratively based on the credential format.

In waltid-libraries/credentials/waltid-verification-policies/src/commonMain/kotlin/id/walt/policies/policies/LdSignaturePolicy.kt:

class LdSignaturePolicy(var baseUrl: String = DEFAULT_BASE_URL) : JwtVerificationPolicy() {
override val name = "signature_ld-vp"
override val description =
"Checks a JSON-LD presentation by verifying its cryptographic signature " +
"using the key referenced by the DID in `verificationMethod`."
override val supportedVCFormats = setOf(VCFormat.ldp_vp)
companion object {
// fido-vc-verifier-sidecar — assumes loopback colocation with walt.id; override for split-host deployments
const val DEFAULT_BASE_URL = "http://localhost:8081"
}
private val http = HttpClient {
expectSuccess = false
install(ContentNegotiation) { json() }
defaultRequest { url(baseUrl) }
}
override suspend fun verify(
credential: String, args: Any?, context: Map<String, Any>
): Result<JsonElement> = runCatching {
val ldp = Json.parseToJsonElement(credential).jsonObject
val response = http.post("/verify") {
setBody(ldp)
header(HttpHeaders.ContentType, ContentType.Application.Json)
}
val body = response.body<JsonObject>()
val verified = body["verified"]?.jsonPrimitive?.content?.toBoolean()
?: throw VerificationException("Invalid response format")
if (!verified) {
throw VerificationException(
body["error"]?.jsonPrimitive?.content ?: "Signature verification failed"
)
}
ldp
}
}

Register it in PolicyManager:

init {
registerPolicies(
JwtSignaturePolicy(),
SdJwtVCSignaturePolicy(),
LdSignaturePolicy(), // <-- add this
// ...
)
}

Step 2 — Route ldp_vc / ldp_vp through the new policy

Section titled “Step 2 — Route ldp_vc / ldp_vp through the new policy”

In VerifierService.kt (or your equivalent format-routing layer):

when (format) {
VCFormat.sd_jwt_vc -> listOf(PolicyRequest(SdJwtVCSignaturePolicy()))
VCFormat.ldp_vc -> listOf(PolicyRequest(LdSignaturePolicy()))
else -> listOf(PolicyRequest(JwtSignaturePolicy()))
}

If your walt.id instance is also issuing credentials, advertise the cryptosuite in waltid-services/waltid-issuer-api/config/credential-issuer-metadata.conf:

credentialSupported = {
# ...
proof_types_supported = {
ldp_vp = {
proof_signing_alg_values_supported = ["fido4vc-jcs-2026"]
}
}
}

Clone fido-vc-verifier-sidecar on the same machine (or in the same docker network / Kubernetes pod) as your walt.id verifier — LdSignaturePolicy posts to http://<sidecar-host>:8081/verify, with <sidecar-host> defaulting to localhost. The sidecar must be reachable from walt.id at whatever address <sidecar-host> resolves to (see Common pitfalls for non-loopback deployments):

Terminal window
git clone https://github.com/fido4vc/fido-vc-verifier-sidecar
cd fido-vc-verifier-sidecar
npm install
npm run build
PORT=8081 npm start

The sidecar starts on port 8081, matching LdSignaturePolicy’s default <sidecar-host>:8081 target. For containerized deployments, run it alongside your walt.id container with the WALLET_API_URL env var pointing to the walt.id Wallet API.

Use the included verifier test script in fido-vc-middleware/tests/walt-id-verifier.js:

{
"vp_policies": ["signature_ld-vp", "holder-binding"],
"vc_policies": ["signature", "expired", "not-before"],
"request_credentials": [
{ "format": "jwt_vc_json", "type": "BankId" }
]
}

A successful flow: wallet submits a presentation with a fido4vc-jcs-2026 proof → walt.id’s signature_ld-vp policy fires → sidecar verifies → returns verified: true → walt.id returns success to the verifier.

If you’re not using walt.id, the integration is conceptually identical:

  1. Detect the cryptosuite. When your verifier sees a VP with proof.type = "DataIntegrityProof" and proof.cryptosuite = "fido4vc-jcs-2026", route to the verification logic.
  2. Verify per the spec. Run the verify algorithm. Either:
    • Call the sidecar over HTTP, or
    • Import the TS reference implementation directly (for Node-based verifier stacks).
  3. Return success/failure to your verification pipeline. The cryptosuite verification is just one step — your pipeline still validates the embedded VCs, presentation definition, freshness, etc.

The cryptosuite is platform-agnostic; nothing about it is walt.id-specific.

Sidecar not reachable. LdSignaturePolicy calls http://<sidecar-host>:8081/verify, where <sidecar-host> defaults to localhost. That default only works when the sidecar shares a loopback with walt.id (same host, same docker network, or same Kubernetes pod — containers in one pod share localhost). For split deployments, override baseUrl to a reachable address — e.g. LdSignaturePolicy(baseUrl = "http://sidecar.internal:8081") for a private DNS host, or a Kubernetes Service DNS name like http://fido-sidecar.default.svc.cluster.local:8081.

JCS implementation drift. If signatures from a JS signer don’t verify under a JVM verifier (or vice versa), the cause is almost always a non-RFC-8785-compliant JCS implementation. Use Erdtman’s reference on both sides — canonicalize npm package for JS, io.github.erdtman:java-json-canonicalization for JVM.

Wallet returning the wrong proof structure. proofValue MUST be an object with signature, authenticatorData, clientData. Some VC-DI libraries expect a string here and will reject the proof at parse time. If you see schema errors at the verifier, check that the wallet implementation is emitting the structured object format.

origin mismatch. WebAuthn’s clientDataJSON.origin must match the FIDO middleware’s configured origin. If your middleware moves to a new domain, all previously-registered FIDO credentials become unusable (this is a WebAuthn invariant, not a FIDO4VC choice).

A complete reference deployment looks like:

[Browser: Wallet UI] ← Next.js
↓ HTTP
[FIDO Middleware] ← Express, port 8080
↓ HTTP
[walt.id Wallet API] ← Kotlin/JVM, port 7001
↓ HTTP / outbound
[Issuer API + Verifier API] ← walt.id, ports 7002/7003
↓ HTTP (verifier policy)
[fido-vc-verifier-sidecar] ← Express, port 8081
↓ (in-process)
[fido-vc-cryptosuite-ts]

Plus a database for the FIDO Middleware’s WebAuthn credential records and walt.id’s wallet storage.

A docker-compose.yml for the full local stack is in the parent waltid-identity fork.