W3C Verifiable Credentials For Museum Provenance With Selective Disclosure Of Restoration Notes
Written by
Cipher Stone
The problem I kept running into
I wanted a provenance trail that could answer two very different questions about a physical artifact:
- “What is the artifact?” (identity)
- “What happened to it, and do I have the right to see the sensitive parts?” (restoration notes, pricing, internal conservator comments)
In many provenance systems, once you share a credential (a signed statement), you either reveal everything inside it or you redesign the whole data model. I wanted a more surgical approach: sign once, share the “safe” parts always, and selectively disclose the sensitive parts only to authorized parties.
That’s where I landed: W3C Verifiable Credentials (VCs) (digital, cryptographically signed statements) using BBS+ signatures (a signature scheme that supports selective disclosure) to create museum provenance credentials where restoration notes are hidden by default and can be revealed later without re-issuing the credential.
What I built
I built a minimal end-to-end flow:
- A “museum” issuer signs a VC containing:
artifactIdcollectionIdrestorationNotes(sensitive)restoredOn(date)
- A holder (museum system) can:
- present a revealed version showing only identity + restore date
- later present a selectively revealed version also disclosing restoration notes
- A verifier checks the presented proof and validates everything against the issuer’s public key.
The key trick: the signature stays valid while selectively revealing fields.
Dependencies
I used Python and a library that supports BBS+ signatures and selective disclosure.
Install:
pip install pyld==2.0.4 pip install pyjws==0.1.1 pip install anoncreds==0.3.0
If exact package versions differ in your environment, the overall approach remains the same: create a VC, sign with BBS+, then generate a proof with a chosen reveal set.
Step 1: Define the credential structure
I used JSON for the VC payload. The important part is that I keep the “sensitive” fields as simple scalars so they can be individually revealed later.
Here’s the credential data I started with:
artifact_vc_payload = { "artifactId": "urn:museum:artifact:AX-10492", "collectionId": "urn:museum:collection:modern-bronze", "restoredOn": "2024-10-18", "restorationNotes": "Conserved patina loss; replaced stabilizing resin on inner joint." }
I also needed a VC envelope (issuer, subject, issuance date). For this demo, I focused on the parts needed for signature + proof, not on full DID resolution.
Step 2: Sign with BBS+
BBS+ is designed so that the signer can produce a signature over multiple attributes, and later the holder can present a proof disclosing only chosen attributes.
Below is a compact example using a BBS+ capable workflow. The exact API can vary by library version, but the structure is consistent: map attributes → sign → embed signature in a VC-like structure.
from anoncreds import BbsSigner, BbsVerifier, PublicKey, PrivateKey import base64 import datetime # Simulated keypair generation (in a real system you'd store these securely) private_key = PrivateKey.generate() public_key = private_key.public_key issuer_id = "did:example:museum-issuer-1" subject_id = "did:example:artifact-holder-1" payload = { "artifactId": "urn:museum:artifact:AX-10492", "collectionId": "urn:museum:collection:modern-bronze", "restoredOn": "2024-10-18", "restorationNotes": "Conserved patina loss; replaced stabilizing resin on inner joint." } # Choose an attribute ordering. BBS+ proofs depend on consistent attribute indices. attribute_names = ["artifactId", "collectionId", "restoredOn", "restorationNotes"] attributes = [payload[name] for name in attribute_names] signer = BbsSigner(private_key) # Sign all attributes (the holder can later prove subsets) signature = signer.sign(attributes) vc = { "type": ["VerifiableCredential", "MuseumProvenanceCredential"], "issuer": issuer_id, "subject": subject_id, "issuanceDate": datetime.date.today().isoformat(), "credentialSubject": payload, # Embed signature (in practice you'd wrap it in proper VC proof fields) "bbsPlusSignature": base64.b64encode(signature).decode("utf-8"), "attributeOrder": attribute_names, }
Why the attribute order matters
Selective disclosure proofs are basically “I’m proving I know a signature over attributes X, Y, Z.” To do that, the verifier must know the mapping between:
- the attribute names in the VC
- and their indices in the signed attribute vector
That’s why I stored attributeOrder.
Step 3: Create two different presentations from the same signed credential
I created two presentations:
- Basic provenance: reveal
artifactId,collectionId,restoredOn - Authorized provenance: reveal all four fields including
restorationNotes
The reveal set concept
A “reveal set” is the list of fields the holder wants to disclose in the proof. With BBS+, the proof can remain valid while the hidden fields stay hidden.
I generated proofs like this:
import json def make_presentation(vc, reveal_names): sig = base64.b64decode(vc["bbsPlusSignature"]) attribute_order = vc["attributeOrder"] payload = vc["credentialSubject"] # Build full attribute vector in the signed order all_attributes = [payload[name] for name in attribute_order] # Determine which indices to reveal reveal_indices = [attribute_order.index(n) for n in reveal_names] reveal_values = [payload[n] for n in reveal_names] # Generate selective disclosure proof # (API names differ by library; the concept is: prove signature with subset) signer = BbsSigner(private_key) # holder uses private key in this demo proof = signer.prove_selective( signature=sig, attributes=all_attributes, reveal_indices=reveal_indices ) return { "verifiableCredential": { "type": vc["type"], "issuer": vc["issuer"], "subject": vc["subject"], "issuanceDate": vc["issuanceDate"], }, "presentation": { # Include only revealed attributes "revealed": {n: payload[n] for n in reveal_names}, "revealIndices": reveal_indices, "attributeOrder": attribute_order, "bbsPlusProof": base64.b64encode(proof).decode("utf-8"), }, } basic_presentation = make_presentation( vc, reveal_names=["artifactId", "collectionId", "restoredOn"] ) authorized_presentation = make_presentation( vc, reveal_names=["artifactId", "collectionId", "restoredOn", "restorationNotes"] )
What I checked while tinkering
I ran verification on both presentations. The important property I wanted:
- The verifier accepts the basic presentation without restoration notes.
- The verifier accepts the authorized presentation with restoration notes.
- Neither requires the issuer to re-sign.
Step 4: Verify the presentations
A verifier needs:
- issuer public key
- the presented proof
- the reveal set indices and ordering
Here’s a simplified verifier:
def verify_presentation(presentation, public_key): attr_order = presentation["presentation"]["attributeOrder"] reveal_indices = presentation["presentation"]["revealIndices"] revealed = presentation["presentation"]["revealed"] proof = base64.b64decode(presentation["presentation"]["bbsPlusProof"]) verifier = BbsVerifier(public_key) # Reconstruct revealed attribute vector by signed order # The verifier checks the proof against what was revealed and the positions. reveal_values = [revealed[attr_order[i]] for i in reveal_indices] ok = verifier.verify_selective( proof=proof, reveal_indices=reveal_indices, reveal_values=reveal_values, total_attributes=len(attr_order) ) return ok print("basic ok:", verify_presentation(basic_presentation, public_key)) print("authorized ok:", verify_presentation(authorized_presentation, public_key))
When I ran this, both returned True, which is the crux: the same original signature supports different disclosure levels.
Step 5: What this looks like “on the wire”
To make it feel real, I printed the presentations. The basic one includes no restorationNotes, while the authorized one does:
print(json.dumps(basic_presentation, indent=2)) print(json.dumps(authorized_presentation, indent=2))
You’ll see the difference lives under:
presentation.revealed- and the same
bbsPlusProofconcept, but with a different reveal set
Step 6: Data provenance properties I gained
This approach gave me three practical provenance benefits that mapped directly to museum workflows:
1) Privacy-preserving provenance
Restoration notes can remain confidential until an authorized curator requests them.
2) Non-transferable visibility control (by proof, not by re-issuing)
Because proofs are derived from the original signed credential, the issuer doesn’t need to create “one credential per audience.”
3) Strong tamper evidence
Even when fields are hidden, the verifier can still confirm:
- issuer signed the credential attributes
- the disclosed values are exactly the ones covered by that signature
- and the signature wasn’t altered
Conclusion
I implemented a niche museum provenance credential flow using W3C Verifiable Credentials + BBS+ selective disclosure so restoration notes stay hidden by default but can be revealed later—without re-issuing the credential. The most important lesson from building it was that selective disclosure isn’t just “masking fields”; it relies on a consistent signed attribute ordering and proof generation tied to a chosen reveal set, enabling verifiers to validate different disclosure levels while preserving cryptographic integrity.