sd-cwt 0.1.0

Selective Disclosure CBOR Web Token (SD-CWT) helpers for cose2.
Documentation
  • Coverage
  • 98.95%
    94 out of 95 items documented0 out of 53 items with examples
  • Size
  • Source code size: 66.07 kB This is the summed size of all the files inside the crates.io package for this release.
  • Documentation size: 1.29 MB This is the summed size of all files generated by rustdoc for all configured targets
  • Ø build duration
  • this release: 16s Average build duration of successful builds.
  • all releases: 16s Average build duration of successful builds in releases after 2024-10-23.
  • Links
  • Homepage
  • ldclabs/cose2
    1 0 0
  • crates.io
  • Dependencies
  • Versions
  • Owners
  • zensh

sd-cwt

Selective Disclosure CBOR Web Token (SD-CWT) helpers for Rust, built on cose2 and cbor2.

This crate implements the SD-CWT disclosure and redaction mechanics from draft-ietf-spice-sd-cwt-08. It does not replace cose2's COSE signing and verification APIs. Instead, it provides the SD-CWT-specific pieces you compose with cose2::Sign1Message:

  • registered SD-CWT header labels: sd_claims, sd_alg, sd_aead_encrypted_claims, sd_aead, kcwt, CWT_Claims, and typ;
  • simple(59) for redacted_claim_keys, using cbor2::Value::Simple;
  • tag 60 redacted array elements;
  • tag 58 / tag 62 pre-issuance conversion into issued redactions and disclosures;
  • Salted Disclosed Claim encoding, decoding, and SHA-256 hashing;
  • Holder and Verifier restoration modes;
  • AEAD encrypted disclosure wire structures and header helpers.

The crate does not generate randomness and does not implement KBT signing policy for you. Issuers provide 16-byte salts; applications still use cose2 to sign and verify SD-CWT and KBT messages.

Install

[dependencies]
cose2 = "0.3"
sd-cwt = "0.1"

When working from this repository:

[dependencies]
cose2 = { path = ".." }
sd-cwt = { path = "../sd-cwt" }

Basic flow

The example below shows the core SD-CWT flow without real cryptography:

  1. The Holder or client sends a pre-issued claims value with tag 58 redaction requests.
  2. The Issuer converts it into an issued claims value, creating sd_claims.
  3. The Issuer signs the redacted payload with cose2::Sign1Message.
  4. The Holder validates with the strict one-to-one disclosure rule.
  5. The Verifier receives a presentation with only selected disclosures and removes the rest.
use cbor2::Value;
use cose2::{Error, Sign1Message};
use sd_cwt::{
    issue_from_preissuance, restore_payload_from_message, set_disclosures,
    set_sd_alg, set_sd_cwt_typ, RedactionHasher, RestoreMode, Sha256RedactionHasher,
    TO_BE_REDACTED_TAG,
};

# struct DemoSigner;
# impl cose2::Signer for DemoSigner {
#     fn alg(&self) -> Option<cose2::Label> { Some(cose2::iana::AlgorithmEdDSA.into()) }
#     fn sign(&self, data: &[u8]) -> Result<Vec<u8>, Error> { Ok(data.to_vec()) }
# }
# struct DemoVerifier;
# impl cose2::Verifier for DemoVerifier {
#     fn alg(&self) -> Option<cose2::Label> { Some(cose2::iana::AlgorithmEdDSA.into()) }
#     fn verify(&self, data: &[u8], signature: &[u8]) -> Result<(), Error> {
#         if data == signature { Ok(()) } else { Err(Error::verify("signature mismatch")) }
#     }
# }
# fn example() -> Result<(), Error> {
let preissued = Value::Map(vec![
    (Value::from(1), Value::from("https://issuer.example")),
    (
        Value::Tag(TO_BE_REDACTED_TAG, Box::new(Value::from("name"))),
        Value::from("Alice Example"),
    ),
]);

// sd-cwt never generates randomness. Use fresh unpredictable salts in production.
let mut salt_counter = 1u8;
let mut salts = move || {
    let salt = [salt_counter; 16];
    salt_counter += 1;
    salt
};

let issued = issue_from_preissuance(preissued, &mut salts, &Sha256RedactionHasher)?;

let mut msg = Sign1Message::new(Some(cbor2::to_vec(&issued.value)?));
set_sd_cwt_typ(&mut msg.protected);
set_sd_alg(&mut msg.protected, Sha256RedactionHasher.algorithm());
set_disclosures(&mut msg.unprotected, issued.disclosures.as_slice());

let encoded = msg.sign_and_encode(&DemoSigner, None)?;
let verified = Sign1Message::verify_and_decode(&DemoVerifier, &encoded, None)?;
let restored = restore_payload_from_message(&verified, RestoreMode::Holder)?;

assert_eq!(restored.disclosed, 1);
# Ok(())
# }

Run the full example:

cargo run -p sd-cwt --example basic

API by task

Task API
Create simple(59) redacted map key label redacted_claim_keys_label()
Create tag 60 redacted array element redacted_element(hash)
Convert tag 58/62 pre-issuance claims into issued SD-CWT claims issue_from_preissuance(...)
Provide issuance salts Implement SaltGenerator, or pass a FnMut() -> [u8; 16]
Build a Salted Disclosed Claim manually Disclosure::claim, Disclosure::element, Disclosure::decoy
Read or write sd_claims disclosures_from_unprotected, set_disclosures, DisclosureSet::from_unprotected
Read or write sd_alg sd_alg, set_sd_alg, default_hasher_for_sd_alg
Restore as Holder restore_for_holder or restore_payload_from_message(..., RestoreMode::Holder)
Restore as Verifier restore_for_verifier or restore_payload_from_message(..., RestoreMode::Verifier)
Handle AEAD encrypted disclosure metadata AeadEncryptedDisclosure, set_aead_encrypted_disclosures, aead_encrypted_disclosures_from_unprotected

Holder vs Verifier restoration

RestoreMode::Holder is strict. Every redacted claim hash in the payload must have a matching disclosure, and every disclosure must match a redaction. This is the mode for issuance-time Holder validation.

RestoreMode::Verifier accepts partial disclosure presentations. Disclosures must still match a redacted claim hash, but redactions without a selected disclosure are removed from the validated claims set.

In both modes, a disclosure that restores a map key already present at the same level is rejected.

Protocol boundaries

  • sd_claims is an unprotected COSE header parameter. A production presentation needs a Key Binding Token (KBT) to bind the selected disclosures to the Holder.
  • sd_alg defaults to SHA-256 (-16) when omitted. The built-in helper supports SHA-256; profiles using another hash can implement RedactionHasher.
  • AEAD encrypted disclosures are represented by wire structures and header helpers. Key selection, key management, and concrete AEAD encryption or decryption are profile/application responsibilities.
  • Salted disclosures are hashed over their bstr-encoded Salted Disclosed Claim bytes. Disclosure::from_encoded preserves those exact bytes for hashing.

Verification

cargo test -p sd-cwt
cargo run -p sd-cwt --example basic
RUSTDOCFLAGS='-D warnings' cargo doc -p sd-cwt --no-deps