Verify a Credential
PDTF credential verification has four jobs: validate the VC structure, verify the Data Integrity proof, confirm the issuer is trusted for the claimed paths, and check the credential has not been revoked.
In @pdtf/core, the easiest way to do that is VcValidator. It combines DID resolution, proof verification, TIR lookup, and Bitstring Status List checking in one pipeline.
1. Install and initialise
Section titled “1. Install and initialise”npm install @pdtf/coreimport { DidResolver, TirClient, VcValidator, type VerifiableCredential } from '@pdtf/core';
const didResolver = new DidResolver({ defaultTtlMs: 60 * 60 * 1000,});
const tirClient = new TirClient({ registryUrl: 'https://tir.platform.example.com/v1/registry',});
const validator = new VcValidator();cargo add pdtf-coreuse pdtf_core::did::resolver::DidResolver;use pdtf_core::federation::client::FederationRegistryResolver;use pdtf_core::validator::verify::verify_vc;
let resolver = DidResolver::new()?;let trust_resolver = FederationRegistryResolver::new(vec![ "https://tir.platform.example.com/v1/registry".into(),])?;pip install pdtf-corefrom pdtf_core import verify_vc, DidResolver, TirClient
resolver = DidResolver(default_ttl_ms=3_600_000)tir = TirClient(registry_url="https://tir.platform.example.com/v1/registry")dotnet add package Pdtf.Coreusing Pdtf.Core;
var resolver = new DidResolver(defaultTtlMs: 3_600_000);var tirClient = new TirClient("https://tir.platform.example.com/v1/registry");DidResolver resolves did:key locally and did:web over HTTPS. TirClient loads the Trusted Issuer Registry and caches it. VcValidator orchestrates the checks.
2. Supply the credential paths
Section titled “2. Supply the credential paths”TIR authorisation is path-based, not issuer-wide. That means you should tell the validator which PDTF entity paths the credential is asserting.
For an EPC credential, that might be:
const credentialPaths = [ 'Property:/energyEfficiency/*',];let claimed_paths = vec![ "Property:/energyEfficiency/*".to_string(),];credential_paths = ["Property:/energyEfficiency/*"]var credentialPaths = new[] { "Property:/energyEfficiency/*" };If you skip credentialPaths, the validator can still check structure, signature, and revocation, but it cannot confirm the issuer is authorised for the data being claimed.
3. Validate the credential
Section titled “3. Validate the credential”const vc: VerifiableCredential = await loadCredentialSomehow();
const result = await validator.validate(vc, { didResolver, tirClient, credentialPaths,});
if (!result.valid) { console.error(result.stages); throw new Error('Credential failed validation');}
console.log('Credential is valid');console.log(result.stages.tir.details);use pdtf_core::validator::verify::{verify_vc, VerifyVcOptions};use std::sync::Arc;
let vc = load_credential_somehow().await?;
let result = verify_vc(VerifyVcOptions { vc: &vc, resolver: &resolver, trust_resolver: Some(Arc::new(trust_resolver)), claimed_paths, status_list_bitstring: None, // fetched automatically}).await;
if !result.valid { eprintln!("Errors: {:?}", result.errors); return Err("Credential failed validation".into());}
println!("Credential is valid");import json
vc = json.loads(load_credential_somehow())
result = verify_vc( vc_json=json.dumps(vc), resolver=resolver, tir_client=tir, credential_paths=credential_paths,)
if not result["valid"]: raise Exception(f"Credential failed: {result['errors']}")
print("Credential is valid")var vc = LoadCredentialSomehow();
var result = await PdtfCore.VerifyVc(vc, new VerifyOptions{ Resolver = resolver, TirClient = tirClient, CredentialPaths = credentialPaths,});
if (!result.Valid) throw new Exception($"Credential failed: {string.Join(", ", result.Errors)}");
Console.WriteLine("Credential is valid");The result is stage-based:
structure: required VC and PDTF fieldssignature: resolves the issuer DID, checksverificationMethod, confirms the key is inassertionMethod, then runsverifyProoftir: checks the issuer DID against the TIR and verifies path coveragestatus: fetches the Bitstring Status List and checks the credential bit
4. What the validator enforces
Section titled “4. What the validator enforces”VcValidator is intentionally fail-closed in the places that matter:
issuermust match the DID used inproof.verificationMethod- the verification method must exist in the DID document
- the verification method must be listed in
assertionMethod credentialStatusmust be present for revocation checking- a set status bit means the credential is rejected
That aligns with the PDTF model: a valid signature is not enough on its own.
5. Verify just the proof, if you need a lower-level check
Section titled “5. Verify just the proof, if you need a lower-level check”If you already resolved the public key yourself, you can call verifyProof directly:
import { DidResolver, verifyProof, type VerifiableCredential } from '@pdtf/core';import { base58btc } from 'multiformats/bases/base58';
const vc: VerifiableCredential = await loadCredentialSomehow();const resolver = new DidResolver();
const vm = vc.proof!.verificationMethod;const issuerDid = vm.split('#')[0]!;const didDoc = await resolver.resolve(issuerDid);const method = didDoc.verificationMethod!.find((m) => m.id === vm)!;
const decoded = base58btc.decode(method.publicKeyMultibase!);const publicKey = decoded[0] === 0xed && decoded[1] === 0x01 ? decoded.slice(2) : decoded;
const ok = verifyProof({ document: vc, publicKey,});
console.log({ ok });use pdtf_core::signer::proof::verify_proof;use pdtf_core::did::resolver::DidResolver;
let resolver = DidResolver::new()?;let vc = load_credential_somehow().await?;
let vm = vc.proof.as_ref().unwrap().verification_method.clone();let issuer_did = vm.split('#').next().unwrap();let did_doc = resolver.resolve(issuer_did).await?;
let method = did_doc.verification_method.iter() .find(|m| m.id == vm) .expect("Verification method not found");
let public_key = method.decode_public_key()?;let ok = verify_proof(&vc, &public_key)?;
println!("Proof valid: {ok}");from pdtf_core import verify_proof, DidResolver
resolver = DidResolver()vc = load_credential_somehow()
vm = vc["proof"]["verificationMethod"]issuer_did = vm.split("#")[0]did_doc = resolver.resolve(issuer_did)
method = next(m for m in did_doc["verificationMethod"] if m["id"] == vm)ok = verify_proof(vc_json=json.dumps(vc), public_key_hex=method["publicKeyHex"])
print(f"Proof valid: {ok}")var resolver = new DidResolver();var vc = LoadCredentialSomehow();
var vm = vc.Proof.VerificationMethod;var issuerDid = vm.Split('#')[0];var didDoc = await resolver.Resolve(issuerDid);
var method = didDoc.VerificationMethod.First(m => m.Id == vm);var publicKey = method.DecodePublicKey();
var ok = PdtfCore.VerifyProof(vc, publicKey);Console.WriteLine($"Proof valid: {ok}");Use this lower-level path when you are embedding verification into a custom flow, but prefer VcValidator for production verification because it also checks TIR authorisation and revocation.
6. Recommended production pattern
Section titled “6. Recommended production pattern”For most PDTF consumers, the right flow is:
- Parse the VC JSON.
- Call
validator.validate(...)withcredentialPaths. - Reject if any stage fails.
- Log
warningsand TIR details for audit. - Cache DID documents, TIR responses, and status lists for short periods only.
If the credential is about access or authority, such as SellerCapacity, Representation, or TransactionRole, do not skip the status check. Revocation is what turns an old mandate into an invalid one.