PasskeyBridge

Engineering · 2026-04-06

Selective Disclosure with SD-JWT: Revealing Only the Claims That Matter

By J. W. Bouckaert

Selective Disclosure with SD-JWT: Revealing Only the Claims That Matter

The problem with all-or-nothing credentials

Traditional JWTs are binary. When you present a JWT to a verifier, every claim in the payload is visible. Every one. If your identity credential contains your name, date of birth, address, national ID number, employer, and credit score—the airport lounge verifier who only needs to confirm you're over 18 sees all of it.

This is a fundamental architectural failure that the identity industry has tolerated for over a decade.

The credential that proves you're over 18 should not also reveal your home address. Selective disclosure is a prerequisite for ethical credential design rather than a feature.

The W3C Verifiable Credentials Data Model v2.0 acknowledges this requirement but does not prescribe an implementation. The IETF filled that gap with SD-JWT (RFC 9901), published as a Standards Track RFC in November 2025, and its verifiable credential profile SD-JWT-VC (draft-ietf-oauth-sd-jwt-vc-16). PasskeyBridge implements both, natively, with hybrid PQC signatures, and with zero PII stored at any layer.

This article walks through the complete SD-JWT-VC lifecycle as implemented in our credential issuance pipeline: from disclosure frame to signed token, from holder wallet to verifier presentation, and from digest verification to claim-stripping attack prevention.

The SD-JWT mechanism

SD-JWT extends the JWT format with three concepts:

  1. Disclosures: A disclosure is a base64url-encoded JSON array containing three elements: [salt, claim_name, claim_value]. The salt is a cryptographically random 128-bit value that prevents brute-force enumeration of hidden claims.
  1. Digests: Each disclosure is hashed with SHA-256 to produce a digest. These digests are collected in an _sd array within the JWT payload, replacing the original claim key-value pairs.
  1. Selective Presentation: The holder receives both the signed JWT and all disclosures. When presenting to a verifier, the holder includes only the disclosures for claims they want to reveal. The verifier hashes each presented disclosure and checks that the resulting digest appears in the _sd array.

The elegance of this design is that the issuer commits to all claims at issuance time (via the digests), but the holder controls which claims are revealed at presentation time—without requiring re-issuance or issuer involvement.

The disclosure data structure

A single disclosure for a claim date_of_birth: "1990-03-15" looks like this:

["WyJHaFkzMk5OdGt4ZkRDajgz...", "date_of_birth", "1990-03-15"]

Encoded as base64url:

WyJHaFkzMk5OdGt4ZkRDajgzIiwgImRhdGVfb2ZfYmlydGgiLCAiMTk5MC0wMy0xNSJd

The SHA-256 digest of this encoded string becomes one entry in the _sd array. The original date_of_birth key is removed entirely from the JWT payload—it exists only in the disclosure.

PasskeyBridge's SD-JWT-VC issuance pipeline

When a tenant issues a credential through the shield-vc-issue endpoint, the request body includes an optional disclosure_frame—a declarative object that specifies which claims should be selectively disclosable:

{
  "subject_did": "did:key:z6Mkhafc...",
  "credential_type": "IdentityAttestation",
  "claims": {
    "given_name_hash": "a1b2c3...",
    "family_name_hash": "d4e5f6...",
    "date_of_birth_hash": "789abc...",
    "nationality_hash": "def012...",
    "document_type": "passport",
    "issuing_authority_hash": "345678...",
    "address_hash": "9abcde...",
    "phone_hash": "f01234...",
    "email_hash": "567890...",
    "employer_hash": "abcdef...",
    "credit_tier": "A",
    "verification_method": "in_person"
  },
  "disclosure_frame": {
    "given_name_hash": true,
    "family_name_hash": true,
    "date_of_birth_hash": true,
    "nationality_hash": true,
    "address_hash": true,
    "phone_hash": true,
    "email_hash": true,
    "employer_hash": true,
    "credit_tier": true,
    "issuing_authority_hash": true
  },
  "expiration_days": 90,
  "holder_public_jwk": {
    "kty": "EC",
    "crv": "P-256",
    "x": "f83Og...",
    "y": "x_FEz..."
  }
}

In this example, 12 claims are issued. The disclosure frame marks 10 of them as selectively disclosable. Two claims—document_type and verification_method—are always visible because they are absent from the frame. The credentialSubject.id (the subject DID) is automatically protected from selective disclosure per the W3C VC specification.

Pipeline operations

The issuance pipeline executes five operations in sequence:

StepOperationOutput
1Parse disclosure frameMap of claim → SD/visible
2Generate 128-bit salt per SD claim10 unique salts
3Create disclosures: base64url([salt, key, value])10 encoded disclosures
4Hash each disclosure with SHA-25610 digests in _sd array
5Sign JWT with hybrid PQC (ES256 + ML-DSA-65)SD-JWT-VC token

The resulting JWT payload contains only the always-visible claims and the _sd array of digests:

{
  "iss": "did:web:passkeybridge.io:tenants:acme",
  "sub": "did:key:z6Mkhafc...",
  "jti": "urn:uuid:550e8400-e29b-41d4-a716-446655440000",
  "iat": 1712419200,
  "exp": 1720195200,
  "_sd_alg": "sha-256",
  "cnf": {
    "jwk": { "kty": "EC", "crv": "P-256", "x": "f83Og...", "y": "x_FEz..." }
  },
  "vc": {
    "@context": ["https://www.w3.org/2018/credentials/v1"],
    "type": ["VerifiableCredential", "IdentityAttestation"],
    "credentialSubject": {
      "id": "did:key:z6Mkhafc...",
      "document_type": "passport",
      "verification_method": "in_person",
      "_sd": [
        "2h0e...", "3Kp1...", "7xRm...", "Bq4n...",
        "Fj8k...", "Lm2p...", "Qw9r...", "Tv5s...",
        "Xz1u...", "Yc6w..."
      ]
    }
  }
}

Notice: the 10 selectively disclosable claims are gone from the payload. Only their digests remain in the sorted _sd array. A verifier who receives this JWT without any disclosures can see that the subject has a passport verified in person—but nothing else.

The final token format

The complete SD-JWT-VC string follows the format specified in the IETF draft:

<issuer-jwt>~<disclosure1>~<disclosure2>~...~<disclosureN>~

Each ~ delimiter separates a component. The trailing ~ indicates no Key Binding JWT is appended (more on KB-JWT below). The issuer delivers this complete string—JWT plus all disclosures—to the holder's wallet. The holder stores everything.

ComponentPurposeWho Sees It
Issuer JWTSigned credential with digestsEveryone
DisclosuresEncoded claim valuesHolder only (until presented)
KB-JWT (optional)Proves holder possessionVerifier

Selective presentation: The holder's choice

When the holder presents the credential to a verifier—say, an age-verification service—they include only the disclosures for the claims the verifier needs. For an age check, the holder presents:

<issuer-jwt>~<date_of_birth_hash_disclosure>~

One disclosure out of ten. The verifier receives the JWT, parses the single disclosure, computes its SHA-256 digest, and confirms that the digest exists in the _sd array. The other nine claims remain hidden—the verifier cannot derive them from the digests because each disclosure contains a 128-bit random salt.

The verification algorithm

PasskeyBridge's verifyDisclosures() function executes the following steps for each presented disclosure:

  1. Decode: Parse the base64url-encoded disclosure into [salt, claim_name, claim_value]
  2. Hash: Compute SHA-256(encoded_disclosure) to produce the digest
  3. Match: Verify the digest exists in the JWT's _sd array
  4. Deduplicate: Reject any disclosure whose digest has already been matched (replay protection)
CheckFailure ModeResponse
DecodeMalformed base64url or JSONReject disclosure
Hash matchDigest not in _sd arrayReject—possible tampering
DuplicateSame digest presented twiceReject—replay attempt

If all checks pass, the verified claims are reconstructed and returned to the relying party. The unmatched digests in _sd are reported as undisclosed—the verifier knows they exist but cannot see their contents.

Claim-stripping attacks and KB-JWT

There is a subtle but critical attack vector in naive SD-JWT implementations: claim-stripping. A malicious intermediary intercepts the holder's presentation and removes one or more disclosures before forwarding it to the verifier. The verifier sees fewer disclosed claims and might approve a transaction that the holder intended to restrict.

Consider: a holder presents a credential to a lending platform with disclosures for credit_tier and employer_hash. A man-in-the-middle strips the employer_hash disclosure. The lender sees only the credit tier and—depending on their policy—may approve the loan without employment verification. The holder's intent (to prove both claims) is subverted.

The KB-JWT defense

PasskeyBridge addresses this with Key Binding JWT (KB-JWT), specified in RFC 9901 §4.3. When the holder's wallet creates a presentation, it:

  1. Assembles the SD-JWT with only the selected disclosures: <issuer-jwt>~<d1>~<d2>~...~
  2. Computes the SHA-256 hash of this entire string—the sd_hash
  3. Signs a KB-JWT containing sd_hash, the verifier's audience (aud), a replay nonce, and the issuance timestamp
  4. Appends the KB-JWT to the SD-JWT: <issuer-jwt>~<d1>~<d2>~...~<kb-jwt>

The KB-JWT is signed with the holder's ECDSA P-256 private key—the corresponding public key is embedded in the credential's cnf (confirmation) claim at issuance time. The verifier validates the KB-JWT signature against the cnf.jwk and checks that the sd_hash matches the SHA-256 of the SD-JWT it received.

If any disclosure is stripped after the holder signs, the sd_hash will not match. The verifier rejects the presentation.

KB-JWT ClaimPurposeAttack It Prevents
sd_hashSHA-256 of the SD-JWT stringClaim-stripping
audVerifier identifier (DID or URL)Token replay to wrong verifier
nonceSingle-use challenge from verifierReplay attacks
iatIssuance timestampStale presentations

Salt entropy and brute-force resistance

The security of selective disclosure depends entirely on the unpredictability of the disclosure salt. If an attacker can guess the salt, they can compute the digest for a suspected claim value and check it against the _sd array—effectively de-anonymizing the hidden claim.

PasskeyBridge generates salts using crypto.getRandomValues(), which draws from the operating system's CSPRNG (Cryptographically Secure Pseudo-Random Number Generator). Each salt is 16 bytes (128 bits), base64url-encoded.

The brute-force resistance is straightforward:

Salt SizeSearch SpaceTime at 10^12 hashes/sec
64 bits1.8 × 10^19~5 hours
128 bits3.4 × 10^38~10^19 years
256 bits1.2 × 10^77Heat death of universe

128 bits is the recommended entropy in RFC 9901 §4.2.1 and §9.3. It is computationally infeasible to enumerate even for nation-state adversaries with access to purpose-built hardware. We do not use 256-bit salts because they increase disclosure size without meaningful security benefit at current and projected computational capabilities.

Nested selective disclosure

PasskeyBridge supports structured selective disclosure—where a claim value is itself an object, and individual sub-claims within it can be independently disclosed or hidden. This is critical for address credentials where a verifier might need the country but not the street:

{
  "claims": {
    "address": {
      "street_hash": "abc...",
      "city_hash": "def...",
      "country": "US",
      "postal_code_hash": "ghi..."
    }
  },
  "disclosure_frame": {
    "address": {
      "street_hash": true,
      "city_hash": true,
      "postal_code_hash": true
    }
  }
}

The applyDisclosureFrame() function recurses into nested objects: country remains visible in the JWT payload while street_hash, city_hash, and postal_code_hash each get their own disclosure and digest. The holder can present country (always visible) plus city_hash (selectively) without revealing the street or postal code.

Digest ordering and information leakage

A subtle implementation detail: the order of digests in the _sd array must not leak information about the original claim ordering. If digests were stored in insertion order, a verifier who knows the schema could infer which position corresponds to which claim—even without the disclosure.

PasskeyBridge sorts the _sd array lexicographically before embedding it in the JWT payload. RFC 9901 §4.2.4.1 requires the Issuer to hide the original claim order and recommends sorting alphanumerically (or randomly) after optionally adding decoy digests. This is implemented in the applyDisclosureFrame() function:

if (sdDigests.length > 0) {
  // Per RFC 9901 §4.2.4.1: hide original claim order to prevent leakage
  payload["_sd"] = sdDigests.sort();
}

Sorting destroys the positional correlation between digests and claims. Without the disclosure (and its 128-bit salt), the verifier cannot determine which digest corresponds to which claim name.

Integration with hybrid PQC signatures

SD-JWT-VC is signature-algorithm-agnostic. The selective disclosure mechanism operates on the JWT payload structure—it does not care how the JWT is signed. This is a critical architectural property because it means selective disclosure composes cleanly with PasskeyBridge's hybrid PQC signing scheme.

Every SD-JWT-VC issued by PasskeyBridge carries:

  • An ES256 (ECDSA P-256) classical signature for immediate backward compatibility
  • An ML-DSA-65 (FIPS 204) lattice signature for quantum resilience

The JWT header declares "alg": "PQC-HYBRID" and "typ": "vc+sd-jwt". The signature segment contains a JSON object with both signatures, the algorithm identifier, the key fingerprint, and the quantum nonce.

LayerTechnologyPurpose
PayloadSD-JWT-VC (draft-16) over SD-JWT (RFC 9901)Selective disclosure
Signature (classical)ES256 (P-256)Backward-compatible verification
Signature (PQC)ML-DSA-65 (FIPS 204)Quantum-resilient verification
Holder bindingECDSA P-256 KB-JWTClaim-stripping prevention

This layered architecture means a credential issued today with selective disclosure will remain verifiable and quantum-resilient when cryptographically relevant quantum computers arrive. The holder's selective disclosure choices are protected by both classical and post-quantum cryptography.

The zero-PII guarantee

Even the "visible" claims in PasskeyBridge credentials are hashed values. The claim given_name_hash is a SHA-256 digest of the actual given name—not the name itself. Selective disclosure operates on these hashes.

This creates a two-layer privacy architecture:

  1. Layer 1 (SD-JWT): The holder chooses which claim hashes to disclose
  2. Layer 2 (Zero-PII): Even disclosed claims contain no plaintext PII—only cryptographic commitments

A verifier who receives a disclosed phone_hash can confirm it matches a phone number they independently collected (by hashing it and comparing), but they cannot derive the phone number from the hash. Selective disclosure on top of zero-PII architecture is defense in depth for credential privacy.

Practical implications for credential design

The availability of selective disclosure changes how credentials should be designed. Instead of issuing narrow, single-purpose credentials (one for age, one for employment, one for address), issuers can create rich, multi-claim credentials where the holder controls what is revealed per transaction.

ApproachCredentials NeededIssuer InteractionsHolder Control
Single-purpose JWTs6 separate credentials6 issuance ceremoniesNone (all-or-nothing)
SD-JWT-VC (12 claims)1 credential1 issuance ceremonyPer-claim, per-verifier

This reduces issuance overhead, simplifies wallet management, and—most importantly—puts the holder in control of their data at presentation time rather than issuance time.

Batch presentations and status binding

SD-JWT-VC as implemented today handles single-credential selective presentation. The PBWallet SDK already supports batch VP token presentation (up to 20 tokens per request) and OID4VCI redemption, enabling holders to present multiple SD-JWT-VCs in a single OpenID4VP exchange.

The next phase integrates StatusList2021 revocation with selective disclosure—ensuring that a revoked credential's status can be checked without revealing which claims were disclosed in prior presentations. This is the intersection of privacy and auditability that enterprise deployments demand.

Selective disclosure is the minimum viable architecture for credential privacy in modern identity systems, where no verifier needs, or should receive, every claim.

Start free · Test the API