jeswr / jeswr/solid-vc

Decision: safeHttpIri is now LEXICAL-PRESERVING (aligned to the @jeswr/rdf-serialize invariant)

Open
#6 3 comments 0 reactions 0 assignees View on GitHub
Dominant language
No language data
Stars
0
Forks
0
PR merge metrics
No merged PRs in 30d

Description

## Summary

`safeHttpIri` previously **canonicalised** http(s) IRIs via `new URL().href`. That contradicts the single suite-wide **lexical-preserving** IRI invariant that `@jeswr/rdf-serialize`'s `escapeIri` establishes (the iri-consolidation, suite-tracker-3juf), and — because these IRIs end up in the RDF graph that gets canonicalised and **signed** — it was deliberately kept local (only `escapeIri` was consolidated) pending a bounded engineering decision.

**Decision (suite-tracker-c77v): adopt option (a) — make `safeHttpIri` lexical-preserving.** It validates the http(s) scheme via `new URL()` but **discards** the canonicalised `.href`, returning `escapeIri(value)` — the IRI is preserved byte-for-byte and only Turtle-IRIREF injection is neutralised.

Landed on `fix/iri-lexical-safehttpiri` (not yet merged; awaiting the orchestrator's verify + roborev gate).

## Where `safeHttpIri` touched a signed / canonicalised graph

Via `safeObjectIri` / `requireObjectIri` in `credentialToRdf` (the pre-image the Data Integrity proof is computed over):

- the credential **`issuer`** (identity-bearing, `requireObjectIri`, fail-closed);
- each credential **`type`** IRI (`safeObjectIri`);
- **`relatedResource.id`** (the G1 policy-content binding);
- **`credentialStatus.statusListCredential`** and **`credentialStatus.id`**.

And via `safeHttpIri` **directly** in `src/webid.ts`: the published / resolved WebID **controller** + **verification-method** IRIs (`publishVerificationMethod`, `resolveWebIdKey`, `createWebIdKeyResolver`).

Note the credential/subject `id`, claim-value IRIs, and the **proof `verificationMethod`** already went through `escapeIri` (lexical) at the `GraphBuilder` write chokepoint — this change brings `issuer` et al. into line with them.

## Can canonicalisation change a real issuer's IRI?

Yes. `new URL().href` strips a default `:443`/`:80` port, lower-cases the scheme+host, resolves dot-segments, and **inserts a trailing `/` into an empty path** (`https://alice.example#me` → `https://alice.example/#me`). A real issuer can legitimately supply any of these, so the two behaviours genuinely differ.

## Why (a) and not (b) keep-canonicalising

The canonicalisation was a **latent bug**, not a feature:

1. **Turtle ⇄ JSON-LD lock-step was broken.** `credentialToRdf` canonicalised the issuer, but `credentialToJsonLd` emits it **verbatim** (and the subject id / claims / `verificationMethod` are all lexical). So a non-canonical issuer produced **disagreeing** RDF and JSON-LD projections of the *same* credential — the exact byte-for-byte lock-step the code elsewhere works hard to preserve.
2. **External interop.** Standard JSON-LD→RDF processing does **not** URL-canonicalise absolute IRIs, so a third party parsing the emitted JSON-LD would get the lexical form and, over RDFC-1.0, hash different bytes than our canonicalised signing pre-image.
3. **One invariant across the suite** (matching `@jeswr/rdf-serialize`), rather than a per-field carve-out.

Choosing (b) would instead have required canonicalising **everywhere** (subject id, claims, `verificationMethod`) to restore lock-step — a larger, more surprising change that still diverges from external verifiers.

## Why it does NOT break verification of already-issued VCs

`issue()` and `verifyCredential()` both lower through the **same** `credentialToRdf` (`verify.ts` re-lowers `unsigned(vc)`), so the round-trip is **invariant** to whether `safeHttpIri` canonicalises or preserves — a credential signed then verified matches **either way**. The change is only observable as different **absolute** bytes for a non-canonical input URL.

Evidence:
- The captured **characterization goldens** (canonical N-Quads, the 64-byte signing pre-image hash, the Ed25519 `proofValue`, the Turtle) use only canonical IRIs and are **byte-identical** after the change.
- New `test/iri-lexical-identity.test.ts` (15 tests): re-verification round-trip over non-canonical `issuer` / `relatedResource` / `credentialStatus` / `type` IRIs (`verified: true`); signed-RDF lexical regression guards (`:443` preserved, canonical form absent); the Turtle⇄JSON-LD issuer lock-step; and a WebID publish⇄resolve round-trip under a non-canonical controller/key id.
- WebID publish/resolve stay **symmetric** (both sides lexical); the network fetch target is still canonicalised in `documentUrlOf` (correct — it's a transport URL, not an RDF term).
- No persisted cross-version signed-credential/WebID-doc corpus exists (experimental package).
- Injection stays fully closed: `escapeIri` percent-encodes the whole Turtle-IRIREF forbidden set (C0 controls + space + `` <>"{}|^`\ ``), a strict superset of what `new URL()` handled.

## Alternative kept on record (option b)

Keep `new URL().href` canonicalisation as "correct-for-VC" and document a deliberate carve-out from the rdf-serialize invariant — arguing VC signing *wants* normalised IRIs so that `https://x:443/#me` and `https://x/#me` sign identically. **Rejected** because (i) it breaks the internal Turtle⇄JSON-LD lock-step unless canonicalisation is applied to *every* IRI field (much larger change), and (ii) it diverges from external W3C verifiers, which don't URL-canonicalise. Recorded here so the maintainer can steer back to (b) if the interop trade-off is judged differently.

## Gate

lint + typecheck + **255 tests** + build + `check:dist` + `api:report` (public API unchanged) all green.

---
🤖 PSS agent — @jeswr's agent for `prod-solid-server` / the Solid app+Pod-Manager suite. Model: claude-opus-4-8.

Contributor guide

No contributing guide indexed for this repository

Research direction

Start with src/webid.ts and test/iri-lexical-identity.test.ts, then inspect the existing fix/iri-lexical-safehttpiri work described in the issue. Confirm the lexical-preserving behavior across credential and WebID paths, and run lint, typecheck, the 255 tests, build, check:dist, and api:report to verify completion.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
cryptography, security, testing
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Clearly specified
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.