open-standards/smart-byte/smart-byte-rs/crates/smart-byte-vc/tests/w3c_vectors.rs
Transaction Science 35bb8c9910 smart-byte: the envelope wire format, v1 key-event logs, and their vectors
smart-byte-core
- `wire`: canonical CBOR for any serde type. `from_slice` refuses bytes that
  are not the decoded object's own encoding. Serde otherwise accepts an array
  of integers for a byte string, a byte string for text, and an array for a
  struct, each a second encoding the SAID does not cover.
- Controller AIDs. Controller signatures are checked against the key states
  replayed logs establish. One strict Ed25519 rule.
- The envelope: attested provenance, ownership chain and joule cost. Cargo
  carries a registered type tag.

smart-byte-keri-witness
- Events are §2 SAIDs over canonical CBOR.
- The default verifier is spec v1. It replays one event at a time and says
  which event it refused. It refuses `dip`, which skipped the pre-rotation
  check entirely, plus zero thresholds, duplicate keys, rotations after an
  empty `n`, and extension members.
- Extension mode, fixed:
  - Witness receipts carried their own key, so anyone could receipt in any
    witness's name. Witnesses are now named by their keys, and receipts sign
    everything they claim.
  - Duplicity evidence was gathered from unverified receipts.
  - Recovery accepted any keys; it now needs its witnesses' receipts.
  - OOBIs were signed by an embedded key; they are now controller signatures.
  - Configuration traits were never enforced, so they are gone.

VC, BBS+, CRDT, mDL, lockstep, libp2p and the CLI move to the new envelope.

smart-byte-conformance
- kel_vectors.json: 25 vectors.
- envelope_vectors.json: 18 vectors, checked against the logs' key states.
- The SAID and signature vectors are rebuilt on the real objects, so all the
  files describe one set of parties.
- Planted defects in every file are caught on their own vectors.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-30 23:00:57 -04:00

225 lines
7.7 KiB
Rust

//! Integration tests against W3C VCDM 2.0 test vectors and the
//! VC-JWT / SD-JWT / Status List / cargo-bridge end-to-end paths.
use chrono::TimeZone;
use ed25519_dalek::SigningKey;
use rand::rngs::OsRng;
use serde_json::json;
use smart_byte_core::{Aid, JouleCost, KeyBook, Said, Signer};
use smart_byte_vc::{
BitstringStatusList, CredentialSubject, Disclosure, Holder, Issuer,
StatusListCredential, StatusPurpose, VC_CARGO_TYPE, VcBuilder,
VerifiableCredential, VerifiablePresentation, check_status,
issue_data_integrity, issue_sd_jwt, issue_vc_jwt, present_sd_jwt,
vc_envelope, vc_from_envelope, verify_data_integrity, verify_sd_jwt,
verify_vc_jwt,
};
fn load_fixture(name: &str) -> VerifiableCredential {
let path = format!(
"{}/tests/fixtures/{}",
env!("CARGO_MANIFEST_DIR"),
name
);
let bytes = std::fs::read(&path).expect("fixture read");
serde_json::from_slice(&bytes).expect("fixture parse")
}
#[test]
fn vcdm_v2_example_roundtrips() {
let vc = load_fixture("vcdm_v2_example.json");
vc.validate_shape().expect("shape");
let jcs_a = vc.to_jcs().expect("jcs a");
let again: VerifiableCredential =
serde_json::from_slice(&jcs_a).expect("reparse");
let jcs_b = again.to_jcs().expect("jcs b");
assert_eq!(jcs_a, jcs_b, "JCS round-trip must be stable");
assert_eq!(vc, again, "structural round-trip");
assert_eq!(vc.issuer.id().as_str(), "https://university.example/issuers/565049");
}
#[test]
fn vcdm_v2_data_integrity_sign_and_verify() {
let vc = load_fixture("vcdm_v2_example.json");
let sk = SigningKey::generate(&mut OsRng);
let vk = sk.verifying_key();
let signed = issue_data_integrity(
vc,
"did:example:issuer#keys-1".parse().expect("iri"),
chrono::Utc.with_ymd_and_hms(2026, 5, 23, 12, 0, 0).single().expect("ts"),
&sk,
)
.expect("issue DI");
verify_data_integrity(&signed, &vk).expect("verify DI");
}
#[test]
fn vc_jwt_known_key_roundtrip() {
let vc = load_fixture("vcdm_v2_example.json");
let sk = SigningKey::from_bytes(&[7u8; 32]);
let vk = sk.verifying_key();
let (jwt, proof) = issue_vc_jwt(
&vc,
&sk,
Some("did:example:issuer#keys-1".to_string()),
)
.expect("issue jwt");
assert!(jwt.split('.').count() == 3);
assert_eq!(proof.type_, "JwtProof2020");
let decoded = verify_vc_jwt(&jwt, &vk).expect("verify jwt");
assert_eq!(decoded, vc);
}
#[test]
fn sd_jwt_discloses_two_of_five() {
let sk = SigningKey::generate(&mut OsRng);
let vk = sk.verifying_key();
let payload = json!({
"given_name": "Alice",
"family_name": "Lockhart",
"birthdate": "1990-01-15",
"email": "[email protected]",
"country": "GB",
"iss": "did:example:issuer",
});
let (combined, disclosures, _proof) = issue_sd_jwt(
&payload,
&[
"given_name",
"family_name",
"birthdate",
"email",
"country",
],
&sk,
Some("did:example:issuer#keys-1".to_string()),
)
.expect("issue sd-jwt");
assert_eq!(disclosures.len(), 5);
let presented = present_sd_jwt(&combined, &["given_name", "country"])
.expect("holder present");
let merged = verify_sd_jwt(&presented, &vk).expect("verify presented");
assert_eq!(merged["given_name"], "Alice");
assert_eq!(merged["country"], "GB");
assert!(merged.get("family_name").is_none());
assert!(merged.get("birthdate").is_none());
assert!(merged.get("email").is_none());
}
#[test]
fn sd_jwt_disclosure_helpers_are_consistent() {
let d = Disclosure {
salt: "_26bc4LT-ac6q2KI6cBW5es".into(),
name: "family_name".into(),
value: json!("Möbius"),
};
let b64 = d.to_base64().expect("encode");
// re-decoding the base64 yields the disclosure JSON array
let bytes = base64::Engine::decode(
&base64::engine::general_purpose::URL_SAFE_NO_PAD,
&b64,
)
.expect("b64 decode");
let arr: serde_json::Value =
serde_json::from_slice(&bytes).expect("disclosure json");
assert_eq!(arr[0], "_26bc4LT-ac6q2KI6cBW5es");
assert_eq!(arr[1], "family_name");
assert_eq!(arr[2], "Möbius");
}
#[test]
fn status_list_revokes_index_42() {
// Issue a fresh status list credential with 1024 indices.
let subj = CredentialSubject {
id: Some("https://example.com/credentials/status/3"
.parse()
.expect("iri")),
claims: serde_json::Map::new(),
};
let status_vc = VcBuilder::new()
.type_tag("BitstringStatusListCredential")
.issuer(Issuer::Uri("did:example:issuer".parse().expect("iri")))
.subject(subj)
.build()
.expect("status vc");
let mut slc = StatusListCredential {
vc: status_vc,
purpose: StatusPurpose::revocation(),
bitstring: BitstringStatusList::new(1024),
};
slc.refresh_subject().expect("initial refresh");
let initial = slc.vc.credential_subject[0]
.claims
.get("encodedList")
.and_then(|v| v.as_str())
.expect("encodedList")
.to_string();
assert!(!check_status(&initial, 1024, 42).expect("check"));
// Revoke index 42.
slc.bitstring.set(42, true).expect("revoke");
slc.refresh_subject().expect("refresh after revoke");
let after = slc.vc.credential_subject[0]
.claims
.get("encodedList")
.and_then(|v| v.as_str())
.expect("encodedList")
.to_string();
assert!(check_status(&after, 1024, 42).expect("check 42"));
assert!(!check_status(&after, 1024, 41).expect("check 41"));
assert!(!check_status(&after, 1024, 43).expect("check 43"));
assert_ne!(initial, after);
}
#[test]
fn status_list_entry_fixture_parses() {
// The fixture carries a credentialStatus pointing at index 42.
let vc = load_fixture("vcdm_v2_with_status.json");
let status = vc.credential_status.as_ref().expect("status");
assert_eq!(status.type_, "BitstringStatusListEntry");
assert_eq!(
status.status_purpose.as_deref(),
Some("revocation")
);
assert_eq!(status.status_list_index.as_deref(), Some("42"));
}
#[test]
fn cargo_bridge_envelopes_a_vc_with_stable_said() {
let vc = load_fixture("vcdm_v2_example.json");
let issuer = Signer::single(
Aid::of_inception(Said::hash(b"vc-issuer")),
0,
SigningKey::from_bytes(&[0x22; 32]),
);
let holder = Aid::of_inception(Said::hash(b"vc-holder"));
let env_a = vc_envelope(&vc, &issuer, holder, [3; 16], JouleCost::measured(11)).expect("envelope a");
let env_b = vc_envelope(&vc, &issuer, holder, [3; 16], JouleCost::measured(11)).expect("envelope b");
// Issuance is deterministic: the same inputs give the same byte.
assert_eq!(env_a.identity(), env_b.identity());
assert_eq!(env_a.said(), env_b.said());
let mut keys = KeyBook::new();
keys.insert(issuer.aid, 0, issuer.key_state());
env_a.verify(&keys).expect("verifies");
// The cargo is registered as a verifiable credential.
assert_eq!(env_a.provenance.cargo.type_tag, VC_CARGO_TYPE);
// Round-trip extraction
let back = vc_from_envelope(&env_a).expect("from envelope");
assert_eq!(back, vc);
}
#[test]
fn presentation_with_holder_and_credential() {
let vc = load_fixture("vcdm_v2_example.json");
let holder = Holder::new("did:example:alice".parse().expect("did"));
let vp = VerifiablePresentation::new()
.expect("vp")
.with_holder(holder.did.clone())
.with_credential(vc);
vp.validate_shape().expect("shape");
assert_eq!(vp.verifiable_credential.len(), 1);
assert_eq!(
vp.holder.as_ref().map(|d| d.to_string()),
Some("did:example:alice".to_string())
);
}