| name | verifiable-credentials |
| description | Verify and sign RDF graphs with W3C Data Integrity (eddsa-rdfc-2022 — standard Ed25519 over RDFC-1.0 + SHA-256) and resolve did:key/did:web to a verification key, with the opt-in sparq-vc crate. Use when you trust the SIGNER's key but need tamper-evidence / non-repudiation over an RDF answer (audit trails, data marketplaces, journalistic provenance, agent-to-agent exchange) — the standards-interop, vetted-crypto complement to the in-circuit ZK estate (sparq-zk). Covers sign/verify (sign_graph/verify_graph), ProofConfig binding, DidKeyResolver, and the pluggable did:web DidDocumentFetcher. |
| license | MIT |
| metadata | {"version":"0.1.0","homepage":"https://github.com/jeswr/sparq"} |
sparq verifiable-credentials (Data Integrity)
sparq-vc implements the W3C Data Integrity
eddsa-rdfc-2022 cryptosuite: standard Ed25519
(RFC 8032) signatures over the RDFC-1.0 canonical form
sparq already has (sparq-canon), with did:key/did:web resolution.
This is the trust-the-signer model — the ~90% case where the consumer trusts the
signing key but needs tamper-evidence and non-repudiation. It is the
standards-interoperable, vetted-crypto complement to the ZK estate (sparq-zk),
whose Schnorr-over-Baby-JubJub signatures are deliberately non-interoperable.
Honest boundary (load-bearing). eddsa-rdfc-2022 is authenticity + integrity +
non-repudiation only — NOT confidentiality, NOT zero-knowledge, NOT
selective disclosure. It proves who signed and that nothing changed, and reveals
the full signed content to the verifier. This surface makes no privacy claim.
Unlinkable / selective-disclosure presentation is the ZK estate's job (or a later
bbs-2023 phase). did:key is self-certifying (the DID is the key) — a stable
identifier, not a trust anchor; did:web's root of trust is whoever controls the host.
Prerequisites
sparq-vc is an opt-in, publish = false workspace member — nothing in sparq's
default build depends on it. Add it as a path dependency:
sparq-vc = { path = "crates/sparq-vc" }
sparq-vc = { path = "crates/sparq-vc", features = ["did-web"] }
No external toolchain is required (unlike the ZK estate's nargo/bb): Ed25519 is the
vetted ed25519-dalek crate.
Quickstart — sign then verify a graph
use sparq_vc::{sign, verify, ProofConfig, SigningKey, did::DidKeyResolver};
use oxrdf::{Triple, NamedNode, NamedOrBlankNode, Term, Literal};
let key = SigningKey::generate();
let did = key.did_key();
let vm = format!("{did}#{}", did.strip_prefix("did:key:").unwrap());
let triples = vec![Triple::new(
NamedOrBlankNode::NamedNode(NamedNode::new("http://ex/s").unwrap()),
NamedNode::new("http://ex/p").unwrap(),
Term::Literal(Literal::new_simple_literal("v")),
)];
let cfg = ProofConfig::new(vm)
.with_created("2026-06-22T12:00:00Z")
.with_challenge("nonce-from-verifier");
let proof = sign(&triples, &key, &cfg).unwrap();
let verified = verify(&triples, &proof, &DidKeyResolver).unwrap();
assert_eq!(verified.verification_method, cfg.verification_method);
Signing a stored graph / store
sign_graph / verify_graph take a sparq_core::Graph directly (materialize a named
graph or a whole store, then attest it):
use sparq_core::Graph;
use sparq_vc::{sign_graph, verify_graph, ProofConfig, SigningKey, did::DidKeyResolver};
let g = Graph::load_str("<http://ex/s> <http://ex/p> \"v\" .", "ntriples").unwrap();
let key = SigningKey::generate();
let did = key.did_key();
let cfg = ProofConfig::new(format!("{did}#{}", did.strip_prefix("did:key:").unwrap()));
let proof = sign_graph(&g, &key, &cfg).unwrap();
verify_graph(&g, &proof, &DidKeyResolver).unwrap();
A SELECT result-set is signed the same way: materialize it as a CONSTRUCT graph (or any
canonical RDF encoding) and sign the triples.
What binds, and what fails closed
The signature covers SHA-256(canon(proofConfig)) ‖ SHA-256(canon(document)) (proof
config first, per the spec). Consequences you can rely on:
- Isomorphism-stable. A blank-node relabelling or triple reorder of the same graph
canonicalizes identically, so the proof still verifies — order/labels do not matter.
- Tamper-evident. Any content change (one literal, one triple) ⇒
SignatureInvalid.
- Config-bound. The
ProofConfig (verificationMethod, purpose, created, domain,
challenge) is hashed in too, so a verifier expecting a different challenge/domain
than was signed also gets SignatureInvalid — replay / audience binding.
- Key-bound. A signature from a key other than the one the
verificationMethod
resolves to fails closed.
DID resolution
did:key (DidKeyResolver, default) — offline, self-certifying. The Ed25519 key
is encoded in the DID (z-base58btc over the 0xed01 multicodec). did_key_for(&vk)
mints it; the resolver decodes it. Only standard Ed25519 is accepted — a published
W3C did:key example round-trips byte-identically (regression-tested).
did:web (DidWebResolver, did-web feature) — document-fetched, host-rooted.
Maps the DID to its https://…/did.json URL and reads a verification method's
publicKeyMultibase. The HTTP fetch is a pluggable DidDocumentFetcher — this
crate ships no HTTP client, so it forces no reqwest/ureq on anyone and stays
offline-testable (an in-memory map is a valid fetcher).
Not to be confused with sparq-trust::did, which resolves to a sparq-private
Baby-JubJub key for the ZK scheme and rejects the standard Ed25519 z6Mk… form. That
resolver is for the custom ZK estate; this one is the W3C-interoperable resolver.
Scope (v1, honest)
- Cryptosuite:
eddsa-rdfc-2022 only. bbs-2023 (selective disclosure) is out of
scope — it is the ZK estate's selective-disclosure story.
- Inputs are RDF datasets.
sparq-vc operates over the RDF-dataset form of a
credential + its proof config (exactly what RDFC-1.0 canonicalizes). Transforming a
JSON-LD credential to RDF (context expansion) is the caller's job — e.g. via the
engine's JSON-LD parser — to keep the lean build free of a JSON-LD context processor.
See also
zk-query-proofs — the zero-knowledge / selective-disclosure complement (in-circuit, nargo/bb).
rdf-canon — the RDFC-1.0 canonical unit the proof binds to.
prov-lineage — W3C PROV-O lineage for derived graphs.