Skip to content

Fix RDF canonicalization and selective disclosure over Data Integrity proof sets - #709

Merged
Juliano1612 merged 2 commits into
mainfrom
acc-1981-fix-data-integrity-sd-canonicalization
Sep 14, 2026
Merged

Juliano1612 merged 2 commits into
mainfrom
acc-1981-fix-data-integrity-sd-canonicalization

Conversation

@Juliano1612

Copy link
Copy Markdown
Member

Description

Fixes several issues found while verifying and deriving OpenBadge v3 credentials issued by other Data Integrity implementations.

  • Canonicalization now serializes claims through json-ld to_rdf. The linked_data path rewrote context-coerced datatypes (for example https://www.w3.org/2001/XMLSchema#boolean) to the http form, so signatures over such quads never verified. Applies to the standard suites, ecdsa-sd-2023 and bbs-2023.
  • Expansion stays strict through a shared policy in ssi-json-ld. Undefined terms raise an error as required by Data Integrity 2.4.3, never silently dropped.
  • Label replacement canonicalization sorts by N-Quad string form, matching RDFC-1.0 and the existing verifier. The structural sort diverged when IRI and blank node subjects were mixed and broke mandatory indexes.
  • URDNA2015 hash n-degree quads step 5.4.4.1 appends an already-canonical identifier to the path instead of recursing. Verified against the W3C rdf-canon suite.
  • DataIntegrity::select picks the single selective proof from a proof set and ignores full-document signatures. New SelectiveCryptographicSuite::is_selective has a default of true, so existing implementors are unaffected.

Compatibility

  • No public signatures change. Select returns MissingProof instead of NonSelectiveSuite or AmbiguousProof when no selective proof is present.
  • The RDFC transformation impl now requires T: Serialize, which all in-tree claim types already satisfy.

Optional section

  • Reviewers, please pay special attention to the strict expansion policy in ssi-json-ld and its use in canonicalization.rs, ecdsa_sd_2023/transformation/derive.rs and bbs_2023/transformation/derived.rs.
  • Also worth a close look: the sort change in sd-primitives/canonicalize.rs and the proof selection change in core/src/lib.rs, since both affect which quads the mandatory indexes point at.
  • This PR exceeds 500 lines because of test fixtures, not code. Around 2,100 of the 2,700 added lines are JSON-LD contexts bundled for offline tests, two OpenBadge credentials, a multi-proof credential and a 672-quad canonical N-Quads file. Source changes are under 200 lines across nine files.

Tested

  • Undefined term rejection for signing, verification and derived SD verification.
  • OpenBadge credentials with coerced datatypes, full-document and SD proofs, verified offline.
  • Multi-proof credential derivation and verification.
  • Canonicalization idempotency on symmetric blank node lists, cross-checked with jsonld.js canonize.

@timothee-haudebourg timothee-haudebourg left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving, with just a request to select the selective proof between all the proofs, not just the first two.

Comment on lines +93 to +107
// A proof set may pair full-document signatures (e.g. `ecdsa-rdfc-2019`)
// with one SD base proof (e.g. `ecdsa-sd-2023`). Only the SD proof can
// be derived from, so select it and ignore the others.
let mut selective = self.proofs.iter().filter(|p| p.suite().is_selective());
match (selective.next(), selective.next()) {
(Some(proof), None) => {
proof
.suite()
.select(&self.claims, proof.borrowed(), params, options)
.await
}
Some(_) => Err(SelectionError::AmbiguousProof),
None => Err(SelectionError::MissingProof),
// More than one derivable proof.
(Some(_), Some(_)) => Err(SelectionError::AmbiguousProof),
// No derivable proof (empty set, or only full-document signatures).
(None, _) => Err(SelectionError::MissingProof),

@timothee-haudebourg timothee-haudebourg Sep 14, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm this is weird. Why do you only look at the first two proofs? You should iterate over all the proofs:

let mut selected = None;

for proof in &self.proofs {
  if proof.suite().is_selective() && selected.replace(proof).is_some() {
    return Err(AmbiguousProof)
  }
}

select.ok_or(MissingProof).select(...).await

Comment on lines +59 to +62
// Sort by N-Quad serialization, as RDFC-1.0 and the verifier do. The
// structural `LexicalQuad` order differs when IRI and blank-node subjects
// mix (`_` sorts after `<`), which would desync `mandatory_indexes`.
canonical_quads.sort_by_cached_key(|quad| ssi_rdf::NQuadsStatement(quad).to_string());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds like this is an issue with rdf-types more than anything. Let's keep it as a work around for now, but can you flag that?

Comment thread crates/rdf/src/urdna2015.rs Outdated
Comment on lines +440 to +443
// An already-canonical `related` is appended to the path as-is.
// Recursing on it instead re-issues a temporary identifier in
// 5.4.5 and corrupts the path (wrong labels for symmetric
// blank-node structures, e.g. repeated `@list`s).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sounds like an AI comment. You don't need to justify why you don't do something.

.as_ref()
{
recursion_list.push((*canonical_identifier).to_owned());
path.push_str(canonical_identifier.as_str());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I double checked the spec and indeed it should be added to path. I see no version of the spec that says to add to recursion_list, so it's a genuine and probably old bug that we have in there. I don't even know how we missed it with all the tests that we have, we must be lucky.

@Juliano1612
Juliano1612 merged commit d7593b9 into main Sep 14, 2026
3 checks passed
@Juliano1612
Juliano1612 deleted the acc-1981-fix-data-integrity-sd-canonicalization branch September 14, 2026 16:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants