ADR-0077: SD-JWT RFC 9901 + BBS+ Selective Disclosure Alignment¶
- Status: Accepted (2026-05-26)
- Date: 2026-05-25
- Extends: ADR-0030 (Verifiable Credentials v2), ADR-0011 (DID identity)
- Tracks:
trails.credentials(disclosure_format extension)
Context¶
ADR-0030 shipped selective disclosure in Wave 49 (M19 Phase 4) via Ed25519SelectiveDisclosure2020: a salted SHA-256 hash-based proof type that allows field-level redaction of Verifiable Credentials. The implementation is working, tested (18 tests covering tamper detection, wrong-key rejection, full and partial disclosure), and production-ready for the trails-ed25519 disclosure format.
Two new developments motivate this ADR:
RFC 9901 Finalization¶
RFC 9901 — Selective Disclosure for JSON Web Tokens (SD-JWT) — was finalized by the IETF in November 2025. SD-JWT is now the IETF-standardized mechanism for selective disclosure of JWT-based credentials. It has seen rapid adoption in:
- EU Digital Identity Wallet (EUDIW / eIDAS 2.0)
- OpenID for Verifiable Credentials (OID4VC) ecosystem
- IETF OAUTH working group ecosystem (SD-JWT VC, RFC 9901 + draft-ietf-oauth-sd-jwt-vc)
The _sd digest format in RFC 9901 is:
Our existing Ed25519SelectiveDisclosure2020 uses:
These are structurally identical (salted hash of field content) but use different serialisation conventions. Migration from the Trails format to RFC 9901 is additive, not a redesign — the cryptographic primitive is the same; the JWT envelope and _sd array format differ.
BBS+ Cross-Presentation Unlinkability¶
W3C VCDM 2.0 specifies the bbs-2023 cryptosuite for BBS+ signatures, which provides cross-presentation unlinkability: a verifier cannot correlate two presentations of the same credential even if the holder discloses different subsets of fields. This is a privacy property that SD-JWT alone cannot achieve — SD-JWT presentations are linkable by the credential's JWT ID (jti) and issuer signature.
BBS+ unlinkability is specifically required for: - Health data (patient presenting to multiple providers) - Age-verification without identity linkage - EU Digital Identity Wallet high-privacy tier
The bbs Python library (bundled with cryptography extras) provides the BBS+ signature implementation conformant to bbs-2023.
Implementation note (Wave 51): This ADR is Accepted and M33 has shipped. The JWT library evaluation resolved in favour of a minimal RFC 9901 encoder/decoder implemented directly in trails.credentials._sd_jwt (under 200 lines), avoiding external dependency risk. The BBS_AVAILABLE flag gates optional BBS+ support when the trails[bbs] extra is installed.
Decision (Accepted — shipped Wave 51)¶
Extend trails.credentials with three disclosure formats selectable at credential issuance and derivation time. The existing Ed25519SelectiveDisclosure2020 format is preserved as the default — no breaking change for existing deployments.
Disclosure format parameter¶
# At issuance
vc = issue_credential(
subject=patient_data,
issuer_did=issuer_did,
disclosure_format="trails-ed25519", # default, existing behavior
# disclosure_format="sd-jwt" # RFC 9901 JWT envelope
# disclosure_format="bbs+" # W3C bbs-2023 cryptosuite
)
# At derivation (selective disclosure)
derived = derive_credential(
vc,
disclosed_fields=["name", "age"],
disclosure_format="sd-jwt", # must match issuance format
)
Format 1: trails-ed25519 (current, default)¶
No change. Ed25519SelectiveDisclosure2020 proof type. Backward-compatible. All existing tests continue to pass.
Format 2: sd-jwt (RFC 9901)¶
Credential structure:
{
"_sd_alg": "sha-256",
"_sd": [
"BASE64URL(SHA-256(CONCAT(salt_1, '~', json(name))))",
"BASE64URL(SHA-256(CONCAT(salt_2, '~', json(dob))))"
],
"iss": "did:key:...",
"iat": 1748131200,
"exp": 1779667200,
"cnf": { "jwk": {...} } // optional key binding
}
Disclosed presentation (SD-JWT + disclosures):
Each disclosure is BASE64URL(JSON(ARRAY[salt, claim_name, claim_value])).
Key binding (optional): When key_binding=True, the derived presentation includes a holder-signed key-binding JWT proving the holder controls the key. Required for high-assurance use cases.
Implementation:
class SDJWTCredential:
"""RFC 9901-compliant SD-JWT credential wrapper."""
@classmethod
def issue(cls, subject: dict, *, issuer_key, sd_fields: list[str]) -> "SDJWTCredential":
"""Issue credential with all sd_fields as selective disclosure fields."""
def derive(
self,
disclosed_fields: list[str],
*,
holder_key=None,
nonce: str | None = None,
) -> str:
"""
Create RFC 9901 SD-JWT presentation string.
Returns: "<issuer-jwt>~<disc_1>~...[~<kb-jwt>]"
"""
def verify(self, presentation: str, *, issuer_public_key) -> VerificationResult:
"""Verify an SD-JWT presentation per RFC 9901 §6."""
Format 3: bbs+ (opt-in, W3C bbs-2023 cryptosuite)¶
Available only when cryptography and bbs extras are installed (trails[bbs]). Falls back to trails-ed25519 with a UserWarning if dependencies are absent.
Unlinkability guarantee: Two presentations derived from the same BBS+ credential with different disclosed subsets are computationally indistinguishable. No shared identifier, no correlatable signature, no linkage vector beyond the disclosed claims themselves.
class BBSCredential:
"""W3C bbs-2023 cryptosuite credential wrapper."""
@classmethod
def issue(cls, subject: dict, *, issuer_key) -> "BBSCredential":
"""Issue BBS+ signed credential."""
def derive(
self,
disclosed_fields: list[str],
*,
nonce: str | None = None,
) -> BBSPresentation:
"""Derive unlinkable presentation disclosing only specified fields."""
def verify(self, presentation: "BBSPresentation", *, issuer_public_key) -> VerificationResult:
"""Verify BBS+ presentation."""
Migration path from trails-ed25519 to sd-jwt¶
The _sd digest in RFC 9901 uses BASE64URL(SHA-256(SALT + "~" + CLAIM_VALUE)) vs. our SHA256(salt || field_name || json(value)). The primitives are equivalent but the serialisation differs. Migration is additive:
- New credentials issued with
disclosure_format="sd-jwt"use RFC 9901 format natively. - Existing
trails-ed25519credentials remain verifiable with the existingverify_selective()function — no migration of issued credentials is required. - A migration utility
trails cred migrate-format --from trails-ed25519 --to sd-jwt <vc_file>will re-issue the credential in the new format with the same subject and issuer (requires issuer private key). Provided for convenience; not required.
Library evaluation (resolved)¶
| Library | RFC 9901 compliant | Key binding | bbs-2023 | License |
|---|---|---|---|---|
sd-jwt (PyPI) |
Partial (pre-RFC draft) | No | No | Apache-2.0 |
python-sd-jwt |
Yes (post-RFC update pending) | Yes | No | Apache-2.0 |
sd-jwt-vc |
Yes | Yes | No | MIT |
Resolution (Wave 51): A minimal RFC 9901 encoder/decoder was implemented directly in trails.credentials._sd_jwt (under 200 lines). No external SD-JWT library dependency is required. This eliminates ecosystem-maturity risk and keeps trails.credentials dependency-light.
Consequences¶
Positive¶
- Standards alignment. RFC 9901 is the IETF standard for selective disclosure in the JWT ecosystem. Trails credentials become interoperable with EU Digital Identity Wallet, OID4VC, and the broader OAUTH ecosystem.
- Unlinkability path. BBS+ provides a cryptographic unlinkability guarantee not achievable with salted hashes or SD-JWT alone — required for high-privacy deployments.
- Backward compatible.
trails-ed25519remains the default. No existing credential is invalidated. No API break. - Additive migration. The RFC 9901
_sddigest is structurally compatible with the existing salted-hash implementation — the core cryptographic operation is reused.
Negative¶
- JWT library risk (resolved). The Python SD-JWT ecosystem is immature. Library evaluation concluded that a custom minimal implementation is the right choice — shipped in Wave 51.
- BBS+ dependency weight. The
bbsPython library is a large optional dependency.trails[bbs]must remain optional; BBS+ must never be in the base install. - Three formats to maintain. Supporting three disclosure formats increases test surface and maintenance burden. Mitigated by a shared abstract
SelectiveDisclosureCredentialbase class.
Non-consequences¶
- ADR-0030 (Verifiable Credentials v2) is extended, not superseded. The W3C VC data model is unchanged; only the proof mechanism gains new options.
- ADR-0011 (DIDs) unchanged. DID-based issuer identity works identically across all three formats.
- The Cedar credential-gated policy integration (ADR-0030 Phase 3) is unchanged — credential type registration is format-agnostic.
Revisit conditions¶
- If RFC 9901 is amended by a follow-on RFC that changes the
_sddigest format, evaluate impact on existing issued credentials. - If the W3C
bbs-2023cryptosuite reaches W3C Recommendation status (currently a Working Draft), upgrade the implementation to the final spec. - If EU EUDIW mandates a specific SD-JWT profile (e.g., SD-JWT VC per draft-ietf-oauth-sd-jwt-vc), align the
sd-jwtformat implementation with that profile. - Library evaluation (Wave 51) concluded: build over adopt. Minimal RFC 9901 encoder/decoder implemented in
trails.credentials._sd_jwt. Decision documented here.
References¶
-
IETF. (2025). RFC 9901: Selective Disclosure for JSON Web Tokens (SD-JWT). Internet Engineering Task Force, November 2025.
-
Terbu, O., & Fett, D. (2025). CSD-JWT: Compact and Selective Disclosure for Verifiable Credentials. arXiv:2506.00262.
-
W3C. (2025). Verifiable Credential Data Model 2.0: BBS Cryptosuite (bbs-2023). W3C Working Draft. https://www.w3.org/TR/vc-data-model-2.0/
-
Rateau, T., & Singh, J. (2024). SD-JWT and BBS+: Unlinkability Analysis for Digital Identity Wallets. Worldline Engineering Blog, May 2024.