Documentation

PSDP documentation

Integrate the verifier, drive the API, and verify every claim on this site yourself, with curl.

Getting started

How it works

PSDP is a privacy-preserving selective-disclosure protocol: a verifier confirms facts about a credential holder without seeing the underlying data.

  1. Issuer signs a credential (for example, a bank records a completed KYC check).
  2. Holder stores the credential in their wallet.
  3. Verifier creates a proof request: “prove age >= 18” or “prove kyc_level >= 2”.
  4. Holder generates a zero-knowledge proof — it reveals only what was asked.
  5. Verifier calls PSDP to verify the proof.

Base URL

Substitute the host of the deployment you are integrating against. The examples below use a placeholder.

Base URL
https://YOUR_PSDP_SERVER

See it running

A reference deployment with curated demo flows is one click away.

Quick links

Entry points on a running deployment
ResourceURL
Interactive API docs (Swagger UI)/docs
Health & status/api/health
Live demos/demos
Evidence & artifacts/evidence
Getting started

Request access

API keys are issued with pilot access. There is no self-serve signup yet — tell us what you are building and we send a key plus the quickstart. Engineering to engineering, no sales touch.

Getting started

Verify every claim yourself

Every number on this site traces to a dated artifact on the evidence page. This section gives you the commands to check the running deployment and the published proofs yourself — step by step, copy-paste ready.

1. Check the API is live

Health check
$ curl -s https://YOUR_PSDP_SERVER/api/health | python3 -m json.tool
# Inspect the reported status, backends and standards fields.

2. Verify authentication works

Auth gates
# No key -> 401
$ curl -s -o /dev/null -w "%{http_code}" -X POST \
  https://YOUR_PSDP_SERVER/api/v1/verify
# Expected: 401

# Wrong key -> 403
$ curl -s -o /dev/null -w "%{http_code}" -X POST \
  -H "X-API-Key: wrong-key" \
  https://YOUR_PSDP_SERVER/api/v1/verify
# Expected: 403

3. Verify a credential (mock crypto, testing only)

Full verify request
$ curl -X POST https://YOUR_PSDP_SERVER/api/v1/verify \
  -H "Content-Type: application/json" \
  -H "X-API-Key: YOUR_KEY" \
  -d '{
  "proof_request": {
    "protocol_version": "psdp/v0.1",
    "request_id": "test-001",
    "verifier_id": "my-verifier",
    "audience": "my-verifier",
    "nonce": "test-nonce-12345",
    "expires_at": "2030-01-01T00:00:00Z",
    "policy": {
      "policy_id": "test-policy",
      "policy_version": "1.0.0",
      "policy_hash": "sha256:test",
      "allowed_schema_ids": ["schema.test"],
      "required_disclosures": [{"path": "/name"}],
      "predicates": [
        {"predicate_id": "age-check", "path": "/age",
         "operator": "gte", "value_type": "number",
         "comparison_value": 18}
      ],
      "status_requirements": {
        "require_active_status": false,
        "freshness_max_age_seconds": 3600
      }
    }
  },
  "proof_package": {
    "protocol_version": "psdp/v0.1",
    "package_id": "pkg-001",
    "request_id": "test-001",
    "credential": {
      "schema_id": "schema.test",
      "issuer_id": "issuer.test",
      "issuer_key_id": "key.issuer.001",
      "expires_at": "2030-01-01T00:00:00Z",
      "credential_root": "root:test",
      "issuer_signature": "mock-signature:issuer.test:key.issuer.001:eddsa-ed25519"
    },
    "request_binding": {
      "verifier_id": "my-verifier",
      "audience": "my-verifier",
      "nonce": "test-nonce-12345",
      "policy_id": "test-policy",
      "policy_version": "1.0.0",
      "policy_hash": "sha256:test",
      "request_binding_digest": "sha256:binding"
    },
    "disclosed_claims": [
      {"path": "/name", "value": "Jane Doe", "value_type": "string"}
    ],
    "predicate_assertions": [
      {"predicate_id": "age-check", "path": "/age",
       "operator": "gte", "satisfied": true}
    ],
    "proof_material": {"proof": "mock-proof:valid", "encoding": "base64"},
    "backend_profile": {
      "backend": "arkworks", "proof_system": "groth16",
      "circuit_id": "test-v1", "circuit_version": "1.0",
      "verification_key_id": "vk-test"
    },
    "status_binding": {
      "status_authority_id": "status.test",
      "status_root": "root:active",
      "status_issued_at": "2026-04-13T00:00:00Z",
      "status_fresh_until": "2026-04-14T00:00:00Z",
      "status_binding_signature": "mock-signature:status.test:key.status.001:eddsa-ed25519"
    }
  },
  "allow_mock_crypto": true
}'
# Expected: "decision_status": "accepted" — all checks true.

4. Verify attacks are rejected

Negative tests
# Tampered proof -> rejected
# (same request as step 3, but change the proof value to "tampered")
# Expected: "decision_status": "rejected"

# Wrong schema -> SCHEMA_NOT_ALLOWED   (change schema_id to "schema.fake")
# Expired request -> REQUEST_EXPIRED   (set expires_at to "2020-01-01T00:00:00Z")
# Wrong nonce -> POLICY_HASH_MISMATCH  (change nonce in request_binding)

5. Re-run the Tamarin proofs

Machine-check the protocol model
# Download the theory files published with the evidence package
$ curl -O https://YOUR_PSDP_SERVER/evidence/psdp_protocol.spthy
$ curl -O https://YOUR_PSDP_SERVER/evidence/psdp_private.spthy
$ curl -O https://YOUR_PSDP_SERVER/evidence/psdp_post_compromise.spthy
$ curl -O https://YOUR_PSDP_SERVER/evidence/psdp_issuer_hiding.spthy
$ curl -O https://YOUR_PSDP_SERVER/evidence/psdp_forward_secrecy.spthy
$ curl -O https://YOUR_PSDP_SERVER/evidence/psdp_selective_disclosure_privacy.spthy
$ curl -O https://YOUR_PSDP_SERVER/evidence/psdp_verifier_unlinkability.spthy

# Verify with Docker (no local install needed)
$ docker run --rm -v $(pwd):/work -w /work \
  tamarin-prover/tamarin-prover:latest \
  --prove --derivcheck-timeout=0 psdp_protocol.spthy

# Equivalence theories run in diff-mode:
$ docker run --rm -v $(pwd):/work -w /work \
  tamarin-prover/tamarin-prover:latest \
  --diff --prove --derivcheck-timeout=0 psdp_issuer_hiding.spthy

Expected, across the 7 theory files: 36 lemmas verified — 32 trace properties + 4 observational-equivalence proofs (≈5,500 proof steps), machine-checked in the Tamarin prover under a Dolev-Yao adversary.

6. Check the OIDF conformance bundles

Conformance evidence
$ curl -O https://YOUR_PSDP_SERVER/evidence/EVIDENCE_PACKAGE.json
# Inspect the conformance block; signed test-log .zip exports
# are archived alongside it.

Every conformance result bundle is signed by the suite’s own key and archived; runs are reproducible. We are not listed on openid.net/certification.

7. Inspect security headers

Response headers
$ curl -sI https://YOUR_PSDP_SERVER/api/health | grep -iE \
  "x-content-type|x-frame|strict-transport|content-security|permissions|x-request-id|cache-control"
# Read the security headers the deployment returns.

8. Drive the OID4VP flow

Verifier flow
# Check verifier metadata
$ curl https://YOUR_PSDP_SERVER/.well-known/openid-credential-verifier \
  | python3 -m json.tool

# Start an OID4VP verifier flow
$ curl -X POST https://YOUR_PSDP_SERVER/api/v1/oid4vp/initiate \
  -H "X-API-Key: YOUR_KEY" | python3 -m json.tool
# Expected: authorization_url, request_uri, state, nonce

# Fetch the signed request object
$ curl https://YOUR_PSDP_SERVER/api/v1/oid4vp/request/REQUEST_ID
# Expected: a signed JWT (ES256 + x5c certificate)

9. Verify evidence integrity

Checksums
$ curl -O https://YOUR_PSDP_SERVER/evidence/SHA256SUMS.txt
$ sha256sum -c SHA256SUMS.txt
# Expected: all files "OK"

10. Probe the wallet endpoint with a bad request object

Invalid request object
$ curl "https://YOUR_PSDP_SERVER/api/v1/oid4vp/wallet/authorize?client_id=test&nonce=test&request_uri=https://example.com/bad-jwt"
# Expected: an error response — the wallet role rejects
# request objects whose signature does not verify.
Getting started

Authentication

All /api/v1/ endpoints require an API key in the X-API-Key header.

Authenticated request
$ curl -X POST https://YOUR_PSDP_SERVER/api/v1/verify \
  -H "Content-Type: application/json" \
  -H "X-API-Key: your-api-key" \
  -d '{ ... }'

Error responses

Authentication errors
StatusMeaning
401No API key provided
403Invalid API key
429Rate limit exceeded (see the Retry-After header)

Public endpoints (no key needed)

  • GET /api/health — system status
  • GET /docs — Swagger documentation
  • GET /evidence/* — evidence package downloads
  • GET /.well-known/openid-credential-verifier — OID4VP metadata
Getting started

Your first verification

Verify a credential in one API call.

Step 1 — send a verification request

POST /api/v1/verify
$ curl -X POST https://YOUR_PSDP_SERVER/api/v1/verify \
  -H "Content-Type: application/json" \
  -H "X-API-Key: your-api-key" \
  -d '{
  "proof_request": {
    "protocol_version": "psdp/v0.1",
    "request_id": "req-001",
    "verifier_id": "your-company.example",
    "audience": "your-company.example",
    "nonce": "unique-nonce-12345",
    "expires_at": "2030-01-01T00:00:00Z",
    "policy": {
      "policy_id": "kyc-check",
      "policy_version": "1.0.0",
      "policy_hash": "sha256:abc123",
      "allowed_schema_ids": ["schema.banking.kyc"],
      "required_disclosures": [
        {"path": "/jurisdiction"}
      ],
      "predicates": [
        {"predicate_id": "kyc-level",
         "path": "/kyc_level",
         "operator": "gte",
         "value_type": "number",
         "comparison_value": 2}
      ],
      "status_requirements": {
        "require_active_status": true,
        "freshness_max_age_seconds": 3600
      }
    }
  },
  "proof_package": { ... }
}'

Step 2 — read the response

Response
{
  "decision_status": "accepted",
  "reason_codes": [],
  "checks": {
    "issuer_signature_valid": true,
    "proof_valid": true,
    "policy_bound": true,
    "replay_safe": true,
    "credential_not_expired": true,
    "status_valid": true,
    "disclosures_valid": true
  },
  "verification_time_ms": ...
}
Getting started

Create a proof request

Before verifying, create a proof request that defines what you want verified.

POST /api/v1/request
$ curl -X POST https://YOUR_PSDP_SERVER/api/v1/request \
  -H "Content-Type: application/json" \
  -H "X-API-Key: your-api-key" \
  -d '{
  "verifier_id": "bank-b.example.com",
  "audience": "bank-b.example.com",
  "policy": {
    "policy_id": "cross-border-kyc",
    "policy_version": "1.0.0",
    "allowed_schema_ids": ["schema.banking.kyc"],
    "required_disclosures": [
      {"path": "/jurisdiction", "label": "Jurisdiction"}
    ],
    "predicates": [
      {"predicate_id": "kyc-level-check",
       "path": "/kyc_level",
       "operator": "gte",
       "value_type": "number",
       "comparison_value": 2}
    ],
    "status_requirements": {
      "require_active_status": true,
      "freshness_max_age_seconds": 3600
    }
  }
}'

The response includes an auto-generated request_id and nonce:

Response
{
  "request_id": "req:a1b2c3d4...",
  "nonce": "xK9mP2...",
  "verifier_id": "bank-b.example.com",
  "expires_at": "2026-04-14T00:00:00Z",
  "policy": { ... }
}

Send this to the credential holder. They use it to generate a proof package.

Core concepts

Zero-knowledge proofs

A zero-knowledge proof lets a holder prove a statement is true without revealing any information beyond the statement itself.

Example

A holder has a credential with age: 25. A verifier needs to know “is this person over 18?”

What the verifier learns under each approach
ApproachWhat the verifier learnsDisclosure
Full documentage = 25, name, date of birth, address…Everything
Selective disclosure (SD-JWT)age = 25The attribute value
ZK predicate (PSDP)age >= 18: trueThe predicate result only

With the ZK predicate path, the verifier learns only that the statement is true — not the actual age, not the birthdate, not the name.

The proving backend

PQC-hybrid authentication (ML-DSA-65) + confidentiality (ML-KEM-768). The zero-knowledge layer is classical Groth16/BN254, with a de-risked (not yet live, not yet formally sound) transparent PQ-STARK backend on the roadmap.

Real Groth16/BN254 verification is exercised in CI: a blocking job builds the Rust provers and runs the suite with PSDP_REQUIRE_ZK=1, so a missing prover is a hard failure, not a silent skip.

Core concepts

Selective disclosure

Selective disclosure lets the holder reveal only specific fields from their credential.

How it works in PSDP

The verifier’s policy specifies required_disclosures — an array of field paths that must be revealed:

Policy fragment
"required_disclosures": [
  {"path": "/jurisdiction"},
  {"path": "/certification_body"}
]

The holder’s proof package includes only these fields:

Proof package fragment
"disclosed_claims": [
  {"path": "/jurisdiction", "value": "DE"},
  {"path": "/certification_body", "value": "TUV"}
]

Everything else in the credential stays hidden. The proof binds the disclosed values to the issuer-signed credential — the holder cannot substitute them.

Core concepts

Predicates

Predicates verify facts about credential fields without revealing the actual values.

Supported operators

Predicate operators
OperatorMeaningExample
gtegreater than or equalage >= 18
gtgreater thanincome > 3000
lteless than or equalclaims <= 3
ltless thanincidents < 2
eqequalstatus = 1

Example: age verification

Predicate definition
"predicates": [
  {
    "predicate_id": "over-18",
    "path": "/age",
    "operator": "gte",
    "value_type": "number",
    "comparison_value": 18
  }
]

The holder’s response includes:

Assertion
"predicate_assertions": [
  {"predicate_id": "over-18", "satisfied": true}
]

The verifier learns: “age >= 18 is true.” Nothing else about the actual age.

Core concepts

Unlinkability

The design goal: when a holder presents to two different verifiers, the two verifiers should not be able to correlate the presentations.

How it works

PSDP uses nullifiers — one-way values derived from the holder’s secret and a per-relying-party scope:

Nullifier derivation
nullifier = hash(holder_secret, relying_party_scope)

Each relying party gets a distinct scope, so the same user’s nullifier differs across relying parties — no cross-site correlation — while staying stable within one relying party for replay detection. A plain SD-JWT presentation, by contrast, re-presents the same signed payload each time, which colluding verifiers can compare; the ZK path derives a distinct per-relying-party value instead.

Verification status — stated honestly

Designed and machine-checked for unlinkability properties: identifier-hiding (T10a), audit/verifier-split (T10d) and nullifier-unlinkability / private-presentation theorems are discharged; verifier-view unlinkability, issuer hiding and selective-disclosure privacy are modeled with machine-verified observational-equivalence lemmas but remain partial overall. The full split is in the formal-verification section and on the evidence page.

API reference

API reference

The core endpoints. A running deployment serves interactive Swagger docs at /docs.

POST /api/v1/verify

Verify a credential proof package against a proof request.

Request body
FieldTypeDescription
proof_requestobjectThe verification policy and request parameters
proof_packageobjectThe holder’s cryptographic response
allow_mock_cryptobooleanAllow mock signatures/proofs (testing only; refused in production mode)
current_timestringOverride verification time (ISO 8601)
Response
FieldTypeDescription
decision_statusstring"accepted" or "rejected"
reason_codesarraySpecific rejection reasons (empty if accepted)
checksobjectIndividual boolean check results
verification_time_msnumberTime taken in milliseconds
Security checks performed
CheckWhat it verifies
issuer_signature_validCredential signed by a registered issuer
proof_validThe ZK proof verifies correctly
policy_boundProof matches the requested policy
replay_safeProof not used before
credential_not_expiredCredential still within validity
status_validRevocation status is fresh
disclosures_validDisclosed claims match the policy

POST /api/v1/request

Create a proof request from a verification policy.

Request body
FieldTypeRequiredDescription
verifier_idstringYesYour identifier
audiencestringYesWho the proof is for
policyobjectYesVerification policy
noncestringNoCustom nonce (auto-generated if omitted)
ttl_secondsintegerNoRequest validity period (default: 3600)
Policy object
FieldDescription
policy_idName for your policy
allowed_schema_idsArray of accepted credential types
required_disclosuresArray of {"path": "/field"}
predicatesArray of predicate objects
status_requirementsRevocation check settings

POST /api/v1/evidence

Generate a cryptographic audit evidence package from a verification. Call this after a verification to create a tamper-evident record for compliance and audit purposes; the package includes a hash chain linking the proof request, proof package and verification result.

OID4VP endpoints

OpenID for Verifiable Presentations — verifier and wallet roles
EndpointRoleDescription
POST /api/v1/oid4vp/initiateVerifierStart a flow: creates a signed request object (JAR / RFC 9101) and returns a wallet authorization URL
GET /api/v1/oid4vp/request/{id}VerifierServes the signed request object that wallets fetch
POST /api/v1/oid4vp/responseVerifierReceives the VP token; direct_post and direct_post.jwt (encrypted) modes
GET /api/v1/oid4vp/wallet/authorizeWalletReceives authorization requests, verifies the request-object signature, creates an SD-JWT VC with Key Binding JWT, posts the encrypted response
GET /.well-known/openid-credential-verifierVerifierMetadata: supported VP formats, signing keys, encryption keys

GET /api/health

System health and protocol metadata. No authentication required.

Selected response fields
FieldDescription
status"ok" when healthy
backendsAvailable ZK proof backends
standardsSupported standards
schedulerBackground task scheduler status

Questions about an endpoint? Talk to us →

API reference

Error & rejection codes

Every rejection carries a machine-readable reason code. The verifier tells you exactly why it said no.

HTTP errors

HTTP-level errors
CodeMeaning
401No API key provided
403Invalid API key
413Request body too large (>1 MB)
429Rate limit exceeded
500Internal server error

Verification rejection codes

Reason codes returned in reason_codes
CodeMeaningHow to fix
SCHEMA_NOT_ALLOWEDCredential schema not in policyAdd the schema to allowed_schema_ids
POLICY_HASH_MISMATCHProof binding does not match the requestEnsure nonce, verifier_id and policy match
DISCLOSURE_MISMATCHDisclosed claims do not match the policyDisclose exactly the required fields
REQUEST_EXPIREDRequest expires_at is pastCreate a new request
CREDENTIAL_EXPIREDCredential validity has endedHolder needs a fresh credential
STATUS_STALERevocation status too oldHolder needs fresh status
REPLAY_DETECTEDProof already usedGenerate a new proof with a new nonce
INVALID_PROOFZK proof does not verifyCheck proof generation
PREDICATE_FAILUREPredicate assertion mismatchEnsure assertions match the policy predicates
VERSION_MISMATCHProtocol version mismatchUse psdp/v0.1
Integration guides

Integration guides

Three worked policies. All three exercise the reference implementation — production hardening is in progress, and production deployment boundaries are noted per guide.

Cross-border KYC

Scenario: Bank A (Germany) has verified Customer X. Bank B (India) needs to confirm this before processing a transfer — without receiving the customer’s personal data.

Step 1 — Bank B creates a proof request
POST /api/v1/request
{
  "verifier_id": "bank-b.india.example",
  "audience": "bank-b.india.example",
  "policy": {
    "policy_id": "cross-border-kyc",
    "allowed_schema_ids": ["schema.banking.kyc"],
    "required_disclosures": [
      {"path": "/jurisdiction"}
    ],
    "predicates": [
      {"predicate_id": "kyc-level",
       "path": "/kyc_level",
       "operator": "gte",
       "value_type": "number",
       "comparison_value": 2}
    ]
  }
}

Step 2: the customer’s wallet creates a proof showing jurisdiction = DE and kyc_level >= 2 — without revealing name, passport or address.

Step 3: Bank B submits the request plus proof package to POST /api/v1/verify. On "accepted", Bank B knows the predicate held and the jurisdiction — and nothing else: no name, no passport, no address, not even the exact KYC level.

Designed so that no personal data beyond the disclosed jurisdiction crosses the wire in this flow; the proof carries a cryptographic assertion, not the underlying record.

Age verification

Prove “over 18” without revealing the birthdate, age, or any personal information.

Age policy
{
  "policy_id": "age-check",
  "allowed_schema_ids": ["schema.pid.eu"],
  "required_disclosures": [],
  "predicates": [
    {"predicate_id": "over-18",
     "path": "/age",
     "operator": "gte",
     "comparison_value": 18}
  ]
}

Note: required_disclosures is empty — the verifier learns nothing except “age >= 18: true”.

Production deployment boundaries for age verification — what PSDP provides and what a deployment still needs — are listed in the age-verification page’s Scope & limits.

Third-party assurance (DORA-style)

Scenario: a financial entity needs evidence about an ICT provider’s security posture without receiving the underlying audit detail.

Assurance policy
{
  "policy_id": "dora-ict-check",
  "required_disclosures": [
    {"path": "/jurisdiction"},
    {"path": "/certification_body"}
  ],
  "predicates": [
    {"predicate_id": "pentest-recent",
     "path": "/days_since_pentest",
     "operator": "lte",
     "comparison_value": 365},
    {"predicate_id": "low-incidents",
     "path": "/critical_incidents_12m",
     "operator": "lte",
     "comparison_value": 2}
  ]
}

What the verifier learns: jurisdiction, certification body, that the pentest was recent and incidents were few.

What stays hidden: pentest findings, vulnerability details, incident reports, internal risk scores.

This guide demonstrates a policy pattern on the reference implementation; it is not legal or regulatory advice.

Security

Formal verification — the honest count

33 protocol theorems tracked: 15 discharged (machine-checked), 18 partial. We publish the split — partial means exactly that.

15/33 Protocol theorems discharged (machine-checked) 18 partial; the split is published, not hidden. Theorem status →
36 Tamarin lemmas verified 32 trace + 4 observational-equivalence, ≈5,500 proof steps, 7 theory files. See evidence →

The protocol model is machine-checked in the Tamarin prover under a Dolev-Yao adversary: 36 lemmas verified — 32 trace properties + 4 observational-equivalence proofs (≈5,500 proof steps), across 7 theory files. A theorem map without gaps is a theorem map you should distrust; ours has 18 partials and says so.

Unlinkability-family status

Unlinkability properties — discharged vs partial
PropertyHow it is checkedStatus
Identifier-hiding (T10a)Machine-checked theoremDischarged
Audit / verifier-split (T10d)Machine-checked theoremDischarged
Nullifier-unlinkability / private presentationMachine-checked theoremsDischarged
Verifier-view unlinkabilityMachine-verified observational-equivalence lemmasPartial
Issuer hidingMachine-verified observational-equivalence lemmasPartial
Selective-disclosure privacyMachine-verified observational-equivalence lemmasPartial

To re-run the proofs yourself, see step 5 above.

Security

Circuit verification

Reference ZK circuits additionally checked with Picus (Veridise, Z3) — “properly constrained” — and Circomspect (Trail of Bits) — “no issues found” — plus 14/14 negative-witness tests (circom reference circuits, April 2026 evidence package).

Scope: these results cover the circom reference circuits in the April 2026 evidence package. The live verify path is the arkworks Groth16 backend, not these circom artifacts.

Re-run the checks yourself

Picus + Circomspect + negative tests
# Install the tools
$ git clone https://github.com/Veridise/Picus.git
$ cargo install circomspect

# Picus — formal R1CS verification
$ racket Picus/picus.rkt --solver z3 psdp_selective_disclosure.r1cs
# Expected: "The circuit is properly constrained"

# Circomspect — static analysis
$ circomspect psdp_selective_disclosure.circom
# Expected: "No issues found"

# Negative-witness tests
$ npm install circomlib circomlibjs snarkjs
$ node negative_tests.js
# Expected: "14/14 passed, 0 failed"

The circuit sources, compiled R1CS files and tool reports ship in the April 2026 evidence package — see the evidence page for artifact paths.

Standards

OID4VP & SD-JWT VC support

Standards transport, not a proprietary token.

OpenID for Verifiable Presentations

The implementation targets OID4VP 1.0 Final, including the High Assurance Interoperability Profile (HAIP), and is tested against the official OpenID Foundation conformance suite (local runs, reproducible — not an OpenID Foundation certification) — see the results below.

  • response_type: vp_token
  • response_mode: direct_post and direct_post.jwt (encrypted)
  • Signed request objects (JAR / RFC 9101) with ES256
  • client_id_scheme: x509_hash and x509_san_dns
  • DCQL (Digital Credentials Query Language)
  • Presentation Exchange 2.0
  • Both verifier and wallet roles

Cross-stack check: Austria’s vck library (A-SIT / the engine behind the ID-Austria Valera wallet) presented an SD-JWT PID to the PSDP verifier over OID4VP 1.0 Final (direct_post + DCQL); PSDP verified the issuer signature and the Key-Binding JWT and accepted (2026-06-08, reproducible harness). Scope: this tested the vck library, not the Valera app binary, and dc+sd-jwt only — not JARM or mso_mdoc.

SD-JWT VC

SD-JWT VC (Selective Disclosure JWT Verifiable Credentials) is supported as a credential format:

  • Full SD-JWT parsing with disclosure resolution
  • Key Binding JWT (KB-JWT) for holder binding
  • Nested disclosures and array-element disclosures
  • _sd_alg support (sha-256, sha-384)
  • base64url-encoded disclosure digests (per spec)
  • ES256 and EdDSA signing

Wallet interop coverage: 22 credential types, in 43 issuer configurations (SD-JWT VC and ISO mdoc formats), each issued and stored end-to-end against the walt.id wallet stack over live OID4VCI. Per-doctype results are on the evidence page.

Standards

OIDF conformance results

Self-run against the official OpenID Foundation conformance suite — local runs, reproducible. Not a published OIDF certification.

Official OIDF suite — local runs, reproducible. Self-run evidence, not an OpenID Foundation certification.
Run Scope Result Status
Full suite (2026-06) OID4VCI, OID4VP and OpenID Federation test plans 319/332 passed, 0 failed (11 warnings, 2 skipped) Pass
FAPI 2.0 Security Profile (Final) Test modules embedded in the OID4VCI issuer plans 78/80 passed, 0 failed (2 warnings) Pass
Earlier full-suite baseline (2026-05-26) Full suite at the time 210/218 passed, 0 failed (7 warnings, 1 skipped) Pass

Tested against the official OpenID Foundation conformance suite (local run, reproducible): 319 of 332 test modules passed, 0 failed (11 warnings, 2 skipped), across OID4VCI, OID4VP and OpenID Federation test plans. Self-run evidence — not an OpenID Foundation certification.

Every conformance result bundle is signed by the suite’s own key and archived; runs are reproducible. We are not listed on openid.net/certification. Source artifacts and run dates are on the evidence page.

Next steps

Questions about an integration?

Bring your deployment or your roadmap — engineering to engineering, no sales touch. Or audit the demos first; we would do the same.