From 6b8dd615fc5a0bb5580505f4307afc6b9c4aad07 Mon Sep 17 00:00:00 2001 From: Reza Maghoul Date: Wed, 27 May 2026 11:07:47 +0200 Subject: [PATCH 1/9] fix(core): round-trip empty witness:{} parameter across implementations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Python and TS implementations serialise an empty `"witness": {}` object in parameters when no witnesses are configured (and `"nextKeyHashes": []` / `"watchers": []` similarly). Two interop bugs followed: 1. NPE on resolve: Gson instantiated WitnessConfig via Unsafe.allocateInstance, skipping the constructor and leaving the internal list null. The first call to isActive() / getWitnesses() then threw NPE on every Python/TS log. 2. SCID, entry hash, and proof verification failed against TS-produced logs because Java re-serialised WitnessConfig as `{"threshold":0,"witnesses":[]}` instead of `{}`, producing a different JCS canonical form. Fix: - Add a no-args constructor to WitnessConfig (Gson now uses Constructor.newInstance, so field initialisers run). - Add WitnessConfigTypeAdapter that emits `{}` when no witnesses are configured and reads either form, matching the spec (§3.7.1) and the Python/TS canonical representation. This also clears two follow-on items in issue #2: the create-side `nextKeyHashes: []` rejection (the existing check already accepts empty lists) and the java-eecc `versionTime must be after previous entry` failures (spec §535-536 mandates strict >, and the failures were a symptom of validation short-circuiting on the NPE above). Interop coverage vendored from swcurran/didwebvh-test-suite@c11fda3 under didwebvh-core/src/test/resources/interop/, exercised by: - InteropEmptyWitnessObjectTest (basic-create/python) - InteropTsBasicUpdateTest (basic-update/ts) - InteropJavaEeccLogsTest (6 java-eecc vectors) Refs: #2 --- .../didwebvh/core/witness/WitnessConfig.java | 15 +++- .../witness/WitnessConfigTypeAdapter.java | 78 +++++++++++++++++++ .../InteropEmptyWitnessObjectTest.java | 53 +++++++++++++ .../core/interop/InteropJavaEeccLogsTest.java | 74 ++++++++++++++++++ .../interop/InteropTsBasicUpdateTest.java | 64 +++++++++++++++ .../src/test/resources/interop/README.md | 25 ++++++ .../interop/basic-create-python/did.jsonl | 1 + .../interop/basic-update-java-eecc/did.jsonl | 2 + .../interop/basic-update-ts/did.jsonl | 2 + .../interop/deactivate-java-eecc/did.jsonl | 2 + .../interop/key-rotation-java-eecc/did.jsonl | 2 + .../interop/multi-update-java-eecc/did.jsonl | 3 + .../interop/services-java-eecc/did.jsonl | 2 + .../witness-update-java-eecc/did.jsonl | 2 + 14 files changed, 323 insertions(+), 2 deletions(-) create mode 100644 didwebvh-core/src/main/java/io/github/decentralizedidentity/didwebvh/core/witness/WitnessConfigTypeAdapter.java create mode 100644 didwebvh-core/src/test/java/io/github/decentralizedidentity/didwebvh/core/interop/InteropEmptyWitnessObjectTest.java create mode 100644 didwebvh-core/src/test/java/io/github/decentralizedidentity/didwebvh/core/interop/InteropJavaEeccLogsTest.java create mode 100644 didwebvh-core/src/test/java/io/github/decentralizedidentity/didwebvh/core/interop/InteropTsBasicUpdateTest.java create mode 100644 didwebvh-core/src/test/resources/interop/README.md create mode 100644 didwebvh-core/src/test/resources/interop/basic-create-python/did.jsonl create mode 100644 didwebvh-core/src/test/resources/interop/basic-update-java-eecc/did.jsonl create mode 100644 didwebvh-core/src/test/resources/interop/basic-update-ts/did.jsonl create mode 100644 didwebvh-core/src/test/resources/interop/deactivate-java-eecc/did.jsonl create mode 100644 didwebvh-core/src/test/resources/interop/key-rotation-java-eecc/did.jsonl create mode 100644 didwebvh-core/src/test/resources/interop/multi-update-java-eecc/did.jsonl create mode 100644 didwebvh-core/src/test/resources/interop/services-java-eecc/did.jsonl create mode 100644 didwebvh-core/src/test/resources/interop/witness-update-java-eecc/did.jsonl diff --git a/didwebvh-core/src/main/java/io/github/decentralizedidentity/didwebvh/core/witness/WitnessConfig.java b/didwebvh-core/src/main/java/io/github/decentralizedidentity/didwebvh/core/witness/WitnessConfig.java index 59f81fc..2355f99 100644 --- a/didwebvh-core/src/main/java/io/github/decentralizedidentity/didwebvh/core/witness/WitnessConfig.java +++ b/didwebvh-core/src/main/java/io/github/decentralizedidentity/didwebvh/core/witness/WitnessConfig.java @@ -1,14 +1,25 @@ package io.github.decentralizedidentity.didwebvh.core.witness; +import com.google.gson.annotations.JsonAdapter; + import java.util.Collections; import java.util.List; import java.util.Objects; /** Witness configuration: a threshold and a list of witnesses. */ +@JsonAdapter(WitnessConfigTypeAdapter.class) public final class WitnessConfig { - private final int threshold; - private final List witnesses; + private int threshold; + private List witnesses = Collections.emptyList(); + + // No-args constructor lets Gson populate the type via reflection without + // hitting sun.misc.Unsafe.allocateInstance, which would skip field + // initializers and leave `witnesses` null. Other implementations + // (Python, TS) serialise an empty `witness: {}` object when no witnesses + // are configured, so the deserializer must produce a usable empty config. + public WitnessConfig() { + } public WitnessConfig(int threshold, List witnesses) { this.threshold = threshold; diff --git a/didwebvh-core/src/main/java/io/github/decentralizedidentity/didwebvh/core/witness/WitnessConfigTypeAdapter.java b/didwebvh-core/src/main/java/io/github/decentralizedidentity/didwebvh/core/witness/WitnessConfigTypeAdapter.java new file mode 100644 index 0000000..d381dc6 --- /dev/null +++ b/didwebvh-core/src/main/java/io/github/decentralizedidentity/didwebvh/core/witness/WitnessConfigTypeAdapter.java @@ -0,0 +1,78 @@ +package io.github.decentralizedidentity.didwebvh.core.witness; + +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * Round-trips the spec's {@code witness} parameter: an empty witness config + * serialises as {@code {}} (matching Python/TS implementations), and a + * configured one as {@code {"threshold":N,"witnesses":[...]}}. Reading + * accepts either form, plus a missing field; writing emits the spec form so + * canonicalised hashes (SCID, entry hash, proof) align across implementations. + */ +final class WitnessConfigTypeAdapter extends TypeAdapter { + + @Override + public void write(JsonWriter out, WitnessConfig value) throws IOException { + if (value == null) { + out.nullValue(); + return; + } + out.beginObject(); + if (value.isActive()) { + out.name("threshold").value(value.getThreshold()); + out.name("witnesses").beginArray(); + for (WitnessEntry entry : value.getWitnesses()) { + out.beginObject(); + out.name("id").value(entry.getId()); + out.endObject(); + } + out.endArray(); + } + out.endObject(); + } + + @Override + public WitnessConfig read(JsonReader in) throws IOException { + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + return null; + } + in.beginObject(); + int threshold = 0; + List witnesses = new ArrayList<>(); + while (in.hasNext()) { + String name = in.nextName(); + if ("threshold".equals(name)) { + threshold = in.nextInt(); + } else if ("witnesses".equals(name)) { + in.beginArray(); + while (in.hasNext()) { + in.beginObject(); + String id = null; + while (in.hasNext()) { + String field = in.nextName(); + if ("id".equals(field)) { + id = in.nextString(); + } else { + in.skipValue(); + } + } + in.endObject(); + witnesses.add(new WitnessEntry(id)); + } + in.endArray(); + } else { + in.skipValue(); + } + } + in.endObject(); + return new WitnessConfig(threshold, witnesses); + } +} diff --git a/didwebvh-core/src/test/java/io/github/decentralizedidentity/didwebvh/core/interop/InteropEmptyWitnessObjectTest.java b/didwebvh-core/src/test/java/io/github/decentralizedidentity/didwebvh/core/interop/InteropEmptyWitnessObjectTest.java new file mode 100644 index 0000000..15456d4 --- /dev/null +++ b/didwebvh-core/src/test/java/io/github/decentralizedidentity/didwebvh/core/interop/InteropEmptyWitnessObjectTest.java @@ -0,0 +1,53 @@ +package io.github.decentralizedidentity.didwebvh.core.interop; + +import io.github.decentralizedidentity.didwebvh.core.model.LogEntry; +import io.github.decentralizedidentity.didwebvh.core.witness.WitnessConfig; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; + +/** + * Interop regression: Python and TS implementations serialize an empty + * {@code "witness": {}} object in parameters when no witnesses are configured. + * The spec permits this (an empty object means "no witnesses"); the Java + * library used to NPE because Gson bypasses the no-args constructor and left + * the internal {@code witnesses} list as {@code null}. + * + *

Source: swcurran/didwebvh-test-suite, vectors/basic-create/python/did.jsonl. + */ +class InteropEmptyWitnessObjectTest { + + @Test + void parsesEmptyWitnessObjectWithoutNpe() throws IOException { + String line = readVector("/interop/basic-create-python/did.jsonl").trim(); + + LogEntry entry = LogEntry.fromJsonLine(line); + + WitnessConfig witness = entry.getParameters().getWitness(); + assertThat(witness).isNotNull(); + assertThatCode(witness::isActive).doesNotThrowAnyException(); + assertThat(witness.isActive()).isFalse(); + assertThat(witness.getWitnesses()).isEmpty(); + assertThat(witness.getThreshold()).isZero(); + } + + private static String readVector(String resourcePath) throws IOException { + try (InputStream in = InteropEmptyWitnessObjectTest.class.getResourceAsStream(resourcePath)) { + if (in == null) { + throw new IllegalStateException("Missing test resource: " + resourcePath); + } + java.io.ByteArrayOutputStream out = new java.io.ByteArrayOutputStream(); + byte[] buf = new byte[4096]; + int read; + while ((read = in.read(buf)) > 0) { + out.write(buf, 0, read); + } + return new String(out.toByteArray(), StandardCharsets.UTF_8); + } + } +} diff --git a/didwebvh-core/src/test/java/io/github/decentralizedidentity/didwebvh/core/interop/InteropJavaEeccLogsTest.java b/didwebvh-core/src/test/java/io/github/decentralizedidentity/didwebvh/core/interop/InteropJavaEeccLogsTest.java new file mode 100644 index 0000000..10b3fbd --- /dev/null +++ b/didwebvh-core/src/test/java/io/github/decentralizedidentity/didwebvh/core/interop/InteropJavaEeccLogsTest.java @@ -0,0 +1,74 @@ +package io.github.decentralizedidentity.didwebvh.core.interop; + +import io.github.decentralizedidentity.didwebvh.core.model.LogEntry; +import io.github.decentralizedidentity.didwebvh.core.validate.LogChainValidator; +import io.github.decentralizedidentity.didwebvh.core.validate.ValidationResult; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Interop regression: java-eecc-produced logs were reported as failing with + * "versionTime must be after previous entry at entry N" — see GitHub issue #2. + * Investigation showed the root cause was canonicalization mismatch on the + * empty {@code witness: {}} parameter, which short-circuited validation before + * the versionTime check; once the witness round-trip was fixed, these logs + * validate. This test pins that behaviour. + * + *

Source: swcurran/didwebvh-test-suite, vectors/<name>/java-eecc/did.jsonl. + */ +class InteropJavaEeccLogsTest { + + @ParameterizedTest(name = "{0}") + @ValueSource(strings = { + "basic-update", + "deactivate", + "key-rotation", + "multi-update", + "services", + "witness-update" + }) + void validatesJavaEeccLog(String vector) throws IOException { + String jsonl = readVector("/interop/" + vector + "-java-eecc/did.jsonl"); + List entries = parseJsonl(jsonl); + + ValidationResult result = new LogChainValidator().validate(entries, null); + + assertThat(result.isValid()) + .as("java-eecc %s log must validate; failure: %s", vector, result.getFailureReason()) + .isTrue(); + } + + private static List parseJsonl(String jsonl) { + List out = new ArrayList<>(); + for (String line : jsonl.split("\n")) { + String trimmed = line.trim(); + if (!trimmed.isEmpty()) { + out.add(LogEntry.fromJsonLine(trimmed)); + } + } + return out; + } + + private static String readVector(String resourcePath) throws IOException { + try (InputStream in = InteropJavaEeccLogsTest.class.getResourceAsStream(resourcePath)) { + if (in == null) { + throw new IllegalStateException("Missing test resource: " + resourcePath); + } + java.io.ByteArrayOutputStream out = new java.io.ByteArrayOutputStream(); + byte[] buf = new byte[4096]; + int read; + while ((read = in.read(buf)) > 0) { + out.write(buf, 0, read); + } + return new String(out.toByteArray(), StandardCharsets.UTF_8); + } + } +} diff --git a/didwebvh-core/src/test/java/io/github/decentralizedidentity/didwebvh/core/interop/InteropTsBasicUpdateTest.java b/didwebvh-core/src/test/java/io/github/decentralizedidentity/didwebvh/core/interop/InteropTsBasicUpdateTest.java new file mode 100644 index 0000000..2fdbb0d --- /dev/null +++ b/didwebvh-core/src/test/java/io/github/decentralizedidentity/didwebvh/core/interop/InteropTsBasicUpdateTest.java @@ -0,0 +1,64 @@ +package io.github.decentralizedidentity.didwebvh.core.interop; + +import io.github.decentralizedidentity.didwebvh.core.model.LogEntry; +import io.github.decentralizedidentity.didwebvh.core.validate.LogChainValidator; +import io.github.decentralizedidentity.didwebvh.core.validate.ValidationResult; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Interop regression: TS implementation always serialises + * {@code "nextKeyHashes": []} and {@code "witness": {}} (plus + * {@code "watchers": []}) even when those features are not configured. + * The Java log-chain validator must treat these as the "feature not active" + * case, not reject them. + * + *

Source: swcurran/didwebvh-test-suite, vectors/basic-update/ts/did.jsonl. + */ +class InteropTsBasicUpdateTest { + + @Test + void validatesTsBasicUpdateLog() throws IOException { + String jsonl = readVector("/interop/basic-update-ts/did.jsonl"); + List entries = parseJsonl(jsonl); + + ValidationResult result = new LogChainValidator().validate(entries, null); + + assertThat(result.isValid()) + .as("TS basic-update log must validate; failure: %s", result.getFailureReason()) + .isTrue(); + } + + private static List parseJsonl(String jsonl) { + List out = new ArrayList<>(); + for (String line : jsonl.split("\n")) { + String trimmed = line.trim(); + if (!trimmed.isEmpty()) { + out.add(LogEntry.fromJsonLine(trimmed)); + } + } + return out; + } + + private static String readVector(String resourcePath) throws IOException { + try (InputStream in = InteropTsBasicUpdateTest.class.getResourceAsStream(resourcePath)) { + if (in == null) { + throw new IllegalStateException("Missing test resource: " + resourcePath); + } + java.io.ByteArrayOutputStream out = new java.io.ByteArrayOutputStream(); + byte[] buf = new byte[4096]; + int read; + while ((read = in.read(buf)) > 0) { + out.write(buf, 0, read); + } + return new String(out.toByteArray(), StandardCharsets.UTF_8); + } + } +} diff --git a/didwebvh-core/src/test/resources/interop/README.md b/didwebvh-core/src/test/resources/interop/README.md new file mode 100644 index 0000000..c2c8bf3 --- /dev/null +++ b/didwebvh-core/src/test/resources/interop/README.md @@ -0,0 +1,25 @@ +# Interop Test Vectors + +Vendored from the [did:webvh test suite](https://github.com/swcurran/didwebvh-test-suite). + +Upstream commit: `c11fda313dcc8ef0d1d50cff907fa754f33df73e` (2026-05-26) + +Each subdirectory tracks one interop regression discovered by the +multi-implementation test suite (see GitHub issue #2). Files are copied +verbatim from the upstream `vectors/` tree and exercised by JUnit tests under +`didwebvh-core/src/test/java/.../interop/`. + +| Directory | Source path in upstream | Exercises | +| --- | --- | --- | +| `basic-create-python/` | `vectors/basic-create/python/` | Empty `witness: {}` deserialization (no NPE) | +| `basic-update-ts/` | `vectors/basic-update/ts/` | TS log with `witness: {}` + `nextKeyHashes: []` + `watchers: []` round-trip (SCID, entry hash, proof) | +| `basic-update-java-eecc/` | `vectors/basic-update/java-eecc/` | java-eecc log validation | +| `deactivate-java-eecc/` | `vectors/deactivate/java-eecc/` | java-eecc deactivate log validation | +| `key-rotation-java-eecc/` | `vectors/key-rotation/java-eecc/` | java-eecc key-rotation log validation | +| `multi-update-java-eecc/` | `vectors/multi-update/java-eecc/` | java-eecc multi-update log validation | +| `services-java-eecc/` | `vectors/services/java-eecc/` | java-eecc services log validation | +| `witness-update-java-eecc/`| `vectors/witness-update/java-eecc/`| java-eecc witness-update log validation | + +To refresh: bump the SHA above, re-download the listed files +(`curl -sSL https://raw.githubusercontent.com/swcurran/didwebvh-test-suite//`), +run `./mvnw -pl didwebvh-core verify`, and reconcile any test deltas. diff --git a/didwebvh-core/src/test/resources/interop/basic-create-python/did.jsonl b/didwebvh-core/src/test/resources/interop/basic-create-python/did.jsonl new file mode 100644 index 0000000..4273d20 --- /dev/null +++ b/didwebvh-core/src/test/resources/interop/basic-create-python/did.jsonl @@ -0,0 +1 @@ +{"versionId": "1-QmWFwGhwjwRBMcQRzHHSQwpebk7iQ8CKd8rihrzbU1BGRt", "versionTime": "2000-01-01T00:00:00Z", "parameters": {"updateKeys": ["z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG"], "portable": false, "watchers": [], "witness": {}, "deactivated": false, "method": "did:webvh:1.0", "scid": "QmXhVjFG6EBTosDastaaHMRypm2qSv4SMGctADsx878Yux"}, "state": {"@context": ["https://www.w3.org/ns/did/v1", "https://w3id.org/security/multikey/v1"], "id": "did:webvh:QmXhVjFG6EBTosDastaaHMRypm2qSv4SMGctADsx878Yux:example.com", "controller": "did:webvh:QmXhVjFG6EBTosDastaaHMRypm2qSv4SMGctADsx878Yux:example.com", "verificationMethod": [{"type": "Multikey", "publicKeyMultibase": "z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG", "purpose": "authentication", "id": "did:webvh:QmXhVjFG6EBTosDastaaHMRypm2qSv4SMGctADsx878Yux:example.com#P5RDjVJG"}], "authentication": ["did:webvh:QmXhVjFG6EBTosDastaaHMRypm2qSv4SMGctADsx878Yux:example.com#P5RDjVJG"], "assertionMethod": [], "keyAgreement": [], "capabilityDelegation": [], "capabilityInvocation": []}, "proof": [{"type": "DataIntegrityProof", "cryptosuite": "eddsa-jcs-2022", "verificationMethod": "did:key:z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG#z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG", "created": "2000-01-01T00:00:00Z", "proofPurpose": "assertionMethod", "proofValue": "z33ApmedSA2ebP5dknMvrUvZLMB1UoNtaSu6bZkRVELao2QxwFB1pSSQQ4MrLidZ8jXCCic8Y4CqMAiUrmrnN3tyg"}]} diff --git a/didwebvh-core/src/test/resources/interop/basic-update-java-eecc/did.jsonl b/didwebvh-core/src/test/resources/interop/basic-update-java-eecc/did.jsonl new file mode 100644 index 0000000..ea9a206 --- /dev/null +++ b/didwebvh-core/src/test/resources/interop/basic-update-java-eecc/did.jsonl @@ -0,0 +1,2 @@ +{"versionId":"1-QmWc66oUVWRAdjUGAay4SAtdXL5721A8awa8ntha5TUrnJ","versionTime":"2026-05-26T18:45:06Z","parameters":{"method":"did:webvh:1.0","scid":"QmXq3qKkf4miH65SnSpkaMbAcXixyR6JU4yt8p4W8ftGWx","updateKeys":["z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG"]},"state":{"@context":["https://www.w3.org/ns/did/v1","https://w3id.org/security/multikey/v1"],"id":"did:webvh:QmXq3qKkf4miH65SnSpkaMbAcXixyR6JU4yt8p4W8ftGWx:example.com","controller":"did:webvh:QmXq3qKkf4miH65SnSpkaMbAcXixyR6JU4yt8p4W8ftGWx:example.com","assertionMethod":[],"keyAgreement":[],"capabilityDelegation":[],"capabilityInvocation":[],"verificationMethod":[{"id":"did:webvh:QmXq3qKkf4miH65SnSpkaMbAcXixyR6JU4yt8p4W8ftGWx:example.com#P5RDjVJG","type":"Multikey","controller":"did:webvh:QmXq3qKkf4miH65SnSpkaMbAcXixyR6JU4yt8p4W8ftGWx:example.com","publicKeyMultibase":"z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG"}],"authentication":["did:webvh:QmXq3qKkf4miH65SnSpkaMbAcXixyR6JU4yt8p4W8ftGWx:example.com#P5RDjVJG"]},"proof":[{"type":"DataIntegrityProof","cryptosuite":"eddsa-jcs-2022","verificationMethod":"did:key:z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG#z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG","created":"2026-05-26T18:45:06Z","proofPurpose":"assertionMethod","proofValue":"z46SCtQSKwSY1PQfsgmoJi9wfts4vNCivuJXey1RNcc7bmNVbikTf1ud2AS2dEKmrp2H3C4KiyuZWBYobgFZoVSdX"}]} +{"versionId":"2-QmeJtPUqLYmrzPoMDbnADCzkp1baK39NvSCM6GoKnoaFgB","versionTime":"2026-05-26T18:45:07Z","parameters":{},"state":{"@context":["https://www.w3.org/ns/did/v1","https://w3id.org/security/multikey/v1"],"id":"did:webvh:QmXq3qKkf4miH65SnSpkaMbAcXixyR6JU4yt8p4W8ftGWx:example.com","controller":"did:webvh:QmXq3qKkf4miH65SnSpkaMbAcXixyR6JU4yt8p4W8ftGWx:example.com","assertionMethod":[],"keyAgreement":[],"capabilityDelegation":[],"capabilityInvocation":[],"verificationMethod":[{"id":"did:webvh:QmXq3qKkf4miH65SnSpkaMbAcXixyR6JU4yt8p4W8ftGWx:example.com#P5RDjVJG","type":"Multikey","controller":"did:webvh:QmXq3qKkf4miH65SnSpkaMbAcXixyR6JU4yt8p4W8ftGWx:example.com","publicKeyMultibase":"z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG"}],"authentication":["did:webvh:QmXq3qKkf4miH65SnSpkaMbAcXixyR6JU4yt8p4W8ftGWx:example.com#P5RDjVJG"],"alsoKnownAs":["did:web:example.com"]},"proof":[{"type":"DataIntegrityProof","cryptosuite":"eddsa-jcs-2022","verificationMethod":"did:key:z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG#z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG","created":"2026-05-26T18:45:06Z","proofPurpose":"assertionMethod","proofValue":"z5bmu7Ta6Tzmff47RhKd5G3WLXnC5NSLsCvehcNEW22ptdFUJ3P8GXUfGerqWPprodq2YRo7Gq6XAPQb2T2cDwq5B"}]} diff --git a/didwebvh-core/src/test/resources/interop/basic-update-ts/did.jsonl b/didwebvh-core/src/test/resources/interop/basic-update-ts/did.jsonl new file mode 100644 index 0000000..b4b9e38 --- /dev/null +++ b/didwebvh-core/src/test/resources/interop/basic-update-ts/did.jsonl @@ -0,0 +1,2 @@ +{"versionId":"1-QmPFhMuZH9gjY2JZgyyrgRuFTywQ4mDhoKGVoGE8uy7hFD","versionTime":"2000-01-01T00:00:00Z","parameters":{"method":"did:webvh:1.0","scid":"Qmdxt11AjZewCNXX69bpEDobgjySeZ7eFwjf4tgpF6p2Dg","updateKeys":["z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG"],"portable":false,"nextKeyHashes":[],"watchers":[],"witness":{},"deactivated":false},"state":{"@context":["https://www.w3.org/ns/did/v1","https://w3id.org/security/multikey/v1"],"id":"did:webvh:Qmdxt11AjZewCNXX69bpEDobgjySeZ7eFwjf4tgpF6p2Dg:example.com","controller":"did:webvh:Qmdxt11AjZewCNXX69bpEDobgjySeZ7eFwjf4tgpF6p2Dg:example.com","verificationMethod":[{"type":"Multikey","publicKeyMultibase":"z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG","purpose":"authentication","id":"did:webvh:Qmdxt11AjZewCNXX69bpEDobgjySeZ7eFwjf4tgpF6p2Dg:example.com#P5RDjVJG"}],"authentication":["did:webvh:Qmdxt11AjZewCNXX69bpEDobgjySeZ7eFwjf4tgpF6p2Dg:example.com#P5RDjVJG"],"assertionMethod":[],"keyAgreement":[],"capabilityDelegation":[],"capabilityInvocation":[]},"proof":[{"type":"DataIntegrityProof","cryptosuite":"eddsa-jcs-2022","verificationMethod":"did:key:z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG#z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG","created":"2000-01-01T00:00:00Z","proofPurpose":"assertionMethod","proofValue":"z3gfipj528cwTsP7aSSWMsPzA5uqSUGSN7WNzJQFf1WTvjpHf9Ftjk6StQmqqzjyjQT9xyqTjEsRp2jw4DBjcyqac"}]} +{"versionId":"2-QmXbbxspnFjjt5FX9QEdn8C6D8FZJsFceQdoHFTx89fyT4","versionTime":"2000-01-02T00:00:00Z","parameters":{"updateKeys":["z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG"],"nextKeyHashes":[],"witness":{},"watchers":[]},"state":{"@context":["https://www.w3.org/ns/did/v1","https://w3id.org/security/multikey/v1"],"id":"did:webvh:Qmdxt11AjZewCNXX69bpEDobgjySeZ7eFwjf4tgpF6p2Dg:example.com","controller":"did:webvh:Qmdxt11AjZewCNXX69bpEDobgjySeZ7eFwjf4tgpF6p2Dg:example.com","verificationMethod":[{"type":"Multikey","publicKeyMultibase":"z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG","purpose":"authentication","id":"did:webvh:Qmdxt11AjZewCNXX69bpEDobgjySeZ7eFwjf4tgpF6p2Dg:example.com#P5RDjVJG"}],"authentication":["did:webvh:Qmdxt11AjZewCNXX69bpEDobgjySeZ7eFwjf4tgpF6p2Dg:example.com#P5RDjVJG"],"assertionMethod":[],"keyAgreement":[],"capabilityDelegation":[],"capabilityInvocation":[],"alsoKnownAs":["did:web:example.com"]},"proof":[{"type":"DataIntegrityProof","cryptosuite":"eddsa-jcs-2022","verificationMethod":"did:key:z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG#z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG","created":"2000-01-02T00:00:00Z","proofPurpose":"assertionMethod","proofValue":"z53PwdwPmSpQoRkumzCVS2buqmTA6GoqaeEyQXJmDPG6diKnctCpcepx8e9NEEobynmM51HMg33kMYPPPWX21rPoy"}]} diff --git a/didwebvh-core/src/test/resources/interop/deactivate-java-eecc/did.jsonl b/didwebvh-core/src/test/resources/interop/deactivate-java-eecc/did.jsonl new file mode 100644 index 0000000..06dd6c1 --- /dev/null +++ b/didwebvh-core/src/test/resources/interop/deactivate-java-eecc/did.jsonl @@ -0,0 +1,2 @@ +{"versionId":"1-QmWc66oUVWRAdjUGAay4SAtdXL5721A8awa8ntha5TUrnJ","versionTime":"2026-05-26T18:45:06Z","parameters":{"method":"did:webvh:1.0","scid":"QmXq3qKkf4miH65SnSpkaMbAcXixyR6JU4yt8p4W8ftGWx","updateKeys":["z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG"]},"state":{"@context":["https://www.w3.org/ns/did/v1","https://w3id.org/security/multikey/v1"],"id":"did:webvh:QmXq3qKkf4miH65SnSpkaMbAcXixyR6JU4yt8p4W8ftGWx:example.com","controller":"did:webvh:QmXq3qKkf4miH65SnSpkaMbAcXixyR6JU4yt8p4W8ftGWx:example.com","assertionMethod":[],"keyAgreement":[],"capabilityDelegation":[],"capabilityInvocation":[],"verificationMethod":[{"id":"did:webvh:QmXq3qKkf4miH65SnSpkaMbAcXixyR6JU4yt8p4W8ftGWx:example.com#P5RDjVJG","type":"Multikey","controller":"did:webvh:QmXq3qKkf4miH65SnSpkaMbAcXixyR6JU4yt8p4W8ftGWx:example.com","publicKeyMultibase":"z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG"}],"authentication":["did:webvh:QmXq3qKkf4miH65SnSpkaMbAcXixyR6JU4yt8p4W8ftGWx:example.com#P5RDjVJG"]},"proof":[{"type":"DataIntegrityProof","cryptosuite":"eddsa-jcs-2022","verificationMethod":"did:key:z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG#z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG","created":"2026-05-26T18:45:06Z","proofPurpose":"assertionMethod","proofValue":"z46SCtQSKwSY1PQfsgmoJi9wfts4vNCivuJXey1RNcc7bmNVbikTf1ud2AS2dEKmrp2H3C4KiyuZWBYobgFZoVSdX"}]} +{"versionId":"2-QmSitS2W7UGLaXHHTKGQvx3MvMUb5YqHv1x6sxCR6aqTUx","versionTime":"2026-05-26T18:45:07Z","parameters":{"updateKeys":[],"deactivated":true},"state":{"@context":["https://www.w3.org/ns/did/v1","https://w3id.org/security/multikey/v1"],"id":"did:webvh:QmXq3qKkf4miH65SnSpkaMbAcXixyR6JU4yt8p4W8ftGWx:example.com","controller":"did:webvh:QmXq3qKkf4miH65SnSpkaMbAcXixyR6JU4yt8p4W8ftGWx:example.com","assertionMethod":[],"keyAgreement":[],"capabilityDelegation":[],"capabilityInvocation":[],"verificationMethod":[{"id":"did:webvh:QmXq3qKkf4miH65SnSpkaMbAcXixyR6JU4yt8p4W8ftGWx:example.com#P5RDjVJG","type":"Multikey","controller":"did:webvh:QmXq3qKkf4miH65SnSpkaMbAcXixyR6JU4yt8p4W8ftGWx:example.com","publicKeyMultibase":"z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG"}],"authentication":["did:webvh:QmXq3qKkf4miH65SnSpkaMbAcXixyR6JU4yt8p4W8ftGWx:example.com#P5RDjVJG"]},"proof":[{"type":"DataIntegrityProof","cryptosuite":"eddsa-jcs-2022","verificationMethod":"did:key:z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG#z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG","created":"2026-05-26T18:45:06Z","proofPurpose":"assertionMethod","proofValue":"z35ALPgVCRCFPKCNZBM9AAjidFqmtqo6cYaU4tsn9DfpkMaRjSq9pvL7imRbz8FQ52kocmWZ2FBqTa1DyYouG2uU5"}]} diff --git a/didwebvh-core/src/test/resources/interop/key-rotation-java-eecc/did.jsonl b/didwebvh-core/src/test/resources/interop/key-rotation-java-eecc/did.jsonl new file mode 100644 index 0000000..83c3cad --- /dev/null +++ b/didwebvh-core/src/test/resources/interop/key-rotation-java-eecc/did.jsonl @@ -0,0 +1,2 @@ +{"versionId":"1-QmWc66oUVWRAdjUGAay4SAtdXL5721A8awa8ntha5TUrnJ","versionTime":"2026-05-26T18:45:06Z","parameters":{"method":"did:webvh:1.0","scid":"QmXq3qKkf4miH65SnSpkaMbAcXixyR6JU4yt8p4W8ftGWx","updateKeys":["z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG"]},"state":{"@context":["https://www.w3.org/ns/did/v1","https://w3id.org/security/multikey/v1"],"id":"did:webvh:QmXq3qKkf4miH65SnSpkaMbAcXixyR6JU4yt8p4W8ftGWx:example.com","controller":"did:webvh:QmXq3qKkf4miH65SnSpkaMbAcXixyR6JU4yt8p4W8ftGWx:example.com","assertionMethod":[],"keyAgreement":[],"capabilityDelegation":[],"capabilityInvocation":[],"verificationMethod":[{"id":"did:webvh:QmXq3qKkf4miH65SnSpkaMbAcXixyR6JU4yt8p4W8ftGWx:example.com#P5RDjVJG","type":"Multikey","controller":"did:webvh:QmXq3qKkf4miH65SnSpkaMbAcXixyR6JU4yt8p4W8ftGWx:example.com","publicKeyMultibase":"z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG"}],"authentication":["did:webvh:QmXq3qKkf4miH65SnSpkaMbAcXixyR6JU4yt8p4W8ftGWx:example.com#P5RDjVJG"]},"proof":[{"type":"DataIntegrityProof","cryptosuite":"eddsa-jcs-2022","verificationMethod":"did:key:z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG#z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG","created":"2026-05-26T18:45:06Z","proofPurpose":"assertionMethod","proofValue":"z46SCtQSKwSY1PQfsgmoJi9wfts4vNCivuJXey1RNcc7bmNVbikTf1ud2AS2dEKmrp2H3C4KiyuZWBYobgFZoVSdX"}]} +{"versionId":"2-QmQ9PkTSedPFyjKgWKj6TvYExjkchkCC9WPisi99MTvcPN","versionTime":"2026-05-26T18:45:07Z","parameters":{"updateKeys":["z6MknGc3ocHs3zdPiJbnaaqDi58NGb4pk1Sp9WxWufuXSdxf"]},"state":{"@context":["https://www.w3.org/ns/did/v1","https://w3id.org/security/multikey/v1"],"id":"did:webvh:QmXq3qKkf4miH65SnSpkaMbAcXixyR6JU4yt8p4W8ftGWx:example.com","controller":"did:webvh:QmXq3qKkf4miH65SnSpkaMbAcXixyR6JU4yt8p4W8ftGWx:example.com","assertionMethod":[],"keyAgreement":[],"capabilityDelegation":[],"capabilityInvocation":[],"verificationMethod":[{"id":"did:webvh:QmXq3qKkf4miH65SnSpkaMbAcXixyR6JU4yt8p4W8ftGWx:example.com#ufuXSdxf","type":"Multikey","controller":"did:webvh:QmXq3qKkf4miH65SnSpkaMbAcXixyR6JU4yt8p4W8ftGWx:example.com","publicKeyMultibase":"z6MknGc3ocHs3zdPiJbnaaqDi58NGb4pk1Sp9WxWufuXSdxf"}],"authentication":["did:webvh:QmXq3qKkf4miH65SnSpkaMbAcXixyR6JU4yt8p4W8ftGWx:example.com#ufuXSdxf"]},"proof":[{"type":"DataIntegrityProof","cryptosuite":"eddsa-jcs-2022","verificationMethod":"did:key:z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG#z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG","created":"2026-05-26T18:45:06Z","proofPurpose":"assertionMethod","proofValue":"z2epMpJjPtKN9wgNTP3BgEEUzaRqyWMuC6QFNjxxJke2nRJNcXFYNU9hiFnq3e3tyyxaKh9joBLNFypmC5Ru3FshR"}]} diff --git a/didwebvh-core/src/test/resources/interop/multi-update-java-eecc/did.jsonl b/didwebvh-core/src/test/resources/interop/multi-update-java-eecc/did.jsonl new file mode 100644 index 0000000..fd7adc3 --- /dev/null +++ b/didwebvh-core/src/test/resources/interop/multi-update-java-eecc/did.jsonl @@ -0,0 +1,3 @@ +{"versionId":"1-QmWc66oUVWRAdjUGAay4SAtdXL5721A8awa8ntha5TUrnJ","versionTime":"2026-05-26T18:45:06Z","parameters":{"method":"did:webvh:1.0","scid":"QmXq3qKkf4miH65SnSpkaMbAcXixyR6JU4yt8p4W8ftGWx","updateKeys":["z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG"]},"state":{"@context":["https://www.w3.org/ns/did/v1","https://w3id.org/security/multikey/v1"],"id":"did:webvh:QmXq3qKkf4miH65SnSpkaMbAcXixyR6JU4yt8p4W8ftGWx:example.com","controller":"did:webvh:QmXq3qKkf4miH65SnSpkaMbAcXixyR6JU4yt8p4W8ftGWx:example.com","assertionMethod":[],"keyAgreement":[],"capabilityDelegation":[],"capabilityInvocation":[],"verificationMethod":[{"id":"did:webvh:QmXq3qKkf4miH65SnSpkaMbAcXixyR6JU4yt8p4W8ftGWx:example.com#P5RDjVJG","type":"Multikey","controller":"did:webvh:QmXq3qKkf4miH65SnSpkaMbAcXixyR6JU4yt8p4W8ftGWx:example.com","publicKeyMultibase":"z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG"}],"authentication":["did:webvh:QmXq3qKkf4miH65SnSpkaMbAcXixyR6JU4yt8p4W8ftGWx:example.com#P5RDjVJG"]},"proof":[{"type":"DataIntegrityProof","cryptosuite":"eddsa-jcs-2022","verificationMethod":"did:key:z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG#z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG","created":"2026-05-26T18:45:06Z","proofPurpose":"assertionMethod","proofValue":"z46SCtQSKwSY1PQfsgmoJi9wfts4vNCivuJXey1RNcc7bmNVbikTf1ud2AS2dEKmrp2H3C4KiyuZWBYobgFZoVSdX"}]} +{"versionId":"2-QmeJtPUqLYmrzPoMDbnADCzkp1baK39NvSCM6GoKnoaFgB","versionTime":"2026-05-26T18:45:07Z","parameters":{},"state":{"@context":["https://www.w3.org/ns/did/v1","https://w3id.org/security/multikey/v1"],"id":"did:webvh:QmXq3qKkf4miH65SnSpkaMbAcXixyR6JU4yt8p4W8ftGWx:example.com","controller":"did:webvh:QmXq3qKkf4miH65SnSpkaMbAcXixyR6JU4yt8p4W8ftGWx:example.com","assertionMethod":[],"keyAgreement":[],"capabilityDelegation":[],"capabilityInvocation":[],"verificationMethod":[{"id":"did:webvh:QmXq3qKkf4miH65SnSpkaMbAcXixyR6JU4yt8p4W8ftGWx:example.com#P5RDjVJG","type":"Multikey","controller":"did:webvh:QmXq3qKkf4miH65SnSpkaMbAcXixyR6JU4yt8p4W8ftGWx:example.com","publicKeyMultibase":"z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG"}],"authentication":["did:webvh:QmXq3qKkf4miH65SnSpkaMbAcXixyR6JU4yt8p4W8ftGWx:example.com#P5RDjVJG"],"alsoKnownAs":["did:web:example.com"]},"proof":[{"type":"DataIntegrityProof","cryptosuite":"eddsa-jcs-2022","verificationMethod":"did:key:z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG#z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG","created":"2026-05-26T18:45:06Z","proofPurpose":"assertionMethod","proofValue":"z5bmu7Ta6Tzmff47RhKd5G3WLXnC5NSLsCvehcNEW22ptdFUJ3P8GXUfGerqWPprodq2YRo7Gq6XAPQb2T2cDwq5B"}]} +{"versionId":"3-QmbFthJC5iCmPW4VSQxgjeHYw5RgsU1LHG1xHkY4NcUYvs","versionTime":"2026-05-26T18:45:08Z","parameters":{},"state":{"@context":["https://www.w3.org/ns/did/v1","https://w3id.org/security/multikey/v1"],"id":"did:webvh:QmXq3qKkf4miH65SnSpkaMbAcXixyR6JU4yt8p4W8ftGWx:example.com","controller":"did:webvh:QmXq3qKkf4miH65SnSpkaMbAcXixyR6JU4yt8p4W8ftGWx:example.com","assertionMethod":[],"keyAgreement":[],"capabilityDelegation":[],"capabilityInvocation":[],"verificationMethod":[{"id":"did:webvh:QmXq3qKkf4miH65SnSpkaMbAcXixyR6JU4yt8p4W8ftGWx:example.com#P5RDjVJG","type":"Multikey","controller":"did:webvh:QmXq3qKkf4miH65SnSpkaMbAcXixyR6JU4yt8p4W8ftGWx:example.com","publicKeyMultibase":"z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG"}],"authentication":["did:webvh:QmXq3qKkf4miH65SnSpkaMbAcXixyR6JU4yt8p4W8ftGWx:example.com#P5RDjVJG"],"alsoKnownAs":["did:web:example.com","did:web:example.org"]},"proof":[{"type":"DataIntegrityProof","cryptosuite":"eddsa-jcs-2022","verificationMethod":"did:key:z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG#z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG","created":"2026-05-26T18:45:06Z","proofPurpose":"assertionMethod","proofValue":"z4JY8rvUGM7C8S2PHqWaH7nZgmETtbdRWVP6F5kQjgs6qrMM7sqP2qJp5Ndq7yQn1VGQGbKhq6Uc5nF3YDZAVK33N"}]} diff --git a/didwebvh-core/src/test/resources/interop/services-java-eecc/did.jsonl b/didwebvh-core/src/test/resources/interop/services-java-eecc/did.jsonl new file mode 100644 index 0000000..5e3bed4 --- /dev/null +++ b/didwebvh-core/src/test/resources/interop/services-java-eecc/did.jsonl @@ -0,0 +1,2 @@ +{"versionId":"1-QmSSGfHkCx7n7VpC1ACofw2wvr1SNFUKj69J5JptdbuGtN","versionTime":"2026-05-26T18:45:06Z","parameters":{"method":"did:webvh:1.0","scid":"QmW4gBYwEpZuqEPSMWd3j3q7T4sZDw5mcFEKeDM7zWmWLB","updateKeys":["z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG"]},"state":{"@context":["https://www.w3.org/ns/did/v1","https://w3id.org/security/multikey/v1"],"id":"did:webvh:QmW4gBYwEpZuqEPSMWd3j3q7T4sZDw5mcFEKeDM7zWmWLB:example.com","controller":"did:webvh:QmW4gBYwEpZuqEPSMWd3j3q7T4sZDw5mcFEKeDM7zWmWLB:example.com","assertionMethod":[],"keyAgreement":[],"capabilityDelegation":[],"capabilityInvocation":[],"verificationMethod":[{"id":"did:webvh:QmW4gBYwEpZuqEPSMWd3j3q7T4sZDw5mcFEKeDM7zWmWLB:example.com#P5RDjVJG","type":"Multikey","controller":"did:webvh:QmW4gBYwEpZuqEPSMWd3j3q7T4sZDw5mcFEKeDM7zWmWLB:example.com","publicKeyMultibase":"z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG"}],"authentication":["did:webvh:QmW4gBYwEpZuqEPSMWd3j3q7T4sZDw5mcFEKeDM7zWmWLB:example.com#P5RDjVJG"],"service":[{"id":"#linked-domain","type":"LinkedDomains","serviceEndpoint":"https://example.com"}]},"proof":[{"type":"DataIntegrityProof","cryptosuite":"eddsa-jcs-2022","verificationMethod":"did:key:z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG#z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG","created":"2026-05-26T18:45:06Z","proofPurpose":"assertionMethod","proofValue":"z2eFCGcPywz1NCkNtYoDWStoPJQJvKRzJikQo47uBTdsQgawpiPbJCJvmezkcxZ8vAJLsTBv8ittx4qV654M26hQZ"}]} +{"versionId":"2-QmWujJ9nY2h9zDzivFUH8x9Lip7XGg4L2gY8R8G6xn1SWR","versionTime":"2026-05-26T18:45:07Z","parameters":{},"state":{"@context":["https://www.w3.org/ns/did/v1","https://w3id.org/security/multikey/v1"],"id":"did:webvh:QmW4gBYwEpZuqEPSMWd3j3q7T4sZDw5mcFEKeDM7zWmWLB:example.com","controller":"did:webvh:QmW4gBYwEpZuqEPSMWd3j3q7T4sZDw5mcFEKeDM7zWmWLB:example.com","assertionMethod":[],"keyAgreement":[],"capabilityDelegation":[],"capabilityInvocation":[],"verificationMethod":[{"id":"did:webvh:QmW4gBYwEpZuqEPSMWd3j3q7T4sZDw5mcFEKeDM7zWmWLB:example.com#P5RDjVJG","type":"Multikey","controller":"did:webvh:QmW4gBYwEpZuqEPSMWd3j3q7T4sZDw5mcFEKeDM7zWmWLB:example.com","publicKeyMultibase":"z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG"}],"authentication":["did:webvh:QmW4gBYwEpZuqEPSMWd3j3q7T4sZDw5mcFEKeDM7zWmWLB:example.com#P5RDjVJG"],"service":[{"id":"#linked-domain","type":"LinkedDomains","serviceEndpoint":"https://example.com"},{"id":"#messaging","type":"DIDCommMessaging","serviceEndpoint":"https://example.com/didcomm"}]},"proof":[{"type":"DataIntegrityProof","cryptosuite":"eddsa-jcs-2022","verificationMethod":"did:key:z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG#z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG","created":"2026-05-26T18:45:06Z","proofPurpose":"assertionMethod","proofValue":"z2zod7RDAM9nhsiXQEyrMy28DfpL8u1BGWyYXAP1o7go93kqdruAh2Foo4h3oVsQQQ1H2CoSdKpamritssKTCytjb"}]} diff --git a/didwebvh-core/src/test/resources/interop/witness-update-java-eecc/did.jsonl b/didwebvh-core/src/test/resources/interop/witness-update-java-eecc/did.jsonl new file mode 100644 index 0000000..9b9b34d --- /dev/null +++ b/didwebvh-core/src/test/resources/interop/witness-update-java-eecc/did.jsonl @@ -0,0 +1,2 @@ +{"versionId":"1-QmZyfnYUNC8URUiwpmT6fmi4VFhvTiYjeZNZjbZWbFKpXN","versionTime":"2026-05-26T18:45:06Z","parameters":{"method":"did:webvh:1.0","scid":"Qmcy68zXi6zWSxhchHQRyRYjbFoVeaRjHsF9wLXGBaCMgx","updateKeys":["z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG"],"witness":{"threshold":2,"witnesses":[{"id":"did:key:z6Mkrv5Cm2XCLumMPTqooLTCw6YDf421d7VdTziwrZ8vNf4L"},{"id":"did:key:z6MkjnHi6KJKx625x56sxudLoKdDVDABJ5VHHdXRFPUea7NP"}]}},"state":{"@context":["https://www.w3.org/ns/did/v1","https://w3id.org/security/multikey/v1"],"id":"did:webvh:Qmcy68zXi6zWSxhchHQRyRYjbFoVeaRjHsF9wLXGBaCMgx:example.com","controller":"did:webvh:Qmcy68zXi6zWSxhchHQRyRYjbFoVeaRjHsF9wLXGBaCMgx:example.com","assertionMethod":[],"keyAgreement":[],"capabilityDelegation":[],"capabilityInvocation":[],"verificationMethod":[{"id":"did:webvh:Qmcy68zXi6zWSxhchHQRyRYjbFoVeaRjHsF9wLXGBaCMgx:example.com#P5RDjVJG","type":"Multikey","controller":"did:webvh:Qmcy68zXi6zWSxhchHQRyRYjbFoVeaRjHsF9wLXGBaCMgx:example.com","publicKeyMultibase":"z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG"}],"authentication":["did:webvh:Qmcy68zXi6zWSxhchHQRyRYjbFoVeaRjHsF9wLXGBaCMgx:example.com#P5RDjVJG"]},"proof":[{"type":"DataIntegrityProof","cryptosuite":"eddsa-jcs-2022","verificationMethod":"did:key:z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG#z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG","created":"2026-05-26T18:45:06Z","proofPurpose":"assertionMethod","proofValue":"zWZbcn2N7npojdoiCpqtxhZrsHQmYYi9rBQVcn5w4p26ymoE2SdYfVZupM42Rws1oic5dF8HVRxAMhLXx36xYy8c"}]} +{"versionId":"2-QmZ38kfnEGATj9Cyook7mTPjayNoHpbmeSwy8E6vYNdMaq","versionTime":"2026-05-26T18:45:07Z","parameters":{"witness":{"threshold":1,"witnesses":[{"id":"did:key:z6Mkrv5Cm2XCLumMPTqooLTCw6YDf421d7VdTziwrZ8vNf4L"}]}},"state":{"@context":["https://www.w3.org/ns/did/v1","https://w3id.org/security/multikey/v1"],"id":"did:webvh:Qmcy68zXi6zWSxhchHQRyRYjbFoVeaRjHsF9wLXGBaCMgx:example.com","controller":"did:webvh:Qmcy68zXi6zWSxhchHQRyRYjbFoVeaRjHsF9wLXGBaCMgx:example.com","assertionMethod":[],"keyAgreement":[],"capabilityDelegation":[],"capabilityInvocation":[],"verificationMethod":[{"id":"did:webvh:Qmcy68zXi6zWSxhchHQRyRYjbFoVeaRjHsF9wLXGBaCMgx:example.com#P5RDjVJG","type":"Multikey","controller":"did:webvh:Qmcy68zXi6zWSxhchHQRyRYjbFoVeaRjHsF9wLXGBaCMgx:example.com","publicKeyMultibase":"z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG"}],"authentication":["did:webvh:Qmcy68zXi6zWSxhchHQRyRYjbFoVeaRjHsF9wLXGBaCMgx:example.com#P5RDjVJG"]},"proof":[{"type":"DataIntegrityProof","cryptosuite":"eddsa-jcs-2022","verificationMethod":"did:key:z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG#z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG","created":"2026-05-26T18:45:06Z","proofPurpose":"assertionMethod","proofValue":"z4woggEbwgas4jYg9qxPWUdzDiYe7FCAADjgLTrQPN7J6Vbd9FEUZaXh9NVk8DAT9CziUHHkMY6vcLkiQg7mpS7Su"}]} From 6abfc036c5faafddd37b851bd2073fccb00c84da Mon Sep 17 00:00:00 2001 From: Reza Maghoul Date: Wed, 27 May 2026 11:18:31 +0200 Subject: [PATCH 2/9] fix(core): authorize pre-rotation log entries with current-entry keys MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Spec §3.7.5 (lines 1071-1082): while Key Pre-Rotation is active, each subsequent log entry's active updateKeys are the keys of the CURRENT entry, not the previous one. The previous entry's nextKeyHashes already pre-committed to those keys, so the rotation entry is self-signed by the newly-revealed key. The validator at LogChainValidator:96 unconditionally used the previous entry's updateKeys for i>0, causing "signing key not in active updateKeys at entry 2" against rust and java-eecc pre-rotation-consume vectors. DeactivateDidOperation made the same mistake when emitting the intermediate pre-rotation-consuming entry. Fix: - LogChainValidator: when prevNextKeyHashes is non-empty (pre-rotation active for this entry), authorize against the entry's own updateKeys. - DeactivateDidOperation: sign the intermediate "consume pre-rotation" entry with the nextRotationSigner rather than the previous signer. - Existing pre-rotation tests and the generator-produced pre-rotation-log.jsonl encoded the wrong (old-key-signs) convention; updated to the spec rules. Interop coverage: InteropPreRotationConsumeTest exercises the rust and java-eecc vectors from swcurran/didwebvh-test-suite@c11fda3 that previously failed with "signing key not in active updateKeys at entry 2". Refs: #2 --- .../core/update/DeactivateDidOperation.java | 9 ++- .../core/validate/LogChainValidator.java | 39 +++++++---- .../core/integration/TestVectorGenerator.java | 6 +- .../InteropPreRotationConsumeTest.java | 68 +++++++++++++++++++ .../core/validate/LogChainValidatorTest.java | 15 ++-- .../src/test/resources/interop/README.md | 2 + .../pre-rotation-consume-java-eecc/did.jsonl | 3 + .../pre-rotation-consume-rust/did.jsonl | 3 + .../test-vectors/pre-rotation-log.jsonl | 4 +- 9 files changed, 123 insertions(+), 26 deletions(-) create mode 100644 didwebvh-core/src/test/java/io/github/decentralizedidentity/didwebvh/core/interop/InteropPreRotationConsumeTest.java create mode 100644 didwebvh-core/src/test/resources/interop/pre-rotation-consume-java-eecc/did.jsonl create mode 100644 didwebvh-core/src/test/resources/interop/pre-rotation-consume-rust/did.jsonl diff --git a/didwebvh-core/src/main/java/io/github/decentralizedidentity/didwebvh/core/update/DeactivateDidOperation.java b/didwebvh-core/src/main/java/io/github/decentralizedidentity/didwebvh/core/update/DeactivateDidOperation.java index 8f2ab28..f06033a 100644 --- a/didwebvh-core/src/main/java/io/github/decentralizedidentity/didwebvh/core/update/DeactivateDidOperation.java +++ b/didwebvh-core/src/main/java/io/github/decentralizedidentity/didwebvh/core/update/DeactivateDidOperation.java @@ -37,8 +37,11 @@ static UpdateDidResult execute(DeactivateDidConfig config) { LogEntry previous = config.getExistingState().getLastEntry(); if (preRotationActive) { - // Intermediate entry: signed by current key (signer), reveals the next keys - // so that pre-rotation constraints are satisfied, then clears nextKeyHashes. + // Intermediate entry: reveals the next key (committed via nextKeyHashes + // in the previous entry) and clears nextKeyHashes to turn off pre-rotation. + // Per spec §3.7.5, while pre-rotation is active each entry is signed by its + // own updateKeys — so the intermediate is signed by the next-rotation signer, + // not the prior signer. String nextMultikey = ProofVerifier.extractMultikey( config.getNextRotationSigner().verificationMethod()); Parameters intermediate = new Parameters() @@ -46,7 +49,7 @@ static UpdateDidResult execute(DeactivateDidConfig config) { .setNextKeyHashes(Collections.emptyList()); LogEntry intermediateEntry = UpdateDidOperation.buildEntry( previous, - config.getSigner(), + config.getNextRotationSigner(), null, intermediate); newEntries.add(intermediateEntry); diff --git a/didwebvh-core/src/main/java/io/github/decentralizedidentity/didwebvh/core/validate/LogChainValidator.java b/didwebvh-core/src/main/java/io/github/decentralizedidentity/didwebvh/core/validate/LogChainValidator.java index c1a433c..e3b956a 100644 --- a/didwebvh-core/src/main/java/io/github/decentralizedidentity/didwebvh/core/validate/LogChainValidator.java +++ b/didwebvh-core/src/main/java/io/github/decentralizedidentity/didwebvh/core/validate/LogChainValidator.java @@ -91,9 +91,21 @@ public ValidationResult validate(List entries, String expectedDid) { "missing proof at entry " + (i + 1), activeParams); } DataIntegrityProof proof = entry.getProof().get(0); - // Authorization uses PREVIOUS active keys for i>0 so that key-rotation entries - // are authorized by the old key (the new keys only take effect after validation). - List signingKeys = (i == 0) + // Spec §3.7.5 (lines 1071-1082): the "active updateKeys" that must + // authorize an entry depends on whether Key Pre-Rotation is active + // for this entry: + // • First entry: the entry's own updateKeys. + // • No pre-rotation: the previous entry's updateKeys + // (the new keys only take effect after validation). + // • Pre-rotation active (previous entry committed a non-empty + // nextKeyHashes): the current entry's own updateKeys. The + // previous entry's nextKeyHashes already pre-committed to those + // keys, so they are the authorized signers for this rotation. + List prevNextKeyHashes = activeParams.getNextKeyHashes(); + boolean preRotationActive = i > 0 + && prevNextKeyHashes != null + && !prevNextKeyHashes.isEmpty(); + List signingKeys = (i == 0 || preRotationActive) ? newActiveParams.getUpdateKeys() : activeParams.getUpdateKeys(); if (signingKeys == null || signingKeys.isEmpty()) { return ValidationResult.failure(i - 1, i, @@ -130,17 +142,16 @@ public ValidationResult validate(List entries, String expectedDid) { } } - // 10. Pre-rotation: if previous entry set nextKeyHashes and this entry rotates keys - if (i > 0 && entryParams.getUpdateKeys() != null) { - List prevNextKeyHashes = activeParams.getNextKeyHashes(); - if (prevNextKeyHashes != null && !prevNextKeyHashes.isEmpty()) { - for (String key : entryParams.getUpdateKeys()) { - String hash = PreRotationHashGenerator.generateHash(key); - if (!prevNextKeyHashes.contains(hash)) { - return ValidationResult.failure(i - 1, i, - "updateKey hash not in previous nextKeyHashes at entry " + (i + 1), - activeParams); - } + // 10. Pre-rotation: if previous entry set nextKeyHashes and this entry rotates keys, + // the new updateKeys must hash-match the previously committed nextKeyHashes + // (spec §3.7.7, lines 1119-1123). + if (preRotationActive && entryParams.getUpdateKeys() != null) { + for (String key : entryParams.getUpdateKeys()) { + String hash = PreRotationHashGenerator.generateHash(key); + if (!prevNextKeyHashes.contains(hash)) { + return ValidationResult.failure(i - 1, i, + "updateKey hash not in previous nextKeyHashes at entry " + (i + 1), + activeParams); } } } diff --git a/didwebvh-core/src/test/java/io/github/decentralizedidentity/didwebvh/core/integration/TestVectorGenerator.java b/didwebvh-core/src/test/java/io/github/decentralizedidentity/didwebvh/core/integration/TestVectorGenerator.java index a0a4fac..ce31728 100644 --- a/didwebvh-core/src/test/java/io/github/decentralizedidentity/didwebvh/core/integration/TestVectorGenerator.java +++ b/didwebvh-core/src/test/java/io/github/decentralizedidentity/didwebvh/core/integration/TestVectorGenerator.java @@ -183,13 +183,15 @@ private static void writePreRotationLog(Path outDir) throws IOException { .execute(); DidWebVhState state = DidWebVhState.from(create.getDid(), create.getLogEntry()); - // Rotate: reveal next key and commit another future rotation. + // Rotate: reveal next key and commit another future rotation. Per spec + // §3.7.5 the rotation entry is signed by its OWN updateKeys (the + // pre-committed `next` key), not the prior author key. String futureKeyHash = PreRotationHashGenerator.generateHash( multikeyOf(TestVectors.seededSigner(TestVectors.UPDATE_SEED))); Parameters rotation = new Parameters() .setUpdateKeys(Collections.singletonList(nextMultikey)) .setNextKeyHashes(Collections.singletonList(futureKeyHash)); - UpdateDidResult rot = DidWebVh.update(state, author) + UpdateDidResult rot = DidWebVh.update(state, next) .changedParameters(rotation) .execute(); for (LogEntry e : rot.getNewEntries()) { diff --git a/didwebvh-core/src/test/java/io/github/decentralizedidentity/didwebvh/core/interop/InteropPreRotationConsumeTest.java b/didwebvh-core/src/test/java/io/github/decentralizedidentity/didwebvh/core/interop/InteropPreRotationConsumeTest.java new file mode 100644 index 0000000..41fd203 --- /dev/null +++ b/didwebvh-core/src/test/java/io/github/decentralizedidentity/didwebvh/core/interop/InteropPreRotationConsumeTest.java @@ -0,0 +1,68 @@ +package io.github.decentralizedidentity.didwebvh.core.interop; + +import io.github.decentralizedidentity.didwebvh.core.model.LogEntry; +import io.github.decentralizedidentity.didwebvh.core.validate.LogChainValidator; +import io.github.decentralizedidentity.didwebvh.core.validate.ValidationResult; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Interop regression: when Key Pre-Rotation is active, the spec + * (§3.7.5, lines 1078-1082) requires that each subsequent log entry be + * signed by the {@code updateKeys} of the current entry — not the + * previous one. The Rust and Java-EECC pre-rotation-consume vectors exercise + * this: every entry rotates to a new key whose hash was committed by the + * previous entry's {@code nextKeyHashes}, and the new entry is signed by its + * own key. + * + *

Source: swcurran/didwebvh-test-suite, vectors/pre-rotation-consume/<impl>/did.jsonl. + */ +class InteropPreRotationConsumeTest { + + @ParameterizedTest(name = "{0}") + @ValueSource(strings = {"rust", "java-eecc"}) + void validatesPreRotationConsumeLog(String impl) throws IOException { + String jsonl = readVector("/interop/pre-rotation-consume-" + impl + "/did.jsonl"); + List entries = parseJsonl(jsonl); + + ValidationResult result = new LogChainValidator().validate(entries, null); + + assertThat(result.isValid()) + .as("pre-rotation-consume %s log must validate; failure: %s", impl, result.getFailureReason()) + .isTrue(); + } + + private static List parseJsonl(String jsonl) { + List out = new ArrayList<>(); + for (String line : jsonl.split("\n")) { + String trimmed = line.trim(); + if (!trimmed.isEmpty()) { + out.add(LogEntry.fromJsonLine(trimmed)); + } + } + return out; + } + + private static String readVector(String resourcePath) throws IOException { + try (InputStream in = InteropPreRotationConsumeTest.class.getResourceAsStream(resourcePath)) { + if (in == null) { + throw new IllegalStateException("Missing test resource: " + resourcePath); + } + java.io.ByteArrayOutputStream out = new java.io.ByteArrayOutputStream(); + byte[] buf = new byte[4096]; + int read; + while ((read = in.read(buf)) > 0) { + out.write(buf, 0, read); + } + return new String(out.toByteArray(), StandardCharsets.UTF_8); + } + } +} diff --git a/didwebvh-core/src/test/java/io/github/decentralizedidentity/didwebvh/core/validate/LogChainValidatorTest.java b/didwebvh-core/src/test/java/io/github/decentralizedidentity/didwebvh/core/validate/LogChainValidatorTest.java index 9e3c880..282bbd3 100644 --- a/didwebvh-core/src/test/java/io/github/decentralizedidentity/didwebvh/core/validate/LogChainValidatorTest.java +++ b/didwebvh-core/src/test/java/io/github/decentralizedidentity/didwebvh/core/validate/LogChainValidatorTest.java @@ -283,10 +283,13 @@ void preRotationValidChain() { .execute(); LogEntry e1 = create.getLogEntry(); - // Rotation is signed by the OLD signer (still active); it commits new key. + // Per spec §3.7.5 (lines 1078-1082), while pre-rotation is active each + // log entry is signed by the keys from the CURRENT entry — the new + // signer, not the old one. The previous entry pre-committed to this + // key via nextKeyHashes. Parameters rotateParams = new Parameters() .setUpdateKeys(Collections.singletonList(multikey2)); - LogEntry e2 = buildUpdateEntry(e1, create.getDid(), signer, rotateParams, null); + LogEntry e2 = buildUpdateEntry(e1, create.getDid(), signer2, rotateParams, null); ValidationResult vr = validator.validate(Arrays.asList(e1, e2), create.getDid()); assertThat(vr.isValid()).isTrue(); @@ -305,13 +308,15 @@ void preRotationWrongKeyFails() { .execute(); LogEntry e1 = create.getLogEntry(); - // Try to rotate to a different key (not the committed one) + // Try to rotate to a different key (not the committed one). Pre-rotation + // makes the new entry's own updateKeys the active signing keys, so the + // entry must be self-signed by signer3 to clear the authorization check + // and reach the rotation-hash mismatch (the actual interesting failure). Signer signer3 = makeTestSigner(); String multikey3 = extractMultikey(signer3.verificationMethod()); Parameters wrongRotate = new Parameters() .setUpdateKeys(Collections.singletonList(multikey3)); - // Sign with signer (still the active key) to get past auth check - LogEntry e2 = buildUpdateEntry(e1, create.getDid(), signer, wrongRotate, null); + LogEntry e2 = buildUpdateEntry(e1, create.getDid(), signer3, wrongRotate, null); ValidationResult vr = validator.validate(Arrays.asList(e1, e2), create.getDid()); diff --git a/didwebvh-core/src/test/resources/interop/README.md b/didwebvh-core/src/test/resources/interop/README.md index c2c8bf3..2502f9f 100644 --- a/didwebvh-core/src/test/resources/interop/README.md +++ b/didwebvh-core/src/test/resources/interop/README.md @@ -19,6 +19,8 @@ verbatim from the upstream `vectors/` tree and exercised by JUnit tests under | `multi-update-java-eecc/` | `vectors/multi-update/java-eecc/` | java-eecc multi-update log validation | | `services-java-eecc/` | `vectors/services/java-eecc/` | java-eecc services log validation | | `witness-update-java-eecc/`| `vectors/witness-update/java-eecc/`| java-eecc witness-update log validation | +| `pre-rotation-consume-rust/` | `vectors/pre-rotation-consume/rust/` | Pre-rotation: each entry signed by its own (committed) updateKeys | +| `pre-rotation-consume-java-eecc/`| `vectors/pre-rotation-consume/java-eecc/`| Pre-rotation: each entry signed by its own (committed) updateKeys | To refresh: bump the SHA above, re-download the listed files (`curl -sSL https://raw.githubusercontent.com/swcurran/didwebvh-test-suite//`), diff --git a/didwebvh-core/src/test/resources/interop/pre-rotation-consume-java-eecc/did.jsonl b/didwebvh-core/src/test/resources/interop/pre-rotation-consume-java-eecc/did.jsonl new file mode 100644 index 0000000..e6f9209 --- /dev/null +++ b/didwebvh-core/src/test/resources/interop/pre-rotation-consume-java-eecc/did.jsonl @@ -0,0 +1,3 @@ +{"versionId":"1-QmUAtPKg1uXBpip5vcY8H35x7zBsBk1RzsmQXU7mh49Te3","versionTime":"2026-05-26T18:45:06Z","parameters":{"method":"did:webvh:1.0","scid":"QmWZCzqBbRzYvH4nWeVZ6gViAfRYNuci6aT8o7ajAJZzWe","updateKeys":["z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG"],"nextKeyHashes":["Qmf2V5jB2UwPcFL5bvmKed7VvY3CSQ1RXyDdtip7ufpQ3R"]},"state":{"@context":["https://www.w3.org/ns/did/v1","https://w3id.org/security/multikey/v1"],"id":"did:webvh:QmWZCzqBbRzYvH4nWeVZ6gViAfRYNuci6aT8o7ajAJZzWe:example.com","controller":"did:webvh:QmWZCzqBbRzYvH4nWeVZ6gViAfRYNuci6aT8o7ajAJZzWe:example.com","assertionMethod":[],"keyAgreement":[],"capabilityDelegation":[],"capabilityInvocation":[],"verificationMethod":[{"id":"did:webvh:QmWZCzqBbRzYvH4nWeVZ6gViAfRYNuci6aT8o7ajAJZzWe:example.com#P5RDjVJG","type":"Multikey","controller":"did:webvh:QmWZCzqBbRzYvH4nWeVZ6gViAfRYNuci6aT8o7ajAJZzWe:example.com","publicKeyMultibase":"z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG"}],"authentication":["did:webvh:QmWZCzqBbRzYvH4nWeVZ6gViAfRYNuci6aT8o7ajAJZzWe:example.com#P5RDjVJG"]},"proof":[{"type":"DataIntegrityProof","cryptosuite":"eddsa-jcs-2022","verificationMethod":"did:key:z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG#z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG","created":"2026-05-26T18:45:06Z","proofPurpose":"assertionMethod","proofValue":"z4zGxiksYG8M5rLPeqceJp6mGbcBVd1gBSywNuvRSQ4kvnnGCnUXFRQvV94XYGYpm71X9km882y9S5LXJhvvaSDzJ"}]} +{"versionId":"2-QmXhtcqT5EWWo8k66UMmTC2b8Mk86RZsXNDGGcX9xhYaPq","versionTime":"2026-05-26T18:45:07Z","parameters":{"updateKeys":["z6MknGc3ocHs3zdPiJbnaaqDi58NGb4pk1Sp9WxWufuXSdxf"],"nextKeyHashes":["QmdP2WQEBfT4vht72FZ2p2X7airS3FxmaGuoHgHQoDW1u9"]},"state":{"@context":["https://www.w3.org/ns/did/v1","https://w3id.org/security/multikey/v1"],"id":"did:webvh:QmWZCzqBbRzYvH4nWeVZ6gViAfRYNuci6aT8o7ajAJZzWe:example.com","controller":"did:webvh:QmWZCzqBbRzYvH4nWeVZ6gViAfRYNuci6aT8o7ajAJZzWe:example.com","assertionMethod":[],"keyAgreement":[],"capabilityDelegation":[],"capabilityInvocation":[],"verificationMethod":[{"id":"did:webvh:QmWZCzqBbRzYvH4nWeVZ6gViAfRYNuci6aT8o7ajAJZzWe:example.com#ufuXSdxf","type":"Multikey","controller":"did:webvh:QmWZCzqBbRzYvH4nWeVZ6gViAfRYNuci6aT8o7ajAJZzWe:example.com","publicKeyMultibase":"z6MknGc3ocHs3zdPiJbnaaqDi58NGb4pk1Sp9WxWufuXSdxf"}],"authentication":["did:webvh:QmWZCzqBbRzYvH4nWeVZ6gViAfRYNuci6aT8o7ajAJZzWe:example.com#ufuXSdxf"]},"proof":[{"type":"DataIntegrityProof","cryptosuite":"eddsa-jcs-2022","verificationMethod":"did:key:z6MknGc3ocHs3zdPiJbnaaqDi58NGb4pk1Sp9WxWufuXSdxf#z6MknGc3ocHs3zdPiJbnaaqDi58NGb4pk1Sp9WxWufuXSdxf","created":"2026-05-26T18:45:06Z","proofPurpose":"assertionMethod","proofValue":"z4oztszLXroa9TzZfDkCiAgR7AY2tr5VhJZK39zyQNzEqKb6Gnguyc1c8y41C221Qe316Xhb135r6BgtMU2uW1k5B"}]} +{"versionId":"3-QmXuBz3MwEkM6XNfQHxLde6Ar5SY1iASHBhNi9QanHiijn","versionTime":"2026-05-26T18:45:08Z","parameters":{"updateKeys":["z6MkvqoYXQfDDJRv8L4wKzxYeuKyVZBfi9Qo6Ro8MiLH3kDQ"],"nextKeyHashes":["QmPz1YVUT3mXMEixAWj8J6NkiQbskFmNu3DLYQLdynmrtR"]},"state":{"@context":["https://www.w3.org/ns/did/v1","https://w3id.org/security/multikey/v1"],"id":"did:webvh:QmWZCzqBbRzYvH4nWeVZ6gViAfRYNuci6aT8o7ajAJZzWe:example.com","controller":"did:webvh:QmWZCzqBbRzYvH4nWeVZ6gViAfRYNuci6aT8o7ajAJZzWe:example.com","assertionMethod":[],"keyAgreement":[],"capabilityDelegation":[],"capabilityInvocation":[],"verificationMethod":[{"id":"did:webvh:QmWZCzqBbRzYvH4nWeVZ6gViAfRYNuci6aT8o7ajAJZzWe:example.com#MiLH3kDQ","type":"Multikey","controller":"did:webvh:QmWZCzqBbRzYvH4nWeVZ6gViAfRYNuci6aT8o7ajAJZzWe:example.com","publicKeyMultibase":"z6MkvqoYXQfDDJRv8L4wKzxYeuKyVZBfi9Qo6Ro8MiLH3kDQ"}],"authentication":["did:webvh:QmWZCzqBbRzYvH4nWeVZ6gViAfRYNuci6aT8o7ajAJZzWe:example.com#MiLH3kDQ"]},"proof":[{"type":"DataIntegrityProof","cryptosuite":"eddsa-jcs-2022","verificationMethod":"did:key:z6MkvqoYXQfDDJRv8L4wKzxYeuKyVZBfi9Qo6Ro8MiLH3kDQ#z6MkvqoYXQfDDJRv8L4wKzxYeuKyVZBfi9Qo6Ro8MiLH3kDQ","created":"2026-05-26T18:45:06Z","proofPurpose":"assertionMethod","proofValue":"zF7V6bmuYMuhoUtKqmXazvi8xJcvpkq1FgZ14VN1LY5Meexy4gpb7MZwQ7iERtuWfi4TPrqDALgXa71by5exxWuB"}]} diff --git a/didwebvh-core/src/test/resources/interop/pre-rotation-consume-rust/did.jsonl b/didwebvh-core/src/test/resources/interop/pre-rotation-consume-rust/did.jsonl new file mode 100644 index 0000000..221d567 --- /dev/null +++ b/didwebvh-core/src/test/resources/interop/pre-rotation-consume-rust/did.jsonl @@ -0,0 +1,3 @@ +{"versionId":"1-QmUPj8g2FxDTpSRJugYRXJjv8w4NRYXsDPiaSkZc2ZJ18p","versionTime":"2000-01-01T00:00:00Z","parameters":{"method":"did:webvh:1.0","scid":"QmUQVmu7yyX6x9jCUH863pBacUmg8Wuu4ZZtsdfv7Uv5iD","updateKeys":["z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG"],"nextKeyHashes":["Qmf2V5jB2UwPcFL5bvmKed7VvY3CSQ1RXyDdtip7ufpQ3R"]},"state":{"@context":["https://www.w3.org/ns/did/v1","https://w3id.org/security/multikey/v1"],"assertionMethod":[],"authentication":["did:webvh:QmUQVmu7yyX6x9jCUH863pBacUmg8Wuu4ZZtsdfv7Uv5iD:example.com#P5RDjVJG"],"capabilityDelegation":[],"capabilityInvocation":[],"controller":"did:webvh:QmUQVmu7yyX6x9jCUH863pBacUmg8Wuu4ZZtsdfv7Uv5iD:example.com","id":"did:webvh:QmUQVmu7yyX6x9jCUH863pBacUmg8Wuu4ZZtsdfv7Uv5iD:example.com","keyAgreement":[],"verificationMethod":[{"controller":"did:webvh:QmUQVmu7yyX6x9jCUH863pBacUmg8Wuu4ZZtsdfv7Uv5iD:example.com","id":"did:webvh:QmUQVmu7yyX6x9jCUH863pBacUmg8Wuu4ZZtsdfv7Uv5iD:example.com#P5RDjVJG","publicKeyMultibase":"z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG","type":"Multikey"}]},"proof":[{"type":"DataIntegrityProof","cryptosuite":"eddsa-jcs-2022","created":"2026-05-14T19:50:40Z","verificationMethod":"did:key:z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG#z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG","proofPurpose":"assertionMethod","proofValue":"z4iA8Bb81tqDyjzXGqES4opXXjyVn2vhc6vWjXC6fdgCx5mMQz3nQWFpDrbgdA2WVH3noy8AcrP8uFFQHkxZfBe8M"}]} +{"versionId":"2-QmRUBVSMad4vNeSq14193UzmyEnsPATo5YFZPeRA1f7tq3","versionTime":"2000-01-02T00:00:00Z","parameters":{"updateKeys":["z6MknGc3ocHs3zdPiJbnaaqDi58NGb4pk1Sp9WxWufuXSdxf"],"nextKeyHashes":["QmdP2WQEBfT4vht72FZ2p2X7airS3FxmaGuoHgHQoDW1u9"]},"state":{"@context":["https://www.w3.org/ns/did/v1","https://w3id.org/security/multikey/v1"],"assertionMethod":[],"authentication":["did:webvh:QmUQVmu7yyX6x9jCUH863pBacUmg8Wuu4ZZtsdfv7Uv5iD:example.com#ufuXSdxf"],"capabilityDelegation":[],"capabilityInvocation":[],"controller":"did:webvh:QmUQVmu7yyX6x9jCUH863pBacUmg8Wuu4ZZtsdfv7Uv5iD:example.com","id":"did:webvh:QmUQVmu7yyX6x9jCUH863pBacUmg8Wuu4ZZtsdfv7Uv5iD:example.com","keyAgreement":[],"verificationMethod":[{"controller":"did:webvh:QmUQVmu7yyX6x9jCUH863pBacUmg8Wuu4ZZtsdfv7Uv5iD:example.com","id":"did:webvh:QmUQVmu7yyX6x9jCUH863pBacUmg8Wuu4ZZtsdfv7Uv5iD:example.com#ufuXSdxf","publicKeyMultibase":"z6MknGc3ocHs3zdPiJbnaaqDi58NGb4pk1Sp9WxWufuXSdxf","type":"Multikey"}]},"proof":[{"type":"DataIntegrityProof","cryptosuite":"eddsa-jcs-2022","created":"2026-05-14T19:50:40Z","verificationMethod":"did:key:z6MknGc3ocHs3zdPiJbnaaqDi58NGb4pk1Sp9WxWufuXSdxf#z6MknGc3ocHs3zdPiJbnaaqDi58NGb4pk1Sp9WxWufuXSdxf","proofPurpose":"assertionMethod","proofValue":"zCCpct89Th2j8ELnAHnKYSpHGChiyi4aooJQnEX3UFRbVqvwS85ke3TBQaYQpEWyUwvXzuuvSH5zn7y5koUgGBui"}]} +{"versionId":"3-QmTa52uaWAABzmTTfaKnEqpxwYNeTvuT8C2TStGZonrGKy","versionTime":"2000-01-03T00:00:00Z","parameters":{"updateKeys":["z6MkvqoYXQfDDJRv8L4wKzxYeuKyVZBfi9Qo6Ro8MiLH3kDQ"],"nextKeyHashes":["QmPz1YVUT3mXMEixAWj8J6NkiQbskFmNu3DLYQLdynmrtR"]},"state":{"@context":["https://www.w3.org/ns/did/v1","https://w3id.org/security/multikey/v1"],"assertionMethod":[],"authentication":["did:webvh:QmUQVmu7yyX6x9jCUH863pBacUmg8Wuu4ZZtsdfv7Uv5iD:example.com#MiLH3kDQ"],"capabilityDelegation":[],"capabilityInvocation":[],"controller":"did:webvh:QmUQVmu7yyX6x9jCUH863pBacUmg8Wuu4ZZtsdfv7Uv5iD:example.com","id":"did:webvh:QmUQVmu7yyX6x9jCUH863pBacUmg8Wuu4ZZtsdfv7Uv5iD:example.com","keyAgreement":[],"verificationMethod":[{"controller":"did:webvh:QmUQVmu7yyX6x9jCUH863pBacUmg8Wuu4ZZtsdfv7Uv5iD:example.com","id":"did:webvh:QmUQVmu7yyX6x9jCUH863pBacUmg8Wuu4ZZtsdfv7Uv5iD:example.com#MiLH3kDQ","publicKeyMultibase":"z6MkvqoYXQfDDJRv8L4wKzxYeuKyVZBfi9Qo6Ro8MiLH3kDQ","type":"Multikey"}]},"proof":[{"type":"DataIntegrityProof","cryptosuite":"eddsa-jcs-2022","created":"2026-05-14T19:50:40Z","verificationMethod":"did:key:z6MkvqoYXQfDDJRv8L4wKzxYeuKyVZBfi9Qo6Ro8MiLH3kDQ#z6MkvqoYXQfDDJRv8L4wKzxYeuKyVZBfi9Qo6Ro8MiLH3kDQ","proofPurpose":"assertionMethod","proofValue":"z2ufgLc1eVP8biA9PHGff18mz5AGt5YziGxUwzLonk4UvwycU6hhwnsL8WPvNZYc73DC1UVMU3C5iEjV2y6zynxbn"}]} diff --git a/didwebvh-core/src/test/resources/test-vectors/pre-rotation-log.jsonl b/didwebvh-core/src/test/resources/test-vectors/pre-rotation-log.jsonl index 64be48a..58b7b55 100644 --- a/didwebvh-core/src/test/resources/test-vectors/pre-rotation-log.jsonl +++ b/didwebvh-core/src/test/resources/test-vectors/pre-rotation-log.jsonl @@ -1,2 +1,2 @@ -{"versionId":"1-QmT5Qo2RhdsxELYaA364TzVtNtRw3MQjy2hNTkXxbtUbDe","versionTime":"2026-04-20T14:56:01Z","parameters":{"method":"did:webvh:1.0","scid":"QmbE6xSTXgsmRJK4651HjoEk4nVo35EsmDTcEE2FkFhgxy","updateKeys":["z6MkrqE7t5Sepvay1uuKJfMpjzsJ1cBdqibx8b7C3VwiE7wK"],"nextKeyHashes":["QmNaXxedyD9kNfajeHzeRqybxiV3s6ajVAQ5oNmeDPmx6q"]},"state":{"@context":"https://www.w3.org/ns/did/v1","id":"did:webvh:QmbE6xSTXgsmRJK4651HjoEk4nVo35EsmDTcEE2FkFhgxy:example.com","controller":"did:webvh:QmbE6xSTXgsmRJK4651HjoEk4nVo35EsmDTcEE2FkFhgxy:example.com","verificationMethod":[{"id":"did:webvh:QmbE6xSTXgsmRJK4651HjoEk4nVo35EsmDTcEE2FkFhgxy:example.com#z6MkrqE7t5Sepvay1uuKJfMpjzsJ1cBdqibx8b7C3VwiE7wK","type":"Multikey","controller":"did:webvh:QmbE6xSTXgsmRJK4651HjoEk4nVo35EsmDTcEE2FkFhgxy:example.com","publicKeyMultibase":"z6MkrqE7t5Sepvay1uuKJfMpjzsJ1cBdqibx8b7C3VwiE7wK"}],"authentication":["did:webvh:QmbE6xSTXgsmRJK4651HjoEk4nVo35EsmDTcEE2FkFhgxy:example.com#z6MkrqE7t5Sepvay1uuKJfMpjzsJ1cBdqibx8b7C3VwiE7wK"],"assertionMethod":["did:webvh:QmbE6xSTXgsmRJK4651HjoEk4nVo35EsmDTcEE2FkFhgxy:example.com#z6MkrqE7t5Sepvay1uuKJfMpjzsJ1cBdqibx8b7C3VwiE7wK"]},"proof":[{"type":"DataIntegrityProof","cryptosuite":"eddsa-jcs-2022","verificationMethod":"did:key:z6MkrqE7t5Sepvay1uuKJfMpjzsJ1cBdqibx8b7C3VwiE7wK#z6MkrqE7t5Sepvay1uuKJfMpjzsJ1cBdqibx8b7C3VwiE7wK","proofPurpose":"assertionMethod","created":"2026-04-20T14:56:01Z","proofValue":"z39ba6J3swdeidJRoTNeM1DdVMrP5EjLficinnYSJmTbJJnRWcEoHrYNCyfrP2CrUVeDXkEfLWH23vC9nJqonPJmt"}]} -{"versionId":"2-QmbdSE2Fki4pyn1x5ko7HzkNia8wC1UyNmmExxUjo8rG4b","versionTime":"2026-04-20T14:56:02Z","parameters":{"updateKeys":["z6MkojnT9YPG1rGnAfsp7GheGT8iXEYYZUgZyEDzMjXBjxsT"],"nextKeyHashes":["QmRNAiJ626yV1ajz68ZrSu4tdYUQwio2gSq49j8kigtbBi"]},"state":{"@context":"https://www.w3.org/ns/did/v1","id":"did:webvh:QmbE6xSTXgsmRJK4651HjoEk4nVo35EsmDTcEE2FkFhgxy:example.com","controller":"did:webvh:QmbE6xSTXgsmRJK4651HjoEk4nVo35EsmDTcEE2FkFhgxy:example.com","verificationMethod":[{"id":"did:webvh:QmbE6xSTXgsmRJK4651HjoEk4nVo35EsmDTcEE2FkFhgxy:example.com#z6MkrqE7t5Sepvay1uuKJfMpjzsJ1cBdqibx8b7C3VwiE7wK","type":"Multikey","controller":"did:webvh:QmbE6xSTXgsmRJK4651HjoEk4nVo35EsmDTcEE2FkFhgxy:example.com","publicKeyMultibase":"z6MkrqE7t5Sepvay1uuKJfMpjzsJ1cBdqibx8b7C3VwiE7wK"}],"authentication":["did:webvh:QmbE6xSTXgsmRJK4651HjoEk4nVo35EsmDTcEE2FkFhgxy:example.com#z6MkrqE7t5Sepvay1uuKJfMpjzsJ1cBdqibx8b7C3VwiE7wK"],"assertionMethod":["did:webvh:QmbE6xSTXgsmRJK4651HjoEk4nVo35EsmDTcEE2FkFhgxy:example.com#z6MkrqE7t5Sepvay1uuKJfMpjzsJ1cBdqibx8b7C3VwiE7wK"]},"proof":[{"type":"DataIntegrityProof","cryptosuite":"eddsa-jcs-2022","verificationMethod":"did:key:z6MkrqE7t5Sepvay1uuKJfMpjzsJ1cBdqibx8b7C3VwiE7wK#z6MkrqE7t5Sepvay1uuKJfMpjzsJ1cBdqibx8b7C3VwiE7wK","proofPurpose":"assertionMethod","created":"2026-04-20T14:56:01Z","proofValue":"z28Au5NWzieDc1gods6i7VcyGUYQds9DdNeYSQaiU6g3Qjv3nuVAhjeqCPcMinugpM2JyK8C4y39ViD7yxEVApt4E"}]} +{"versionId":"1-QmPPJTcbGS9iLViSJV1TQCRyhTS5RAeWZ8PxxCqo7UhBVo","versionTime":"2026-05-27T09:14:22Z","parameters":{"method":"did:webvh:1.0","scid":"QmdgCRqQ1w2MBn6pdxzragAfLdtrxW3tkut6yZyAEchAhX","updateKeys":["z6MkrqE7t5Sepvay1uuKJfMpjzsJ1cBdqibx8b7C3VwiE7wK"],"nextKeyHashes":["QmNaXxedyD9kNfajeHzeRqybxiV3s6ajVAQ5oNmeDPmx6q"]},"state":{"@context":"https://www.w3.org/ns/did/v1","id":"did:webvh:QmdgCRqQ1w2MBn6pdxzragAfLdtrxW3tkut6yZyAEchAhX:example.com","controller":"did:webvh:QmdgCRqQ1w2MBn6pdxzragAfLdtrxW3tkut6yZyAEchAhX:example.com","verificationMethod":[{"id":"did:webvh:QmdgCRqQ1w2MBn6pdxzragAfLdtrxW3tkut6yZyAEchAhX:example.com#z6MkrqE7t5Sepvay1uuKJfMpjzsJ1cBdqibx8b7C3VwiE7wK","type":"Multikey","controller":"did:webvh:QmdgCRqQ1w2MBn6pdxzragAfLdtrxW3tkut6yZyAEchAhX:example.com","publicKeyMultibase":"z6MkrqE7t5Sepvay1uuKJfMpjzsJ1cBdqibx8b7C3VwiE7wK"}],"authentication":["did:webvh:QmdgCRqQ1w2MBn6pdxzragAfLdtrxW3tkut6yZyAEchAhX:example.com#z6MkrqE7t5Sepvay1uuKJfMpjzsJ1cBdqibx8b7C3VwiE7wK"],"assertionMethod":["did:webvh:QmdgCRqQ1w2MBn6pdxzragAfLdtrxW3tkut6yZyAEchAhX:example.com#z6MkrqE7t5Sepvay1uuKJfMpjzsJ1cBdqibx8b7C3VwiE7wK"]},"proof":[{"type":"DataIntegrityProof","cryptosuite":"eddsa-jcs-2022","verificationMethod":"did:key:z6MkrqE7t5Sepvay1uuKJfMpjzsJ1cBdqibx8b7C3VwiE7wK#z6MkrqE7t5Sepvay1uuKJfMpjzsJ1cBdqibx8b7C3VwiE7wK","proofPurpose":"assertionMethod","created":"2026-05-27T09:14:22Z","proofValue":"z5kPyxyxer98gwxWkMh21Y3v8eigTFjLqPBqPejy5kwiiiauVKqribDjmvHABW6sexkEFfLCt6pmxY2juYHQJL4fJ"}]} +{"versionId":"2-QmamjEZx9jEPxhL16D1PMnXfB8G14T2UaWmBNT73Gak1pV","versionTime":"2026-05-27T09:14:23Z","parameters":{"updateKeys":["z6MkojnT9YPG1rGnAfsp7GheGT8iXEYYZUgZyEDzMjXBjxsT"],"nextKeyHashes":["QmRNAiJ626yV1ajz68ZrSu4tdYUQwio2gSq49j8kigtbBi"]},"state":{"@context":"https://www.w3.org/ns/did/v1","id":"did:webvh:QmdgCRqQ1w2MBn6pdxzragAfLdtrxW3tkut6yZyAEchAhX:example.com","controller":"did:webvh:QmdgCRqQ1w2MBn6pdxzragAfLdtrxW3tkut6yZyAEchAhX:example.com","verificationMethod":[{"id":"did:webvh:QmdgCRqQ1w2MBn6pdxzragAfLdtrxW3tkut6yZyAEchAhX:example.com#z6MkrqE7t5Sepvay1uuKJfMpjzsJ1cBdqibx8b7C3VwiE7wK","type":"Multikey","controller":"did:webvh:QmdgCRqQ1w2MBn6pdxzragAfLdtrxW3tkut6yZyAEchAhX:example.com","publicKeyMultibase":"z6MkrqE7t5Sepvay1uuKJfMpjzsJ1cBdqibx8b7C3VwiE7wK"}],"authentication":["did:webvh:QmdgCRqQ1w2MBn6pdxzragAfLdtrxW3tkut6yZyAEchAhX:example.com#z6MkrqE7t5Sepvay1uuKJfMpjzsJ1cBdqibx8b7C3VwiE7wK"],"assertionMethod":["did:webvh:QmdgCRqQ1w2MBn6pdxzragAfLdtrxW3tkut6yZyAEchAhX:example.com#z6MkrqE7t5Sepvay1uuKJfMpjzsJ1cBdqibx8b7C3VwiE7wK"]},"proof":[{"type":"DataIntegrityProof","cryptosuite":"eddsa-jcs-2022","verificationMethod":"did:key:z6MkojnT9YPG1rGnAfsp7GheGT8iXEYYZUgZyEDzMjXBjxsT#z6MkojnT9YPG1rGnAfsp7GheGT8iXEYYZUgZyEDzMjXBjxsT","proofPurpose":"assertionMethod","created":"2026-05-27T09:14:22Z","proofValue":"z468s8dwvyrCZUD8iEXJEG5pSm9HfEezWaJSXgtpbTyi6pTPP3Ga212yobbdsCpMVLym9T6ENDFftCvXodiEsM2QH"}]} From a85301ac89468c24321ac745a203786ac699f86b Mon Sep 17 00:00:00 2001 From: Reza Maghoul Date: Wed, 27 May 2026 11:24:07 +0200 Subject: [PATCH 3/9] fix(core): accept pruned witness proofs and bare-multikey witness ids MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Spec §3.7.8 (lines 1244-1247, 1286-1304): a witness's proof at versionId V implicitly approves every prior log entry, and DID Controllers SHOULD prune did-witness.json to keep only the latest proof per witness. Resolvers MUST accept pruned files and MUST ignore proofs whose versionId is not in the published log. WitnessValidator previously looked up an exact-versionId proof entry per log entry and rejected with "missing witness proof" when absent — so the Rust witness-update vector (single proof entry at versionId 2 covering both log entries) failed. It also reconstructed authorized witness ids as "did:key:" from the proof's verificationMethod, which fails to match Rust's bare-multikey form in the witness-threshold vector. Rewrite to pre-verify every published proof once, then for each entry needing witnessing count distinct authorized witnesses whose verified proof targets a versionId at or after the entry's. Match authorized witness ids by their multikey portion so the spec form (did:key:z6Mk...) and the bare form (z6Mk...) both round-trip. Interop coverage: InteropRustWitnessProofsTest exercises the rust witness-update and witness-threshold vectors from swcurran/didwebvh-test-suite@c11fda3 that previously failed with "missing witness proof for entry 1-..." and "insufficient witness proofs ... need 1, got 0". Refs: #2 --- .../core/validate/WitnessValidator.java | 129 +++++++++++++----- .../interop/InteropRustWitnessProofsTest.java | 77 +++++++++++ .../core/validate/WitnessValidatorTest.java | 4 +- .../src/test/resources/interop/README.md | 2 + .../witness-threshold-rust/did-witness.json | 15 ++ .../interop/witness-threshold-rust/did.jsonl | 1 + .../witness-update-rust/did-witness.json | 23 ++++ .../interop/witness-update-rust/did.jsonl | 2 + 8 files changed, 217 insertions(+), 36 deletions(-) create mode 100644 didwebvh-core/src/test/java/io/github/decentralizedidentity/didwebvh/core/interop/InteropRustWitnessProofsTest.java create mode 100644 didwebvh-core/src/test/resources/interop/witness-threshold-rust/did-witness.json create mode 100644 didwebvh-core/src/test/resources/interop/witness-threshold-rust/did.jsonl create mode 100644 didwebvh-core/src/test/resources/interop/witness-update-rust/did-witness.json create mode 100644 didwebvh-core/src/test/resources/interop/witness-update-rust/did.jsonl diff --git a/didwebvh-core/src/main/java/io/github/decentralizedidentity/didwebvh/core/validate/WitnessValidator.java b/didwebvh-core/src/main/java/io/github/decentralizedidentity/didwebvh/core/validate/WitnessValidator.java index 053c7a2..1949ac7 100644 --- a/didwebvh-core/src/main/java/io/github/decentralizedidentity/didwebvh/core/validate/WitnessValidator.java +++ b/didwebvh-core/src/main/java/io/github/decentralizedidentity/didwebvh/core/validate/WitnessValidator.java @@ -4,13 +4,18 @@ import io.github.decentralizedidentity.didwebvh.core.model.DataIntegrityProof; import io.github.decentralizedidentity.didwebvh.core.model.LogEntry; import io.github.decentralizedidentity.didwebvh.core.model.Parameters; +import io.github.decentralizedidentity.didwebvh.core.model.VersionId; import io.github.decentralizedidentity.didwebvh.core.signing.ProofVerifier; import io.github.decentralizedidentity.didwebvh.core.witness.WitnessConfig; import io.github.decentralizedidentity.didwebvh.core.witness.WitnessEntry; import io.github.decentralizedidentity.didwebvh.core.witness.WitnessProofCollection; import io.github.decentralizedidentity.didwebvh.core.witness.WitnessProofEntry; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.Set; /** Validates witness proofs for entries that require witnessing. */ public final class WitnessValidator { @@ -18,8 +23,17 @@ public final class WitnessValidator { /** * Validate witness proofs for entries starting at {@code fromEntryIndex}. * - *

For each entry at or after {@code fromEntryIndex} where witnessing is active, - * verifies that at least {@code threshold} valid proofs exist from authorized witnesses. + *

Spec §3.7.8 (lines 1244-1247, 1286-1304): a witness proof at versionId V + * implicitly approves every prior log entry. The DID Controller is expected + * to prune {@code did-witness.json} so only the latest proof per witness + * remains, and resolvers MUST accept this. Proofs whose {@code versionId} is + * not present in the published log are ignored. + * + *

For each log entry with active witnessing, this validator counts the + * distinct authorized witnesses that have signed at least one valid proof + * at a versionId in the published log whose version number is greater than + * or equal to the entry's. If that count meets the entry's threshold the + * entry is witnessed. * * @param entries the full log entry list (used to accumulate parameters) * @param witnessProofs the witness proof collection from {@code did-witness.json} @@ -33,7 +47,9 @@ public WitnessValidationResult validate(List entries, return WitnessValidationResult.failure(-1, "witness proofs collection is null"); } - // Reconstruct active parameters up to fromEntryIndex + Map publishedVersionNumbers = publishedVersionNumbers(entries); + List verifiedProofs = verifyProofs(witnessProofs, publishedVersionNumbers); + Parameters activeParams = new Parameters(); for (int i = 0; i < fromEntryIndex && i < entries.size(); i++) { Parameters ep = entries.get(i).getParameters(); @@ -53,59 +69,102 @@ public WitnessValidationResult validate(List entries, continue; } - WitnessProofEntry witnessProofEntry = - findWitnessProof(witnessProofs, entry.getVersionId()); - if (witnessProofEntry == null) { - return WitnessValidationResult.failure(i, - "missing witness proof for entry " + entry.getVersionId()); - } - - // The signed document is {"versionId": ""} - JsonObject signedDoc = new JsonObject(); - signedDoc.addProperty("versionId", entry.getVersionId()); - - List authorizedWitnesses = witnessConfig.getWitnesses(); - int validProofCount = 0; + int entryVersion = VersionId.parse(entry.getVersionId()).getVersionNumber(); + List authorized = witnessConfig.getWitnesses(); - for (DataIntegrityProof proof : witnessProofEntry.getProof()) { - String multikey = ProofVerifier.extractMultikey(proof.getVerificationMethod()); - String witnessDid = "did:key:" + multikey; - if (!isAuthorizedWitness(witnessDid, authorizedWitnesses)) { + Set approvers = new HashSet<>(); + for (VerifiedProof vp : verifiedProofs) { + if (vp.versionNumber < entryVersion) { continue; } - if (ProofVerifier.verify(proof, signedDoc)) { - validProofCount++; + String matched = matchAuthorizedWitness(vp.signerMultikey, authorized); + if (matched != null) { + approvers.add(matched); } } - if (validProofCount < witnessConfig.getThreshold()) { + if (approvers.size() < witnessConfig.getThreshold()) { return WitnessValidationResult.failure(i, "insufficient witness proofs for entry " + entry.getVersionId() + ": need " + witnessConfig.getThreshold() - + ", got " + validProofCount); + + ", got " + approvers.size()); } } return WitnessValidationResult.success(); } - private boolean isAuthorizedWitness(String witnessDid, - List authorizedWitnesses) { - for (WitnessEntry w : authorizedWitnesses) { - if (witnessDid.equals(w.getId())) { - return true; + /** Map from each entry's full versionId string to its parsed version number. */ + private static Map publishedVersionNumbers(List entries) { + Map out = new HashMap<>(); + for (LogEntry e : entries) { + out.put(e.getVersionId(), VersionId.parse(e.getVersionId()).getVersionNumber()); + } + return out; + } + + /** + * Verify every proof in the witness file once and keep the ones that + * (a) are published, (b) verify against {"versionId": ""}. Returns one + * entry per verified proof with the signer's multikey already extracted. + */ + private static List verifyProofs(WitnessProofCollection witnessProofs, + Map publishedVersionNumbers) { + List out = new java.util.ArrayList<>(); + for (WitnessProofEntry entry : witnessProofs.getEntries()) { + Integer versionNumber = publishedVersionNumbers.get(entry.getVersionId()); + if (versionNumber == null) { + // Spec line 1294: ignore proofs for unpublished entries. + continue; + } + JsonObject signedDoc = new JsonObject(); + signedDoc.addProperty("versionId", entry.getVersionId()); + if (entry.getProof() == null) { + continue; + } + for (DataIntegrityProof proof : entry.getProof()) { + if (!ProofVerifier.verify(proof, signedDoc)) { + continue; + } + String signerMultikey = ProofVerifier.extractMultikey(proof.getVerificationMethod()); + out.add(new VerifiedProof(versionNumber, signerMultikey)); } } - return false; + return out; } - private WitnessProofEntry findWitnessProof(WitnessProofCollection collection, - String versionId) { - for (WitnessProofEntry entry : collection.getEntries()) { - if (versionId.equals(entry.getVersionId())) { - return entry; + /** + * Return the canonical authorized-witness id matching {@code signerMultikey}, + * or {@code null} if not authorized. Accepts the spec form + * {@code did:key:<multikey>} and the bare-multikey form some + * implementations emit; comparison is on the multikey portion. + */ + private static String matchAuthorizedWitness(String signerMultikey, + List authorized) { + for (WitnessEntry w : authorized) { + if (signerMultikey.equals(stripDidKey(w.getId()))) { + return w.getId(); } } return null; } + + private static String stripDidKey(String witnessId) { + if (witnessId == null) { + return null; + } + String prefix = "did:key:"; + return witnessId.startsWith(prefix) ? witnessId.substring(prefix.length()) : witnessId; + } + + /** Pre-verified proof entry indexed by its (published) version number and signer. */ + private static final class VerifiedProof { + final int versionNumber; + final String signerMultikey; + + VerifiedProof(int versionNumber, String signerMultikey) { + this.versionNumber = versionNumber; + this.signerMultikey = signerMultikey; + } + } } diff --git a/didwebvh-core/src/test/java/io/github/decentralizedidentity/didwebvh/core/interop/InteropRustWitnessProofsTest.java b/didwebvh-core/src/test/java/io/github/decentralizedidentity/didwebvh/core/interop/InteropRustWitnessProofsTest.java new file mode 100644 index 0000000..be68a87 --- /dev/null +++ b/didwebvh-core/src/test/java/io/github/decentralizedidentity/didwebvh/core/interop/InteropRustWitnessProofsTest.java @@ -0,0 +1,77 @@ +package io.github.decentralizedidentity.didwebvh.core.interop; + +import io.github.decentralizedidentity.didwebvh.core.model.JsonSupport; +import io.github.decentralizedidentity.didwebvh.core.model.LogEntry; +import io.github.decentralizedidentity.didwebvh.core.validate.WitnessValidationResult; +import io.github.decentralizedidentity.didwebvh.core.validate.WitnessValidator; +import io.github.decentralizedidentity.didwebvh.core.witness.WitnessProofCollection; +import io.github.decentralizedidentity.didwebvh.core.witness.WitnessProofEntry; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Interop regression: per spec §3.7.8 (lines 1244-1247, 1298-1304), a witness + * proof at versionId V implicitly approves all prior log entries; the DID + * Controller SHOULD prune did-witness.json to keep only the latest proof per + * witness. Resolvers MUST accept this. The Rust witness-update vector has a + * single proof entry at versionId 2 covering both log entries; the Rust + * witness-threshold vector encodes witness ids as bare multikeys rather than + * the {@code did:key:<multikey>} form. + * + *

Source: swcurran/didwebvh-test-suite, vectors/{witness-update,witness-threshold}/rust/. + */ +class InteropRustWitnessProofsTest { + + @ParameterizedTest(name = "{0}") + @ValueSource(strings = {"witness-update", "witness-threshold"}) + void validatesRustWitnessProofs(String vector) throws IOException { + List entries = parseJsonl(readVector("/interop/" + vector + "-rust/did.jsonl")); + WitnessProofCollection proofs = parseProofs(readVector("/interop/" + vector + "-rust/did-witness.json")); + + WitnessValidationResult result = new WitnessValidator().validate(entries, proofs, 0); + + assertThat(result.isValid()) + .as("rust %s witness proofs must verify; failure: %s", vector, result.getFailureReason()) + .isTrue(); + } + + private static WitnessProofCollection parseProofs(String json) { + WitnessProofEntry[] arr = JsonSupport.compact().fromJson(json, WitnessProofEntry[].class); + return new WitnessProofCollection(Arrays.asList(arr)); + } + + private static List parseJsonl(String jsonl) { + List out = new ArrayList<>(); + for (String line : jsonl.split("\n")) { + String trimmed = line.trim(); + if (!trimmed.isEmpty()) { + out.add(LogEntry.fromJsonLine(trimmed)); + } + } + return out; + } + + private static String readVector(String resourcePath) throws IOException { + try (InputStream in = InteropRustWitnessProofsTest.class.getResourceAsStream(resourcePath)) { + if (in == null) { + throw new IllegalStateException("Missing test resource: " + resourcePath); + } + java.io.ByteArrayOutputStream out = new java.io.ByteArrayOutputStream(); + byte[] buf = new byte[4096]; + int read; + while ((read = in.read(buf)) > 0) { + out.write(buf, 0, read); + } + return new String(out.toByteArray(), StandardCharsets.UTF_8); + } + } +} diff --git a/didwebvh-core/src/test/java/io/github/decentralizedidentity/didwebvh/core/validate/WitnessValidatorTest.java b/didwebvh-core/src/test/java/io/github/decentralizedidentity/didwebvh/core/validate/WitnessValidatorTest.java index c239f61..b82427f 100644 --- a/didwebvh-core/src/test/java/io/github/decentralizedidentity/didwebvh/core/validate/WitnessValidatorTest.java +++ b/didwebvh-core/src/test/java/io/github/decentralizedidentity/didwebvh/core/validate/WitnessValidatorTest.java @@ -133,7 +133,9 @@ void missingWitnessProofEntryFails() { Collections.singletonList(entry), proofs, 0); assertThat(result.isValid()).isFalse(); - assertThat(result.getFailureReason()).contains("missing witness proof"); + // Spec §3.7.8: proofs whose versionId is not in the published log are + // ignored. With no usable proofs, the threshold is unmet. + assertThat(result.getFailureReason()).contains("insufficient witness proofs"); } @Test diff --git a/didwebvh-core/src/test/resources/interop/README.md b/didwebvh-core/src/test/resources/interop/README.md index 2502f9f..c0c0a12 100644 --- a/didwebvh-core/src/test/resources/interop/README.md +++ b/didwebvh-core/src/test/resources/interop/README.md @@ -21,6 +21,8 @@ verbatim from the upstream `vectors/` tree and exercised by JUnit tests under | `witness-update-java-eecc/`| `vectors/witness-update/java-eecc/`| java-eecc witness-update log validation | | `pre-rotation-consume-rust/` | `vectors/pre-rotation-consume/rust/` | Pre-rotation: each entry signed by its own (committed) updateKeys | | `pre-rotation-consume-java-eecc/`| `vectors/pre-rotation-consume/java-eecc/`| Pre-rotation: each entry signed by its own (committed) updateKeys | +| `witness-update-rust/` | `vectors/witness-update/rust/` | Witness proof pruning: single latest proof covers all prior entries | +| `witness-threshold-rust/` | `vectors/witness-threshold/rust/` | Witness id as bare multikey (without `did:key:` prefix) | To refresh: bump the SHA above, re-download the listed files (`curl -sSL https://raw.githubusercontent.com/swcurran/didwebvh-test-suite//`), diff --git a/didwebvh-core/src/test/resources/interop/witness-threshold-rust/did-witness.json b/didwebvh-core/src/test/resources/interop/witness-threshold-rust/did-witness.json new file mode 100644 index 0000000..94f640f --- /dev/null +++ b/didwebvh-core/src/test/resources/interop/witness-threshold-rust/did-witness.json @@ -0,0 +1,15 @@ +[ + { + "versionId": "1-QmdDTDXMshdRwAbnN7b22BKAUy1CvQsq6y6zQqSP7reDJ7", + "proof": [ + { + "type": "DataIntegrityProof", + "cryptosuite": "eddsa-jcs-2022", + "created": "2026-05-14T19:50:40Z", + "verificationMethod": "did:key:z6Mkrv5Cm2XCLumMPTqooLTCw6YDf421d7VdTziwrZ8vNf4L#z6Mkrv5Cm2XCLumMPTqooLTCw6YDf421d7VdTziwrZ8vNf4L", + "proofPurpose": "assertionMethod", + "proofValue": "z4kxvBoDWKQ87AyuzjUYNkcohnyku8yH49FDHvo9Dpf8ktpmrpReXhWafxJ1QmDoJuMg3PUgQGTMmyoxyyyojKNqC" + } + ] + } +] \ No newline at end of file diff --git a/didwebvh-core/src/test/resources/interop/witness-threshold-rust/did.jsonl b/didwebvh-core/src/test/resources/interop/witness-threshold-rust/did.jsonl new file mode 100644 index 0000000..b4cdff6 --- /dev/null +++ b/didwebvh-core/src/test/resources/interop/witness-threshold-rust/did.jsonl @@ -0,0 +1 @@ +{"versionId":"1-QmdDTDXMshdRwAbnN7b22BKAUy1CvQsq6y6zQqSP7reDJ7","versionTime":"2000-01-01T00:00:00Z","parameters":{"method":"did:webvh:1.0","scid":"QmW9K6bJYn5LhixSSi7UKD3W7uNGeQtKKuZ9NzqYtUcK69","updateKeys":["z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG"],"witness":{"threshold":1,"witnesses":[{"id":"z6Mkrv5Cm2XCLumMPTqooLTCw6YDf421d7VdTziwrZ8vNf4L"}]}},"state":{"@context":["https://www.w3.org/ns/did/v1","https://w3id.org/security/multikey/v1"],"assertionMethod":[],"authentication":["did:webvh:QmW9K6bJYn5LhixSSi7UKD3W7uNGeQtKKuZ9NzqYtUcK69:example.com#P5RDjVJG"],"capabilityDelegation":[],"capabilityInvocation":[],"controller":"did:webvh:QmW9K6bJYn5LhixSSi7UKD3W7uNGeQtKKuZ9NzqYtUcK69:example.com","id":"did:webvh:QmW9K6bJYn5LhixSSi7UKD3W7uNGeQtKKuZ9NzqYtUcK69:example.com","keyAgreement":[],"verificationMethod":[{"controller":"did:webvh:QmW9K6bJYn5LhixSSi7UKD3W7uNGeQtKKuZ9NzqYtUcK69:example.com","id":"did:webvh:QmW9K6bJYn5LhixSSi7UKD3W7uNGeQtKKuZ9NzqYtUcK69:example.com#P5RDjVJG","publicKeyMultibase":"z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG","type":"Multikey"}]},"proof":[{"type":"DataIntegrityProof","cryptosuite":"eddsa-jcs-2022","created":"2026-05-14T19:50:40Z","verificationMethod":"did:key:z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG#z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG","proofPurpose":"assertionMethod","proofValue":"zWJXqQPpbg2RF34Qp9bumPJz3vMNuCgpUk1NHZuhA9q6H5hByDrUBzZu2hcKqPUDN93AVfA54mWAQEu4H4mxxvUt"}]} diff --git a/didwebvh-core/src/test/resources/interop/witness-update-rust/did-witness.json b/didwebvh-core/src/test/resources/interop/witness-update-rust/did-witness.json new file mode 100644 index 0000000..5c1fd8b --- /dev/null +++ b/didwebvh-core/src/test/resources/interop/witness-update-rust/did-witness.json @@ -0,0 +1,23 @@ +[ + { + "versionId": "2-Qmbs1akRcXtWH1sWTGGMD5DHdaKAJk7CTK5gP3hwPDEUpe", + "proof": [ + { + "type": "DataIntegrityProof", + "cryptosuite": "eddsa-jcs-2022", + "created": "2026-05-14T19:50:40Z", + "verificationMethod": "did:key:z6Mkrv5Cm2XCLumMPTqooLTCw6YDf421d7VdTziwrZ8vNf4L#z6Mkrv5Cm2XCLumMPTqooLTCw6YDf421d7VdTziwrZ8vNf4L", + "proofPurpose": "assertionMethod", + "proofValue": "z43FgUhcYtLScBx7JCuvfQPF7GqD9pNo17NurhrNSpyaaXpfXHbKxbdB1n4sxRfrEGgPBg5Gs2UdzxUNy3uqUPGbL" + }, + { + "type": "DataIntegrityProof", + "cryptosuite": "eddsa-jcs-2022", + "created": "2026-05-14T19:50:40Z", + "verificationMethod": "did:key:z6MkjnHi6KJKx625x56sxudLoKdDVDABJ5VHHdXRFPUea7NP#z6MkjnHi6KJKx625x56sxudLoKdDVDABJ5VHHdXRFPUea7NP", + "proofPurpose": "assertionMethod", + "proofValue": "z2t93XSkNLswTLRcYWYfT6VEoujCQa6BmLbp7CGQyxsSsHvvqkbJm4Bmu6DsQguUAaeo6Ddgs8wGxWzSNgXJQwuPu" + } + ] + } +] \ No newline at end of file diff --git a/didwebvh-core/src/test/resources/interop/witness-update-rust/did.jsonl b/didwebvh-core/src/test/resources/interop/witness-update-rust/did.jsonl new file mode 100644 index 0000000..c8c4a54 --- /dev/null +++ b/didwebvh-core/src/test/resources/interop/witness-update-rust/did.jsonl @@ -0,0 +1,2 @@ +{"versionId":"1-QmayLd7TkamnZhA8EbQCP6aUvU7YvGW1xW2iauRFC47ziP","versionTime":"2000-01-01T00:00:00Z","parameters":{"method":"did:webvh:1.0","scid":"QmVArGEbmoYrzKp9wmpnCPYLjnvwHWhYC1jK8B8WCwgBd8","updateKeys":["z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG"],"witness":{"threshold":2,"witnesses":[{"id":"z6Mkrv5Cm2XCLumMPTqooLTCw6YDf421d7VdTziwrZ8vNf4L"},{"id":"z6MkjnHi6KJKx625x56sxudLoKdDVDABJ5VHHdXRFPUea7NP"}]}},"state":{"@context":["https://www.w3.org/ns/did/v1","https://w3id.org/security/multikey/v1"],"assertionMethod":[],"authentication":["did:webvh:QmVArGEbmoYrzKp9wmpnCPYLjnvwHWhYC1jK8B8WCwgBd8:example.com#P5RDjVJG"],"capabilityDelegation":[],"capabilityInvocation":[],"controller":"did:webvh:QmVArGEbmoYrzKp9wmpnCPYLjnvwHWhYC1jK8B8WCwgBd8:example.com","id":"did:webvh:QmVArGEbmoYrzKp9wmpnCPYLjnvwHWhYC1jK8B8WCwgBd8:example.com","keyAgreement":[],"verificationMethod":[{"controller":"did:webvh:QmVArGEbmoYrzKp9wmpnCPYLjnvwHWhYC1jK8B8WCwgBd8:example.com","id":"did:webvh:QmVArGEbmoYrzKp9wmpnCPYLjnvwHWhYC1jK8B8WCwgBd8:example.com#P5RDjVJG","publicKeyMultibase":"z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG","type":"Multikey"}]},"proof":[{"type":"DataIntegrityProof","cryptosuite":"eddsa-jcs-2022","created":"2026-05-14T19:50:40Z","verificationMethod":"did:key:z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG#z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG","proofPurpose":"assertionMethod","proofValue":"z5VBHsMVCKQRAbvgFrdjT2xjcm8qxyKtbTMdupiKrBpZHyks7YuX5UfsnX3XHzXYMsncTkpvvognXySAAwddhrW6j"}]} +{"versionId":"2-Qmbs1akRcXtWH1sWTGGMD5DHdaKAJk7CTK5gP3hwPDEUpe","versionTime":"2000-01-02T00:00:00Z","parameters":{"witness":{"threshold":1,"witnesses":[{"id":"z6Mkrv5Cm2XCLumMPTqooLTCw6YDf421d7VdTziwrZ8vNf4L"}]}},"state":{"@context":["https://www.w3.org/ns/did/v1","https://w3id.org/security/multikey/v1"],"assertionMethod":[],"authentication":["did:webvh:QmVArGEbmoYrzKp9wmpnCPYLjnvwHWhYC1jK8B8WCwgBd8:example.com#P5RDjVJG"],"capabilityDelegation":[],"capabilityInvocation":[],"controller":"did:webvh:QmVArGEbmoYrzKp9wmpnCPYLjnvwHWhYC1jK8B8WCwgBd8:example.com","id":"did:webvh:QmVArGEbmoYrzKp9wmpnCPYLjnvwHWhYC1jK8B8WCwgBd8:example.com","keyAgreement":[],"verificationMethod":[{"controller":"did:webvh:QmVArGEbmoYrzKp9wmpnCPYLjnvwHWhYC1jK8B8WCwgBd8:example.com","id":"did:webvh:QmVArGEbmoYrzKp9wmpnCPYLjnvwHWhYC1jK8B8WCwgBd8:example.com#P5RDjVJG","publicKeyMultibase":"z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG","type":"Multikey"}]},"proof":[{"type":"DataIntegrityProof","cryptosuite":"eddsa-jcs-2022","created":"2026-05-14T19:50:40Z","verificationMethod":"did:key:z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG#z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG","proofPurpose":"assertionMethod","proofValue":"z4DX91GNYFR2xfGpWcWKQR1piepfFhNPmhq1FvB4KBd8MeYeBJJeP1VyTkoteD7CHafActDHvo3k8H4DRwHkqjnri"}]} From 5e17e959b2cb6b3ef061de6e06a6b0cdd3624e1e Mon Sep 17 00:00:00 2001 From: Reza Maghoul Date: Wed, 27 May 2026 11:28:57 +0200 Subject: [PATCH 4/9] chore(ci): upgrade GitHub Actions to Node.js 24-compatible versions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit actions/checkout v4 → v5 actions/setup-java v4 → v5 codecov/codecov-action v4 → v5 softprops/action-gh-release v2 → v3 Node.js 20 is deprecated and will be removed from runners on September 16, 2026. Node.js 24 becomes the default June 2, 2026. --- .github/workflows/ci.yml | 6 +++--- .github/workflows/release.yml | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7a40e2e..0ddd43f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,10 +14,10 @@ jobs: java: [ '11', '17', '21', '25' ] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Set up JDK ${{ matrix.java }} - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: java-version: ${{ matrix.java }} distribution: temurin @@ -28,7 +28,7 @@ jobs: - name: Upload coverage to Codecov if: matrix.java == '25' - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v5 with: files: '**/target/site/jacoco/jacoco.xml' fail_ci_if_error: false diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5bda5ad..1fd16a5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,10 +11,10 @@ jobs: permissions: contents: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - name: Set up Java 11 - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: java-version: '11' distribution: temurin @@ -66,7 +66,7 @@ jobs: ./mvnw clean deploy -P release -B --no-transfer-progress -DskipTests - name: Create GitHub Release - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@v3 with: generate_release_notes: true files: | From b530c710f791d8c2bb32145c199acf62b47486c2 Mon Sep 17 00:00:00 2001 From: Reza Maghoul Date: Wed, 27 May 2026 11:38:34 +0200 Subject: [PATCH 5/9] chore(ci): force Node.js 24 in release workflow to silence deprecation warning Transitive dependency actions/github-script@v7 (pinned by softprops/action-gh-release@v3) still targets Node 20. Setting FORCE_JAVASCRIPT_ACTIONS_TO_NODE24=true opts in early ahead of the June 2, 2026 enforcement deadline. --- .github/workflows/release.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1fd16a5..6eae30b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,6 +5,9 @@ on: tags: - 'v*' +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + jobs: release: runs-on: ubuntu-latest From 86be99cffc794645a121c7592d48ccc39c79b303 Mon Sep 17 00:00:00 2001 From: Reza Maghoul Date: Wed, 27 May 2026 11:45:24 +0200 Subject: [PATCH 6/9] chore(ci): force Node.js 24 in CI workflow to silence deprecation warning --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0ddd43f..29411b6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,6 +6,9 @@ on: pull_request: branches: [ main ] +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + jobs: build: runs-on: ubuntu-latest From 7835839f3aef79d0e1c6412f7e86731faeab7dd3 Mon Sep 17 00:00:00 2001 From: Reza Maghoul Date: Thu, 28 May 2026 10:06:29 +0200 Subject: [PATCH 7/9] fix(core): close pre-rotation and portable-SCID negative-test gaps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two of three new negative scenarios in decentralized-identity/didwebvh-test-suite#4 (review comment 4558946830) were real bugs against this branch: * negative-pre-rotation-omit-updatekeys — when the previous entry committed nextKeyHashes, LogChainValidator only enforced the hash-match guard when the current entry actually set updateKeys. An entry that *omitted* the field inherited the old keys via Parameters.merge, and the old (possibly compromised) key kept signing — defeating pre-rotation entirely. Move the check into validateParameters and require entryDelta.updateKeys to be non-empty whenever prevActive.nextKeyHashes is non-empty. Surfaces as a parameter-validation failure (invalidParameters). * negative-portable-scid-swap — parameters.scid is the cryptographic anchor carried across the log; portability moves host/path only. LogChainValidator had no check that state.id's SCID segment matched parameters.scid after entry 0, so a portable migration could quietly swap the SCID while alsoKnownAs satisfied the host check. Add an explicit equality check against the third colon-delimited segment of state.id at every entry. Surfaces as invalidDid. The third scenario, negative-cross-did-witness-replay, is already correctly rejected by f13f750's pruned-proof filter: WitnessValidator.verifyProofs drops any witness proof whose versionId is not in this DID's published log, so a proof lifted from a sibling DID never counts toward the threshold. Added LogProcessorTest#crossDidWitnessReplayRejected to lock that in end-to-end (real DID-B → real signature → injected into DID-A's did-witness.json → resolver throws invalidDid), plus a WitnessValidatorTest unit covering the same shape. Refs: decentralized-identity/didwebvh-java#4 --- .../core/validate/LogChainValidator.java | 29 +++++++++- .../core/resolve/LogProcessorTest.java | 55 +++++++++++++++++++ .../core/validate/LogChainValidatorTest.java | 49 +++++++++++++++++ .../core/validate/WitnessValidatorTest.java | 33 +++++++++++ 4 files changed, 165 insertions(+), 1 deletion(-) diff --git a/didwebvh-core/src/main/java/io/github/decentralizedidentity/didwebvh/core/validate/LogChainValidator.java b/didwebvh-core/src/main/java/io/github/decentralizedidentity/didwebvh/core/validate/LogChainValidator.java index e3b956a..7305d9c 100644 --- a/didwebvh-core/src/main/java/io/github/decentralizedidentity/didwebvh/core/validate/LogChainValidator.java +++ b/didwebvh-core/src/main/java/io/github/decentralizedidentity/didwebvh/core/validate/LogChainValidator.java @@ -140,11 +140,27 @@ public ValidationResult validate(List entries, String expectedDid) { if (expectedDid == null || docId.equals(expectedDid)) { didFound = true; } + // Portability: parameters.scid is the cryptographic anchor and + // MUST NOT change across the log. The SCID component of + // state.id is the third colon-delimited segment of the DID URL + // (did:webvh::…). Portability moves host/path only; + // any entry whose state.id SCID differs from parameters.scid is + // rejected, regardless of portable / alsoKnownAs. + String activeScid = newActiveParams.getScid(); + if (activeScid != null) { + String[] parts = docId.split(":"); + if (parts.length >= 3 && !activeScid.equals(parts[2])) { + return ValidationResult.failure(i - 1, i, + "state.id SCID component does not match parameters.scid at entry " + + (i + 1), activeParams); + } + } } // 10. Pre-rotation: if previous entry set nextKeyHashes and this entry rotates keys, // the new updateKeys must hash-match the previously committed nextKeyHashes - // (spec §3.7.7, lines 1119-1123). + // (spec §3.7.7, lines 1119-1123). The updateKeys-required guard is enforced + // in validateParameters before we ever get here. if (preRotationActive && entryParams.getUpdateKeys() != null) { for (String key : entryParams.getUpdateKeys()) { String hash = PreRotationHashGenerator.generateHash(key); @@ -218,6 +234,17 @@ && compareMethod(entryDelta.getMethod(), prevActive.getMethod()) < 0) { && !Boolean.TRUE.equals(prevActive.getPortable())) { return "portable can only be set to true in first entry"; } + // Pre-rotation: if the previous entry committed nextKeyHashes, + // this entry MUST present a non-empty updateKeys. Otherwise the + // pre-rotation guarantee is defeated by inheriting the old keys. + // (Spec §3.7.7; see negative-pre-rotation-omit-updatekeys.) + List prevNextHashes = prevActive.getNextKeyHashes(); + if (prevNextHashes != null && !prevNextHashes.isEmpty() + && (entryDelta.getUpdateKeys() == null + || entryDelta.getUpdateKeys().isEmpty())) { + return "updateKeys MUST be present and non-empty when previous entry " + + "committed nextKeyHashes (pre-rotation active)"; + } } // Witness config bounds check – only applies when witnesses are actually configured diff --git a/didwebvh-core/src/test/java/io/github/decentralizedidentity/didwebvh/core/resolve/LogProcessorTest.java b/didwebvh-core/src/test/java/io/github/decentralizedidentity/didwebvh/core/resolve/LogProcessorTest.java index cb05f47..eda629e 100644 --- a/didwebvh-core/src/test/java/io/github/decentralizedidentity/didwebvh/core/resolve/LogProcessorTest.java +++ b/didwebvh-core/src/test/java/io/github/decentralizedidentity/didwebvh/core/resolve/LogProcessorTest.java @@ -189,6 +189,61 @@ void missingWitnessProofsThrowInvalidDid() { .hasMessageContaining("Witness proofs"); } + @Test + void crossDidWitnessReplayRejected() { + // negative-cross-did-witness-replay (didwebvh-test-suite): an attacker + // controls DID-A's hosting and lifts a genuine witness proof from + // DID-B (which shares a witness W with DID-A) into DID-A's + // did-witness.json. DID-A's later entries set witness:{} to disable + // witnessing so no DID-A entry will direct-verify against an + // entry-hash from DID-A's log. The resolver MUST reject the replayed + // proof because its versionId is not in DID-A's published log + // (spec §3.7.8). Expected error: invalidDid. + Signer authorA = makeTestSigner(); + Signer authorB = makeTestSigner(); + Signer sharedWitness = makeTestSigner(); + String sharedWitnessDid = "did:key:" + extractMultikey( + sharedWitness.verificationMethod()); + WitnessConfig sharedConfig = new WitnessConfig(1, + Collections.singletonList(new WitnessEntry(sharedWitnessDid))); + + // Build DID-B's 3rd entry to obtain a real versionId the witness + // genuinely signed over. We only need DID-B's versionId — we never + // resolve DID-B. + CreateDidResult createB = DidWebVh.create("other.example.com", authorB) + .witness(sharedConfig) + .execute(); + LogEntry b1 = createB.getLogEntry(); + LogEntry b2 = buildUpdateEntry(b1, authorB, null, null); + LogEntry b3 = buildUpdateEntry(b2, authorB, null, null); + WitnessProofCollection didBProofs = witnessProofs( + b3.getVersionId(), sharedWitness); + + // Build DID-A: entry 1 has witnessing on, entry 2 disables it, + // entry 3 inherits the disabled config. + CreateDidResult createA = DidWebVh.create("example.com", authorA) + .witness(sharedConfig) + .execute(); + LogEntry a1 = createA.getLogEntry(); + Parameters witnessOff = new Parameters().setWitness(WitnessConfig.empty()); + LogEntry a2 = buildUpdateEntry(a1, authorA, witnessOff, null); + LogEntry a3 = buildUpdateEntry(a2, authorA, null, null); + + String didALog = a1.toJsonLine() + "\n" + + a2.toJsonLine() + "\n" + + a3.toJsonLine() + "\n"; + + // did-witness.json for DID-A contains DID-B's proof verbatim — a real + // signature, but the versionId is from DID-B's log, not DID-A's. + String didAWitnessJson = JsonSupport.compact().toJson(didBProofs.getEntries()); + + assertThatThrownBy(() -> processor.process(didALog, didAWitnessJson, + createA.getDid(), ResolveOptions.defaults())) + .isInstanceOf(ResolutionException.class) + .satisfies(t -> assertThat(((ResolutionException) t).getError()) + .isEqualTo("invalidDid")); + } + @Test void problemDetailsUseDidwebvhProblemType() { ResolutionException exception = new ResolutionException( diff --git a/didwebvh-core/src/test/java/io/github/decentralizedidentity/didwebvh/core/validate/LogChainValidatorTest.java b/didwebvh-core/src/test/java/io/github/decentralizedidentity/didwebvh/core/validate/LogChainValidatorTest.java index 282bbd3..a5cdb52 100644 --- a/didwebvh-core/src/test/java/io/github/decentralizedidentity/didwebvh/core/validate/LogChainValidatorTest.java +++ b/didwebvh-core/src/test/java/io/github/decentralizedidentity/didwebvh/core/validate/LogChainValidatorTest.java @@ -509,6 +509,55 @@ static DataIntegrityProof signDocument(Signer s, JsonObject doc) { return proof.setProofValue(Base58Btc.encodeMultibase(signature)); } + @Test + void preRotationOmitUpdateKeysFails() { + // Spec §3.7.7 (and negative-pre-rotation-omit-updatekeys): when the + // previous entry committed nextKeyHashes, the next entry MUST present + // updateKeys. Omitting it must not let the old key keep signing. + Signer signer2 = makeTestSigner(); + String hash2 = PreRotationHashGenerator.generateHash( + extractMultikey(signer2.verificationMethod())); + + CreateDidResult create = DidWebVh.create("example.com", signer) + .nextKeyHashes(Collections.singletonList(hash2)) + .execute(); + LogEntry e1 = create.getLogEntry(); + + // Entry 2 omits updateKeys entirely AND is signed by the old key. + LogEntry e2 = buildUpdateEntry(e1, create.getDid(), signer, null, null); + + ValidationResult vr = validator.validate(Arrays.asList(e1, e2), create.getDid()); + + assertThat(vr.isValid()).isFalse(); + assertThat(vr.getFailureReason()).containsIgnoringCase("updateKeys"); + assertThat(vr.getFailureReason()).containsIgnoringCase("pre-rotation"); + } + + @Test + void portableScidSwapFails() { + // Spec §portability (and negative-portable-scid-swap): SCID component + // of state.id MUST equal parameters.scid; portability changes + // host/path only. + CreateDidResult create = DidWebVh.create("example.com", signer) + .portable(true) + .execute(); + LogEntry e1 = create.getLogEntry(); + + // Build a second entry whose state.id has a different SCID segment. + JsonObject state2 = e1.getState().deepCopy(); + String originalId = state2.get("id").getAsString(); + String[] parts = originalId.split(":"); + parts[2] = "QmAttackerChosenSCID000000000000000000000000000"; + state2.addProperty("id", String.join(":", parts)); + + LogEntry e2 = buildUpdateEntry(e1, create.getDid(), signer, null, state2); + + ValidationResult vr = validator.validate(Arrays.asList(e1, e2), create.getDid()); + + assertThat(vr.isValid()).isFalse(); + assertThat(vr.getFailureReason()).containsIgnoringCase("SCID"); + } + @Test void noProofFails() { CreateDidResult create = DidWebVh.create("example.com", signer).execute(); diff --git a/didwebvh-core/src/test/java/io/github/decentralizedidentity/didwebvh/core/validate/WitnessValidatorTest.java b/didwebvh-core/src/test/java/io/github/decentralizedidentity/didwebvh/core/validate/WitnessValidatorTest.java index b82427f..438106c 100644 --- a/didwebvh-core/src/test/java/io/github/decentralizedidentity/didwebvh/core/validate/WitnessValidatorTest.java +++ b/didwebvh-core/src/test/java/io/github/decentralizedidentity/didwebvh/core/validate/WitnessValidatorTest.java @@ -175,6 +175,39 @@ void noWitnessConfigSkipsValidation() { assertThat(result.isValid()).isTrue(); } + @Test + void crossDidWitnessReplayRejected() { + // negative-cross-did-witness-replay: DID-A's first entry needs a + // witness proof. Attacker injects DID-B's genuine proof for a + // versionId from DID-B's log; later DID-A entries turn witnessing + // off so no later proof carries the threshold for entry 1. The + // resolver MUST ignore proofs whose versionId is not in DID-A's + // published log (spec §3.7.8) — leaving the threshold unmet. + WitnessConfig wc = new WitnessConfig(1, + Collections.singletonList(new WitnessEntry(witnessDid1))); + CreateDidResult create = DidWebVh.create("example.com", authorSigner) + .witness(wc) + .execute(); + LogEntry e1 = create.getLogEntry(); + + // Entry 2 disables witnessing for the remainder of the log. + Parameters witnessOff = new Parameters().setWitness(WitnessConfig.empty()); + LogEntry e2 = LogChainValidatorTest.buildUpdateEntry( + e1, create.getDid(), authorSigner, witnessOff, null); + + // The "lifted" proof is a real signature by the shared witness over + // a versionId from another DID's log — i.e., a versionId NOT present + // in this DID's log. + WitnessProofCollection foreignProofs = makeWitnessProofs( + "3-zForeignDidEntryHashThatDoesNotExistInThisLog", witnessSigner1); + + WitnessValidationResult result = validator.validate( + Arrays.asList(e1, e2), foreignProofs, 0); + + assertThat(result.isValid()).isFalse(); + assertThat(result.getFailureReason()).contains("insufficient witness proofs"); + } + @Test void fromEntryIndexRespected() { // Two entries; only second requires witnessing From a2a7470761d56845763d7991bad1fdd371edcbe4 Mon Sep 17 00:00:00 2001 From: Reza Maghoul Date: Thu, 28 May 2026 19:22:34 +0200 Subject: [PATCH 8/9] fix(core): reject cross-DID witness replay and emit implicit services MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two interop fixes flagged on PR #4 review: 1. `negative-cross-did-witness-replay` now resolves to `invalidDid`. Spec §3.7.5 (lines 884-889): when the `witness` parameter is set to `{}` while witnesses were active, the transition entry MUST itself be witnessed by the prior witnesses. `WitnessValidator` was merging the empty config in first and skipping the entry as inactive, which let an attacker disable witnessing and replay a stale (or cross-DID) proof for an earlier entry. Now the validator tracks the prior config and, on a witness-off transition, requires approval from the prior witnesses. 2. Resolved did:webvh DID Documents now include the implicit `#files` (relativeRef) and `#whois` (LinkedVerifiablePresentation) services required by spec §3.8 and §3.9, unless the controller has already declared services with the same id. The implicit-service logic was previously only applied when generating the parallel did:web document; extracted it into `ImplicitServices` and call it from `LogProcessor` so every resolution carries the services. `DidWebPublisher` now delegates to the same helper. Vendored the upstream `negative-cross-did-witness-replay/ts` fixture from swcurran/didwebvh-test-suite@c11fda3 and added JUnit coverage for both fixes. --- .../didwebvh/core/didweb/DidWebPublisher.java | 76 +------------ .../core/didweb/ImplicitServices.java | 103 ++++++++++++++++++ .../didwebvh/core/resolve/LogProcessor.java | 9 +- .../core/validate/WitnessValidator.java | 25 ++++- ...eropNegativeCrossDidWitnessReplayTest.java | 80 ++++++++++++++ .../core/resolve/LogProcessorTest.java | 26 +++++ .../src/test/resources/interop/README.md | 1 + .../did-witness.json | 15 +++ .../did.jsonl | 2 + .../resolutionResult.json | 7 ++ 10 files changed, 265 insertions(+), 79 deletions(-) create mode 100644 didwebvh-core/src/main/java/io/github/decentralizedidentity/didwebvh/core/didweb/ImplicitServices.java create mode 100644 didwebvh-core/src/test/java/io/github/decentralizedidentity/didwebvh/core/interop/InteropNegativeCrossDidWitnessReplayTest.java create mode 100644 didwebvh-core/src/test/resources/interop/negative-cross-did-witness-replay-ts/did-witness.json create mode 100644 didwebvh-core/src/test/resources/interop/negative-cross-did-witness-replay-ts/did.jsonl create mode 100644 didwebvh-core/src/test/resources/interop/negative-cross-did-witness-replay-ts/resolutionResult.json diff --git a/didwebvh-core/src/main/java/io/github/decentralizedidentity/didwebvh/core/didweb/DidWebPublisher.java b/didwebvh-core/src/main/java/io/github/decentralizedidentity/didwebvh/core/didweb/DidWebPublisher.java index 6caf0a6..d6265e5 100644 --- a/didwebvh-core/src/main/java/io/github/decentralizedidentity/didwebvh/core/didweb/DidWebPublisher.java +++ b/didwebvh-core/src/main/java/io/github/decentralizedidentity/didwebvh/core/didweb/DidWebPublisher.java @@ -18,12 +18,6 @@ */ public final class DidWebPublisher { - private static final String FILES_ID = "#files"; - private static final String WHOIS_ID = "#whois"; - private static final String WELL_KNOWN_SEGMENT = "/.well-known/"; - private static final String DID_LOG_FILENAME = "did.jsonl"; - private static final String WHOIS_FILENAME = "whois.vp"; - private DidWebPublisher() { } @@ -46,13 +40,12 @@ public static DidDocument toDidWeb(DidDocument resolvedWebVh) { DidWebVhUrl parsed = DidWebVhUrl.parse(didWebVh); String scidPrefix = "did:webvh:" + parsed.getScid() + ":"; - String httpsBase = httpsBase(didWebVh); // Step 1: start from the resolved did:webvh DIDDoc (deep copy to avoid mutation). JsonObject doc = resolvedWebVh.asJsonObject().deepCopy(); // Step 2: add implicit #files and #whois services if not already present. - addImplicitServices(doc, didWebVh, httpsBase); + ImplicitServices.addTo(doc, didWebVh); // Step 3: text-replace did:webvh:: with did:web: across the whole document. String replaced = doc.toString().replace(scidPrefix, "did:web:"); @@ -74,73 +67,6 @@ public static String toDidWebUrl(String didWebVhUrl) { return DidToHttpsTransformer.toDidWebUrl(didWebVhUrl); } - /** - * Compute the HTTPS base URL for implicit service endpoints: the DID-to-HTTPS - * URL with the trailing {@code did.jsonl} filename removed and any - * {@code .well-known/} segment stripped. - */ - private static String httpsBase(String didWebVh) { - String didLogUrl = DidToHttpsTransformer.toHttpsUrl(didWebVh); - // Strip the trailing did.jsonl filename. - String base = didLogUrl.substring(0, - didLogUrl.length() - DID_LOG_FILENAME.length()); - // Omit the ".well-known/" segment if present. - int idx = base.indexOf(WELL_KNOWN_SEGMENT); - if (idx >= 0) { - base = base.substring(0, idx + 1) + base.substring(idx + WELL_KNOWN_SEGMENT.length()); - } - return base; - } - - private static void addImplicitServices(JsonObject doc, String didWebVh, - String httpsBase) { - JsonArray services; - JsonElement existing = doc.get("service"); - if (existing != null && existing.isJsonArray()) { - services = existing.getAsJsonArray(); - } else { - services = new JsonArray(); - doc.add("service", services); - } - - if (!hasServiceWithId(services, didWebVh, FILES_ID)) { - JsonObject files = new JsonObject(); - files.addProperty("id", didWebVh + FILES_ID); - files.addProperty("type", "relativeRef"); - files.addProperty("serviceEndpoint", httpsBase); - services.add(files); - } - - if (!hasServiceWithId(services, didWebVh, WHOIS_ID)) { - JsonObject whois = new JsonObject(); - whois.addProperty("@context", - "https://identity.foundation/linked-vp/contexts/v1"); - whois.addProperty("id", didWebVh + WHOIS_ID); - whois.addProperty("type", "LinkedVerifiablePresentation"); - whois.addProperty("serviceEndpoint", httpsBase + WHOIS_FILENAME); - services.add(whois); - } - } - - private static boolean hasServiceWithId(JsonArray services, String did, - String fragment) { - String absolute = did + fragment; - for (JsonElement el : services) { - if (!el.isJsonObject()) { - continue; - } - JsonElement idEl = el.getAsJsonObject().get("id"); - if (idEl == null || idEl.isJsonNull()) { - continue; - } - String id = idEl.getAsString(); - if (fragment.equals(id) || absolute.equals(id)) { - return true; - } - } - return false; - } - private static void addAlsoKnownAs(JsonObject doc, String didWebVh, String didWeb) { Set seen = new LinkedHashSet<>(); diff --git a/didwebvh-core/src/main/java/io/github/decentralizedidentity/didwebvh/core/didweb/ImplicitServices.java b/didwebvh-core/src/main/java/io/github/decentralizedidentity/didwebvh/core/didweb/ImplicitServices.java new file mode 100644 index 0000000..d8bce6d --- /dev/null +++ b/didwebvh-core/src/main/java/io/github/decentralizedidentity/didwebvh/core/didweb/ImplicitServices.java @@ -0,0 +1,103 @@ +package io.github.decentralizedidentity.didwebvh.core.didweb; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import io.github.decentralizedidentity.didwebvh.core.url.DidToHttpsTransformer; + +/** + * Adds the implicit {@code #files} and {@code #whois} services defined for + * {@code did:webvh} by spec §3.8 and §3.9 to a DID Document. + * + *

These services are defined by the method itself and MUST be present in + * the resolved DID Document unless the controller has overridden them by + * declaring services with the same id (or fragment) explicitly. + */ +public final class ImplicitServices { + + private static final String FILES_ID = "#files"; + private static final String WHOIS_ID = "#whois"; + private static final String WELL_KNOWN_SEGMENT = "/.well-known/"; + private static final String DID_LOG_FILENAME = "did.jsonl"; + private static final String WHOIS_FILENAME = "whois.vp"; + + private ImplicitServices() { + } + + /** + * Mutate {@code doc} in place, appending the implicit {@code #files} and + * {@code #whois} services if not already present (compared by service + * fragment, either as bare fragment or fully qualified id). + * + * @param doc the DID Document JSON object (mutated in place) + * @param did the DID string used to construct fully-qualified service ids + */ + public static void addTo(JsonObject doc, String did) { + if (doc == null || did == null || did.isEmpty()) { + return; + } + String httpsBase = httpsBase(did); + + JsonArray services; + JsonElement existing = doc.get("service"); + if (existing != null && existing.isJsonArray()) { + services = existing.getAsJsonArray(); + } else { + services = new JsonArray(); + doc.add("service", services); + } + + if (!hasServiceWithId(services, did, FILES_ID)) { + JsonObject files = new JsonObject(); + files.addProperty("id", did + FILES_ID); + files.addProperty("type", "relativeRef"); + files.addProperty("serviceEndpoint", httpsBase); + services.add(files); + } + + if (!hasServiceWithId(services, did, WHOIS_ID)) { + JsonObject whois = new JsonObject(); + whois.addProperty("@context", + "https://identity.foundation/linked-vp/contexts/v1"); + whois.addProperty("id", did + WHOIS_ID); + whois.addProperty("type", "LinkedVerifiablePresentation"); + whois.addProperty("serviceEndpoint", httpsBase + WHOIS_FILENAME); + services.add(whois); + } + } + + /** + * Compute the HTTPS base URL for implicit service endpoints: the + * DID-to-HTTPS URL with the trailing {@code did.jsonl} filename removed + * and any {@code .well-known/} segment stripped. + */ + static String httpsBase(String did) { + String didLogUrl = DidToHttpsTransformer.toHttpsUrl(did); + String base = didLogUrl.substring(0, + didLogUrl.length() - DID_LOG_FILENAME.length()); + int idx = base.indexOf(WELL_KNOWN_SEGMENT); + if (idx >= 0) { + base = base.substring(0, idx + 1) + base.substring(idx + WELL_KNOWN_SEGMENT.length()); + } + return base; + } + + private static boolean hasServiceWithId(JsonArray services, String did, + String fragment) { + String absolute = did + fragment; + for (JsonElement el : services) { + if (!el.isJsonObject()) { + continue; + } + JsonElement idEl = el.getAsJsonObject().get("id"); + if (idEl == null || idEl.isJsonNull()) { + continue; + } + String id = idEl.getAsString(); + if (fragment.equals(id) || absolute.equals(id)) { + return true; + } + } + return false; + } +} diff --git a/didwebvh-core/src/main/java/io/github/decentralizedidentity/didwebvh/core/resolve/LogProcessor.java b/didwebvh-core/src/main/java/io/github/decentralizedidentity/didwebvh/core/resolve/LogProcessor.java index 8e2fa1d..62e7662 100644 --- a/didwebvh-core/src/main/java/io/github/decentralizedidentity/didwebvh/core/resolve/LogProcessor.java +++ b/didwebvh-core/src/main/java/io/github/decentralizedidentity/didwebvh/core/resolve/LogProcessor.java @@ -1,8 +1,10 @@ package io.github.decentralizedidentity.didwebvh.core.resolve; +import com.google.gson.JsonObject; import com.google.gson.JsonSyntaxException; import io.github.decentralizedidentity.didwebvh.core.ResolutionException; import io.github.decentralizedidentity.didwebvh.core.ValidationException; +import io.github.decentralizedidentity.didwebvh.core.didweb.ImplicitServices; import io.github.decentralizedidentity.didwebvh.core.model.DidDocument; import io.github.decentralizedidentity.didwebvh.core.model.JsonSupport; import io.github.decentralizedidentity.didwebvh.core.model.LogEntry; @@ -66,7 +68,12 @@ ResolveResult process(String didLogContent, String witnessContent, ResolveResult result = new ResolveResult().setMetadata(metadata); if (!Boolean.TRUE.equals(selectedParameters.getDeactivated())) { - result.setDidDocument(new DidDocument(selected.getState().deepCopy())); + // Spec §3.8 and §3.9: the resolved DID Document MUST include the + // implicit #files and #whois services unless the controller has + // already defined explicit services with the same id. + JsonObject state = selected.getState().deepCopy(); + ImplicitServices.addTo(state, selected.getState().get("id").getAsString()); + result.setDidDocument(new DidDocument(state)); } return result; } diff --git a/didwebvh-core/src/main/java/io/github/decentralizedidentity/didwebvh/core/validate/WitnessValidator.java b/didwebvh-core/src/main/java/io/github/decentralizedidentity/didwebvh/core/validate/WitnessValidator.java index 1949ac7..b460712 100644 --- a/didwebvh-core/src/main/java/io/github/decentralizedidentity/didwebvh/core/validate/WitnessValidator.java +++ b/didwebvh-core/src/main/java/io/github/decentralizedidentity/didwebvh/core/validate/WitnessValidator.java @@ -62,10 +62,29 @@ public WitnessValidationResult validate(List entries, LogEntry entry = entries.get(i); Parameters entryParams = entry.getParameters() != null ? entry.getParameters() : new Parameters(); + WitnessConfig priorConfig = activeParams.getWitness(); activeParams = activeParams.merge(entryParams); - - WitnessConfig witnessConfig = activeParams.getWitness(); - if (witnessConfig == null || !witnessConfig.isActive()) { + WitnessConfig afterConfig = activeParams.getWitness(); + + // Spec §3.7.5 (lines 884-889): when the witness parameter is set to + // {} while witnesses were previously active, that log entry MUST be + // witnessed by the prior witnesses. Otherwise an attacker could + // legitimately disable witnessing in a forged entry and slip the + // change past the resolver. Skip only when no witnessing was + // active either side of this entry. + boolean priorActive = priorConfig != null && priorConfig.isActive(); + boolean afterActive = afterConfig != null && afterConfig.isActive(); + boolean witnessChanged = entryParams.getWitness() != null; + WitnessConfig witnessConfig; + if (afterActive) { + // Per spec, "from {} to active" means the new config is + // immediately active and approves this entry. + witnessConfig = afterConfig; + } else if (priorActive && witnessChanged) { + // Transition from active to {}: the prior witnesses must + // approve this transition. + witnessConfig = priorConfig; + } else { continue; } diff --git a/didwebvh-core/src/test/java/io/github/decentralizedidentity/didwebvh/core/interop/InteropNegativeCrossDidWitnessReplayTest.java b/didwebvh-core/src/test/java/io/github/decentralizedidentity/didwebvh/core/interop/InteropNegativeCrossDidWitnessReplayTest.java new file mode 100644 index 0000000..36a2294 --- /dev/null +++ b/didwebvh-core/src/test/java/io/github/decentralizedidentity/didwebvh/core/interop/InteropNegativeCrossDidWitnessReplayTest.java @@ -0,0 +1,80 @@ +package io.github.decentralizedidentity.didwebvh.core.interop; + +import io.github.decentralizedidentity.didwebvh.core.model.JsonSupport; +import io.github.decentralizedidentity.didwebvh.core.model.LogEntry; +import io.github.decentralizedidentity.didwebvh.core.validate.WitnessValidationResult; +import io.github.decentralizedidentity.didwebvh.core.validate.WitnessValidator; +import io.github.decentralizedidentity.didwebvh.core.witness.WitnessProofCollection; +import io.github.decentralizedidentity.didwebvh.core.witness.WitnessProofEntry; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Interop regression: a witness proof from DID-B replayed verbatim into DID-A's + * {@code did-witness.json} must be rejected. The witness signing payload + * ({@code {"versionId": ""}}) carries no DID binding, so resolvers must + * filter proofs to those whose versionId is present in this log + * before counting them toward the threshold (spec §3.7.8). When the proof's + * versionId is not in this log, the entry's witness threshold cannot be met + * and resolution fails with {@code invalidDid}. + * + *

Source: swcurran/didwebvh-test-suite, + * vectors/negative-cross-did-witness-replay/ts/. + */ +class InteropNegativeCrossDidWitnessReplayTest { + + @Test + void replayedCrossDidWitnessProofIsRejected() throws IOException { + List entries = parseJsonl(readVector( + "/interop/negative-cross-did-witness-replay-ts/did.jsonl")); + WitnessProofCollection proofs = parseProofs(readVector( + "/interop/negative-cross-did-witness-replay-ts/did-witness.json")); + + WitnessValidationResult result = new WitnessValidator().validate(entries, proofs, 0); + + assertThat(result.isValid()) + .as("replayed cross-DID witness proof must be rejected; got: %s", + result.getFailureReason()) + .isFalse(); + } + + private static WitnessProofCollection parseProofs(String json) { + WitnessProofEntry[] arr = JsonSupport.compact().fromJson(json, WitnessProofEntry[].class); + return new WitnessProofCollection(Arrays.asList(arr)); + } + + private static List parseJsonl(String jsonl) { + List out = new ArrayList<>(); + for (String line : jsonl.split("\n")) { + String trimmed = line.trim(); + if (!trimmed.isEmpty()) { + out.add(LogEntry.fromJsonLine(trimmed)); + } + } + return out; + } + + private static String readVector(String resourcePath) throws IOException { + try (InputStream in = InteropNegativeCrossDidWitnessReplayTest.class + .getResourceAsStream(resourcePath)) { + if (in == null) { + throw new IllegalStateException("Missing test resource: " + resourcePath); + } + java.io.ByteArrayOutputStream out = new java.io.ByteArrayOutputStream(); + byte[] buf = new byte[4096]; + int read; + while ((read = in.read(buf)) > 0) { + out.write(buf, 0, read); + } + return new String(out.toByteArray(), StandardCharsets.UTF_8); + } + } +} diff --git a/didwebvh-core/src/test/java/io/github/decentralizedidentity/didwebvh/core/resolve/LogProcessorTest.java b/didwebvh-core/src/test/java/io/github/decentralizedidentity/didwebvh/core/resolve/LogProcessorTest.java index eda629e..5a4686f 100644 --- a/didwebvh-core/src/test/java/io/github/decentralizedidentity/didwebvh/core/resolve/LogProcessorTest.java +++ b/didwebvh-core/src/test/java/io/github/decentralizedidentity/didwebvh/core/resolve/LogProcessorTest.java @@ -28,6 +28,32 @@ class LogProcessorTest { private final LogProcessor processor = new LogProcessor(); + @Test + void resolvedDocumentIncludesImplicitFilesAndWhoisServices() { + Signer signer = makeTestSigner(); + CreateDidResult create = DidWebVh.create("example.com", signer).execute(); + + ResolveResult result = processor.process(create.getLogLine(), null, + create.getDid(), ResolveOptions.defaults()); + + com.google.gson.JsonArray services = result.getDidDocument().asJsonObject() + .getAsJsonArray("service"); + assertThat(services).isNotNull(); + assertThat(services).hasSize(2); + + JsonObject files = services.get(0).getAsJsonObject(); + assertThat(files.get("id").getAsString()).isEqualTo(create.getDid() + "#files"); + assertThat(files.get("type").getAsString()).isEqualTo("relativeRef"); + assertThat(files.get("serviceEndpoint").getAsString()) + .isEqualTo("https://example.com/"); + + JsonObject whois = services.get(1).getAsJsonObject(); + assertThat(whois.get("id").getAsString()).isEqualTo(create.getDid() + "#whois"); + assertThat(whois.get("type").getAsString()).isEqualTo("LinkedVerifiablePresentation"); + assertThat(whois.get("serviceEndpoint").getAsString()) + .isEqualTo("https://example.com/whois.vp"); + } + @Test void resolvesLatestEntryWithMetadata() { Signer signer = makeTestSigner(); diff --git a/didwebvh-core/src/test/resources/interop/README.md b/didwebvh-core/src/test/resources/interop/README.md index c0c0a12..e2f606b 100644 --- a/didwebvh-core/src/test/resources/interop/README.md +++ b/didwebvh-core/src/test/resources/interop/README.md @@ -23,6 +23,7 @@ verbatim from the upstream `vectors/` tree and exercised by JUnit tests under | `pre-rotation-consume-java-eecc/`| `vectors/pre-rotation-consume/java-eecc/`| Pre-rotation: each entry signed by its own (committed) updateKeys | | `witness-update-rust/` | `vectors/witness-update/rust/` | Witness proof pruning: single latest proof covers all prior entries | | `witness-threshold-rust/` | `vectors/witness-threshold/rust/` | Witness id as bare multikey (without `did:key:` prefix) | +| `negative-cross-did-witness-replay-ts/` | `vectors/negative-cross-did-witness-replay/ts/` | Replayed cross-DID witness proof rejected: spec §3.7.5 (lines 884-889) requires the entry that disables witnessing to itself be witnessed by the prior witnesses | To refresh: bump the SHA above, re-download the listed files (`curl -sSL https://raw.githubusercontent.com/swcurran/didwebvh-test-suite//`), diff --git a/didwebvh-core/src/test/resources/interop/negative-cross-did-witness-replay-ts/did-witness.json b/didwebvh-core/src/test/resources/interop/negative-cross-did-witness-replay-ts/did-witness.json new file mode 100644 index 0000000..e357337 --- /dev/null +++ b/didwebvh-core/src/test/resources/interop/negative-cross-did-witness-replay-ts/did-witness.json @@ -0,0 +1,15 @@ +[ + { + "versionId": "1-QmW1kazgpSeCNX4kZghibxLU2ye8nr6dqADhQiTz3qPD1C", + "proof": [ + { + "type": "DataIntegrityProof", + "cryptosuite": "eddsa-jcs-2022", + "verificationMethod": "did:key:z6Mkrv5Cm2XCLumMPTqooLTCw6YDf421d7VdTziwrZ8vNf4L#z6Mkrv5Cm2XCLumMPTqooLTCw6YDf421d7VdTziwrZ8vNf4L", + "created": "2000-01-01T00:00:00Z", + "proofPurpose": "assertionMethod", + "proofValue": "zTsLwZHBByHgNnHKaFuUumacoruk5mW4KCgwL7hNdt9W1WcyyP3LoqJ7FA3YgdCZ2C7gb1CLQ75oiYvMbyjsTFjP" + } + ] + } +] diff --git a/didwebvh-core/src/test/resources/interop/negative-cross-did-witness-replay-ts/did.jsonl b/didwebvh-core/src/test/resources/interop/negative-cross-did-witness-replay-ts/did.jsonl new file mode 100644 index 0000000..85e8c22 --- /dev/null +++ b/didwebvh-core/src/test/resources/interop/negative-cross-did-witness-replay-ts/did.jsonl @@ -0,0 +1,2 @@ +{"versionId":"1-QmW1kazgpSeCNX4kZghibxLU2ye8nr6dqADhQiTz3qPD1C","versionTime":"2000-01-01T00:00:00Z","parameters":{"method":"did:webvh:1.0","scid":"QmaaKkr6nu7uSTpjSfAr3r7xBezNZGpWu6Gwtgqr6A4ynC","updateKeys":["z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG"],"portable":false,"nextKeyHashes":[],"watchers":[],"witness":{"threshold":1,"witnesses":[{"id":"did:key:z6Mkrv5Cm2XCLumMPTqooLTCw6YDf421d7VdTziwrZ8vNf4L"}]},"deactivated":false},"state":{"@context":["https://www.w3.org/ns/did/v1","https://w3id.org/security/multikey/v1"],"id":"did:webvh:QmaaKkr6nu7uSTpjSfAr3r7xBezNZGpWu6Gwtgqr6A4ynC:example.com","controller":"did:webvh:QmaaKkr6nu7uSTpjSfAr3r7xBezNZGpWu6Gwtgqr6A4ynC:example.com","verificationMethod":[{"type":"Multikey","publicKeyMultibase":"z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG","purpose":"authentication","id":"did:webvh:QmaaKkr6nu7uSTpjSfAr3r7xBezNZGpWu6Gwtgqr6A4ynC:example.com#P5RDjVJG"}],"authentication":["did:webvh:QmaaKkr6nu7uSTpjSfAr3r7xBezNZGpWu6Gwtgqr6A4ynC:example.com#P5RDjVJG"],"assertionMethod":[],"keyAgreement":[],"capabilityDelegation":[],"capabilityInvocation":[]},"proof":[{"type":"DataIntegrityProof","cryptosuite":"eddsa-jcs-2022","verificationMethod":"did:key:z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG#z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG","created":"2000-01-01T00:00:00Z","proofPurpose":"assertionMethod","proofValue":"z3QhC4dk9WzF4xgmDguyKMUueki4NyQJWPmxfd972mM7g4jhkzerX8x2gnz4dxQCUddia7SgLh16iwy949b3Xmhr"}]} +{"versionId":"2-QmQAJ4FG6Py6JJW1aioswVK5a2f3ziopnFAaRwj8WHmH7u","versionTime":"2000-01-02T00:00:00Z","parameters":{"updateKeys":["z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG"],"nextKeyHashes":[],"witness":{},"watchers":[]},"state":{"@context":["https://www.w3.org/ns/did/v1","https://w3id.org/security/multikey/v1"],"id":"did:webvh:QmaaKkr6nu7uSTpjSfAr3r7xBezNZGpWu6Gwtgqr6A4ynC:example.com","controller":"did:webvh:QmaaKkr6nu7uSTpjSfAr3r7xBezNZGpWu6Gwtgqr6A4ynC:example.com","verificationMethod":[{"type":"Multikey","publicKeyMultibase":"z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG","purpose":"authentication","id":"did:webvh:QmaaKkr6nu7uSTpjSfAr3r7xBezNZGpWu6Gwtgqr6A4ynC:example.com#P5RDjVJG"}],"authentication":["did:webvh:QmaaKkr6nu7uSTpjSfAr3r7xBezNZGpWu6Gwtgqr6A4ynC:example.com#P5RDjVJG"],"assertionMethod":[],"keyAgreement":[],"capabilityDelegation":[],"capabilityInvocation":[]},"proof":[{"type":"DataIntegrityProof","cryptosuite":"eddsa-jcs-2022","verificationMethod":"did:key:z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG#z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG","created":"2000-01-02T00:00:00Z","proofPurpose":"assertionMethod","proofValue":"zDQZamyCJc1uLhWfqGNwXwCTEKcU3ubc8v4UHCKGKUTy2wrMky2kNBq5Ain8VHYiuqV2eGYxrpPdrUTTKh5dND6g"}]} diff --git a/didwebvh-core/src/test/resources/interop/negative-cross-did-witness-replay-ts/resolutionResult.json b/didwebvh-core/src/test/resources/interop/negative-cross-did-witness-replay-ts/resolutionResult.json new file mode 100644 index 0000000..5cbdf9c --- /dev/null +++ b/didwebvh-core/src/test/resources/interop/negative-cross-did-witness-replay-ts/resolutionResult.json @@ -0,0 +1,7 @@ +{ + "didDocument": null, + "didDocumentMetadata": {}, + "didResolutionMetadata": { + "error": "invalidDid" + } +} From 8d08eca1ef82d92e55b4fde312d539b7cfacfdf4 Mon Sep 17 00:00:00 2001 From: Reza Maghoul Date: Thu, 28 May 2026 19:31:21 +0200 Subject: [PATCH 9/9] chore(release): prepare v0.3.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bump parent + module poms from 0.2.0-SNAPSHOT to 0.3.0-SNAPSHOT and update README Maven/Gradle coordinates to 0.3.0 (also corrects the groupId there from `io.github.decentralizedidentity` to the actual `io.github.decentralized-identity`). Reshape the CHANGELOG so current main maps to a single coherent v0.2.0 entry: fold the previous standalone 0.2.1 namespace-migration entry into 0.2.0 as a Changed bullet and re-date 0.2.0 to 2026-05-06. Add a full 0.3.0 entry (2026-05-28) covering this branch's work: - Cross-DID witness-proof replay rejected (spec §3.7.5 lines 884-889): entries that disable witnessing while witnesses were active now require approval from the prior witnesses. - Implicit `#files` and `#whois` services emitted in resolved DID Documents (spec §3.8/§3.9); shared `didweb.ImplicitServices` helper reused by `DidWebPublisher`. - `witness: {}` round-trip across Python/TS (NPE + JCS divergence). - Pre-rotation entries authorized against the current entry's own updateKeys when the previous entry committed `nextKeyHashes`; `DeactivateDidOperation` signs its intermediate entry with the rotation signer. - Witness-proof pruning and bare-multikey witness ids accepted. - Closed `negative-pre-rotation-omit-updatekeys` and `negative-portable-scid-swap` gaps. - Vendored interop fixtures from swcurran/didwebvh-test-suite and added dedicated JUnit coverage for each. - CI/release: GHA upgraded to Node.js 24-compatible versions and base64-encoded OSSRH_TOKEN auto-decoded. Refreshed the compare-link footer with v0.2.0 and v0.3.0 entries. --- CHANGELOG.md | 107 +++++++++++++++++++++++++++------ README.md | 6 +- didwebvh-core/pom.xml | 2 +- didwebvh-signing-local/pom.xml | 2 +- didwebvh-wizard/pom.xml | 2 +- pom.xml | 2 +- 6 files changed, 94 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3719939..97f20fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,29 +7,87 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -## [0.2.1] - 2026-05-06 +## [0.3.0] - 2026-05-28 -### Changed -- **Namespace migration**: - Java package namespace migrated from - `io.github.ivir3zam.*` to - `io.github.decentralizedidentity.*` to reflect the new project - ownership and align with the GitHub organization. -- **Maven coordinates update**: - `groupId` changed from `io.github.ivir3zam` to - `io.github.decentralizedidentity` across all modules to match the - new namespace and publishing coordinates. -- **Repository migration**: - project moved from a personal repository to the - `decentralized-identity` GitHub organization; SCM metadata, - documentation links, and issue tracking URLs updated accordingly. +This release closes the failures reported by the +[did:webvh interop test suite](https://github.com/swcurran/didwebvh-test-suite) +against the v0.2.0 line (see issue #2). + +### Added +- **Negative interop test vectors** vendored from the upstream + did:webvh test suite under + `didwebvh-core/src/test/resources/interop/` + (`basic-create/python`, `basic-update/ts`, full `java-eecc` log + set, `pre-rotation-consume/{rust,java-eecc}`, + `witness-update/rust`, `witness-threshold/rust`, and + `negative-cross-did-witness-replay/ts`), each exercised by a + dedicated JUnit test under `didwebvh-core/.../interop/`. +- **Implicit `#files` and `#whois` services in resolved DID + Documents** (spec §3.8 and §3.9): the resolver now emits a + `relativeRef` `#files` service and a + `LinkedVerifiablePresentation` `#whois` service unless the + controller has already declared services with the same id. The + shared logic lives in `didweb.ImplicitServices` and is also + reused by `DidWebPublisher`. ### Fixed -- **Post-migration build issues**: - resolved inconsistencies caused by outdated package imports and - Maven coordinates after the namespace and repository migration. +- **Cross-DID witness-proof replay** (spec §3.7.5, lines 884-889): + when the `witness` parameter is set to `{}` while witnesses were + active, the transition entry MUST itself be witnessed by the + prior witnesses. `WitnessValidator` was merging the empty config + in first and skipping the entry as inactive, which let an + attacker disable witnessing and replay a stale (or cross-DID) + proof for an earlier entry. The validator now tracks the prior + config and, on a witness-off transition, requires approval from + the prior witnesses. +- **`witness: {}` round-trip across implementations**: Python and + TS serialise an empty `"witness": {}` object in parameters when + no witnesses are configured. Gson was instantiating + `WitnessConfig` via `Unsafe.allocateInstance`, bypassing the + constructor and leaving `witnesses` null — every call to + `isActive()` / `getWitnesses()` then NPE'd on the first + Python/TS log entry. Java was also re-serialising the empty + config as `{"threshold":0,"witnesses":[]}` instead of `{}`, + producing a different JCS canonical form for SCID, entry-hash + and proof computation. Added a no-arg constructor (so Gson uses + `Constructor.newInstance`) and a `WitnessConfigTypeAdapter` that + round-trips the empty-object form. +- **Pre-rotation entries authorized against the wrong key set** + (spec §3.7.5): when the previous entry committed a + `nextKeyHashes`, the active updateKeys for the current entry are + the current entry's own `updateKeys`, not the previous entry's. + `LogChainValidator` was unconditionally using the previous + entry, failing every `pre-rotation-consume` log from rust and + java-eecc. `DeactivateDidOperation` made the same mistake when + emitting its intermediate pre-rotation-consuming entry; it now + signs that entry with `nextRotationSigner`. Regenerated + `pre-rotation-log.jsonl` under the corrected rules. +- **Witness-proof pruning and bare-multikey witness ids** (spec + §3.7.8): a witness proof at versionId V implicitly approves all + prior log entries, and the DID Controller SHOULD prune + `did-witness.json` to retain only the latest proof per witness. + `WitnessValidator` required an exact-versionId proof per entry + and failed with "missing witness proof" against the Rust pruned + files. It also compared witness ids as `did:key:` + against the Rust implementation's bare-multikey form, yielding 0 + authorized proofs. The validator now pre-verifies all proofs + once, counts distinct authorized witnesses per entry, and + matches witness ids by multikey portion to accept both + `did:key:z6Mk…` and bare `z6Mk…` forms. +- **Pre-rotation and portable-SCID negative-test gaps** closed for + `negative-pre-rotation-omit-updatekeys` and + `negative-portable-scid-swap`. +- **Release auth**: auto-detect and decode base64-encoded + `user:pass` `OSSRH_TOKEN` values so Sonatype publishing succeeds + with either token form. + +### Changed +- **CI**: GitHub Actions upgraded to versions compatible with + Node.js 24, and Node.js 24 is forced in both the CI and release + workflows to silence the deprecation warning emitted by older + actions on the GHA runners. -## [0.2.0] - 2026-04-21 +## [0.2.0] - 2026-05-06 ### Added - **Wizard – Export parallel `did:web` document**: new menu option @@ -49,6 +107,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 the library can be used standalone without the wizard. ### Changed +- **Project relocation to `decentralized-identity` GitHub + organization**: Java package namespace migrated from + `io.github.ivir3zam.*` to `io.github.decentralizedidentity.*` and + Maven `groupId` changed from `io.github.ivir3zam` to + `io.github.decentralized-identity` across all modules. SCM + metadata, documentation links, and issue tracking URLs updated + accordingly. - **Wizard – update flow preserves existing state**: - Witness configure seeds from the active `WitnessConfig` so existing witnesses are kept and the threshold can span the full merged set. @@ -123,5 +188,7 @@ Initial public release of `didwebvh-java`, a Java 11+ implementation of the `.github/workflows/release.yml` that publishes to Sonatype Central on tag push (`v*`) and attaches JARs to the GitHub Release. -[Unreleased]: https://github.com/decentralized-identity/didwebvh-java/compare/v0.1.0...HEAD +[Unreleased]: https://github.com/decentralized-identity/didwebvh-java/compare/v0.3.0...HEAD +[0.3.0]: https://github.com/decentralized-identity/didwebvh-java/compare/v0.2.0...v0.3.0 +[0.2.0]: https://github.com/decentralized-identity/didwebvh-java/compare/v0.1.0...v0.2.0 [0.1.0]: https://github.com/decentralized-identity/didwebvh-java/releases/tag/v0.1.0 diff --git a/README.md b/README.md index c56487f..95726c0 100644 --- a/README.md +++ b/README.md @@ -31,9 +31,9 @@ A Java 11+ library for the [did:webvh](https://didwebvh.info/) (DID Web + Verifi ```xml - io.github.decentralizedidentity + io.github.decentralized-identity didwebvh-java - 0.2.0 + 0.3.0 ``` @@ -44,7 +44,7 @@ need the local-key adapter. ### Gradle ```groovy -implementation 'io.github.decentralized-identity:didwebvh-java:0.2.0' +implementation 'io.github.decentralized-identity:didwebvh-java:0.3.0' ``` ## Library Usage diff --git a/didwebvh-core/pom.xml b/didwebvh-core/pom.xml index e7c557b..b627854 100644 --- a/didwebvh-core/pom.xml +++ b/didwebvh-core/pom.xml @@ -7,7 +7,7 @@ io.github.decentralized-identity didwebvh-java - 0.2.0-SNAPSHOT + 0.3.0-SNAPSHOT didwebvh-core diff --git a/didwebvh-signing-local/pom.xml b/didwebvh-signing-local/pom.xml index 49789d6..393d43b 100644 --- a/didwebvh-signing-local/pom.xml +++ b/didwebvh-signing-local/pom.xml @@ -7,7 +7,7 @@ io.github.decentralized-identity didwebvh-java - 0.2.0-SNAPSHOT + 0.3.0-SNAPSHOT didwebvh-signing-local diff --git a/didwebvh-wizard/pom.xml b/didwebvh-wizard/pom.xml index 17a4786..93ae45e 100644 --- a/didwebvh-wizard/pom.xml +++ b/didwebvh-wizard/pom.xml @@ -7,7 +7,7 @@ io.github.decentralized-identity didwebvh-java - 0.2.0-SNAPSHOT + 0.3.0-SNAPSHOT didwebvh-wizard diff --git a/pom.xml b/pom.xml index fbf7e93..afc6dd1 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ io.github.decentralized-identity didwebvh-java - 0.2.0-SNAPSHOT + 0.3.0-SNAPSHOT pom didwebvh-java