Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.3.1] - 2026-06-09

### Fixed
- **Witness-list updates witnessed by the wrong list** (spec §3.7.5,
issue #6): per the spec a replacement `witness` list only becomes
active *after* the entry that introduces it is published, so the
entry making the change must be witnessed by the list in effect
*before* it. Both the proof generator (`WizardWitnessProofs`) and
the resolver (`WitnessValidator`) were instead using the new
(merged) list, so reducing a list (e.g. two witnesses down to one)
produced and required only a single proof. Now an entry is
witnessed by the prior active list whenever witnessing was already
active — covering list changes and turning witnessing off — with
the new list applying only on first activation. This matches the
Rust reference output for `vectors/witness-update`.

## [0.3.0] - 2026-05-29

This release closes the failures reported by the
Expand Down Expand Up @@ -203,7 +219,8 @@ 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.3.0...HEAD
[Unreleased]: https://github.com/decentralized-identity/didwebvh-java/compare/v0.3.1...HEAD
[0.3.1]: https://github.com/decentralized-identity/didwebvh-java/compare/v0.3.0...v0.3.1
[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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ A Java 11+ library for the [did:webvh](https://didwebvh.info/) (DID Web + Verifi
<dependency>
<groupId>io.github.decentralized-identity</groupId>
<artifactId>didwebvh-java</artifactId>
<version>0.3.0</version>
<version>0.3.1</version>
</dependency>
```

Expand All @@ -44,7 +44,7 @@ need the local-key adapter.
### Gradle

```groovy
implementation 'io.github.decentralized-identity:didwebvh-java:0.3.0'
implementation 'io.github.decentralized-identity:didwebvh-java:0.3.1'
```

## Library Usage
Expand Down
2 changes: 1 addition & 1 deletion didwebvh-core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<parent>
<groupId>io.github.decentralized-identity</groupId>
<artifactId>didwebvh-java</artifactId>
<version>0.3.0-SNAPSHOT</version>
<version>0.3.1-SNAPSHOT</version>
</parent>

<artifactId>didwebvh-core</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,24 +66,23 @@
activeParams = activeParams.merge(entryParams);
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.
// Spec §3.7.5 (lines 884-889): a replacement witness list "becomes
// active AFTER the new DID log has been published". So while
// witnesses are already active, every entry — including one that
// changes the list (e.g. two witnesses down to one) or sets it to
// {} — MUST be witnessed by the list in effect BEFORE this entry;

Check warning on line 73 in didwebvh-core/src/main/java/io/github/decentralizedidentity/didwebvh/core/validate/WitnessValidator.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

This block of commented-out lines of code should be removed.

See more on https://sonarcloud.io/project/issues?id=decentralized-identity_didwebvh-java&issues=AZ6swm9DDMjfSxivVjdV&open=AZ6swm9DDMjfSxivVjdV&pullRequest=7
// the replacement only governs subsequent entries. This also stops
// an attacker from shrinking or disabling oversight in a single
// forged entry and slipping it past the resolver. The sole
// exception is the first activation, where there is no prior list.
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.
if (priorActive) {
witnessConfig = priorConfig;
} else if (afterActive) {
// First activation ({} -> active): the new list approves this entry.
witnessConfig = afterConfig;
} else {
continue;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,65 @@ void crossDidWitnessReplayRejected() {
assertThat(result.getFailureReason()).contains("insufficient witness proofs");
}

private WitnessProofCollection makeWitnessProofs(WitnessProofEntry... entries) {
return new WitnessProofCollection(Arrays.asList(entries));
}

private WitnessProofEntry makeProofEntry(String versionId, Signer... signers) {
JsonObject doc = new JsonObject();
doc.addProperty("versionId", versionId);
DataIntegrityProof[] proofs = new DataIntegrityProof[signers.length];
for (int i = 0; i < signers.length; i++) {
proofs[i] = LogChainValidatorTest.signDocument(signers[i], doc);
}
return new WitnessProofEntry(versionId, Arrays.asList(proofs));
}

@Test
void witnessListReductionStillRequiresPriorList() {
// Spec §3.7.5: when entry 2 reduces the witness list from {w1,w2}@2 to
// {w1}@1, that change takes effect only AFTER entry 2 is published, so
// entry 2 must still be witnessed by the PRIOR list {w1,w2}@2. A pruned
// did-witness.json with both proofs at versionId 2 satisfies both
// entries (issue #6). See vectors/witness-update/rust.
CreateDidResult create = createWithWitness(2,
new WitnessEntry(witnessDid1), new WitnessEntry(witnessDid2));
LogEntry e1 = create.getLogEntry();
LogEntry e2 = LogChainValidatorTest.buildUpdateEntry(
e1, create.getDid(), authorSigner,
new Parameters().setWitness(new WitnessConfig(1,
Collections.singletonList(new WitnessEntry(witnessDid1)))),
null);

WitnessProofCollection proofs = makeWitnessProofs(
makeProofEntry(e2.getVersionId(), witnessSigner1, witnessSigner2));

WitnessValidationResult result = validator.validate(Arrays.asList(e1, e2), proofs, 0);
assertThat(result.isValid()).isTrue();
}

@Test
void witnessListReductionWithOnlyNewListFails() {
// The buggy behaviour from issue #6: entry 2 reduces to {w1}@1 and only
// w1 provides a proof. Because entry 2 must be witnessed by the prior
// {w1,w2}@2 list, a single proof is insufficient and resolution fails.
CreateDidResult create = createWithWitness(2,
new WitnessEntry(witnessDid1), new WitnessEntry(witnessDid2));
LogEntry e1 = create.getLogEntry();
LogEntry e2 = LogChainValidatorTest.buildUpdateEntry(
e1, create.getDid(), authorSigner,
new Parameters().setWitness(new WitnessConfig(1,
Collections.singletonList(new WitnessEntry(witnessDid1)))),
null);

WitnessProofCollection proofs = makeWitnessProofs(
makeProofEntry(e2.getVersionId(), witnessSigner1));

WitnessValidationResult result = validator.validate(Arrays.asList(e1, e2), proofs, 0);
assertThat(result.isValid()).isFalse();
assertThat(result.getFailureReason()).contains("insufficient");
}

@Test
void fromEntryIndexRespected() {
// Two entries; only second requires witnessing
Expand Down
2 changes: 1 addition & 1 deletion didwebvh-signing-local/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<parent>
<groupId>io.github.decentralized-identity</groupId>
<artifactId>didwebvh-java</artifactId>
<version>0.3.0-SNAPSHOT</version>
<version>0.3.1-SNAPSHOT</version>
</parent>

<artifactId>didwebvh-signing-local</artifactId>
Expand Down
2 changes: 1 addition & 1 deletion didwebvh-wizard/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<parent>
<groupId>io.github.decentralized-identity</groupId>
<artifactId>didwebvh-java</artifactId>
<version>0.3.0-SNAPSHOT</version>
<version>0.3.1-SNAPSHOT</version>
</parent>

<artifactId>didwebvh-wizard</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,22 +79,28 @@ void collectForNewEntries(Parameters priorParams, List<LogEntry> newEntries, Pat
}

/**
* Per spec: witness proofs are required when either the merged config AFTER this
* entry's delta is active (normal case) OR when this entry sets witness back to
* {@code {}} while it was previously active (transition-off case). In both cases
* the authorized witness set for signing is the previously active set when turning
* witnesses off, otherwise the newly merged set.
* Determine which witness set must approve {@code entryDelta}.
*
* <p>Per spec §3.7.5, a new {@code witness} list "becomes active AFTER the new
* DID log has been published". So while witnesses are already active, every
* entry — including one that <em>changes</em> the list (e.g. from two witnesses
* to one) or one that turns witnessing off ({@code {}}) — MUST be witnessed by
* the list that was in effect <em>before</em> this entry. The replacement list
* only governs subsequent entries. Returning the prior list here also closes the
* loophole where a controller could unilaterally shrink or remove oversight in a
* single unapproved entry.
*
* <p>The only exception is the first activation: when no witnesses were active
* before, there is no prior list to defer to, so the newly merged list applies
* immediately.
*/
private WitnessConfig authorizedWitnesses(Parameters priorParams, Parameters entryDelta) {
WitnessConfig priorActive = priorParams.getWitness();
Parameters merged = priorParams.merge(entryDelta);
WitnessConfig mergedActive = merged.getWitness();
boolean turningOff = priorActive != null && priorActive.isActive()
&& (mergedActive == null || !mergedActive.isActive());
if (turningOff) {
if (priorActive != null && priorActive.isActive()) {
return priorActive;
}
return mergedActive;
// First activation: no prior witnesses, so the new list applies to this entry.
return priorParams.merge(entryDelta).getWitness();
}

private WitnessProofEntry buildProofEntry(String versionId, WitnessConfig authorized,
Expand Down
16 changes: 16 additions & 0 deletions docs/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,20 @@ Use [Conventional Commits](https://www.conventionalcommits.org/):
- `docs: update README with resolve example`
- `chore: configure JaCoCo for coverage reporting`

## Changelog and Commit Workflow

After completing any change to the codebase, the agent MUST:

1. **Update the changelog.** Add a summary of the change under the `## [Unreleased]`
section of `CHANGELOG.md`, in the appropriate `### Added` / `### Changed` /
`### Fixed` / `### Removed` subsection (create the subsection if it does not yet
exist). Follow the existing style: reference the spec section and/or issue number
for non-obvious behaviour.
2. **Propose a commit message.** End the response with a suggested
[Conventional Commits](https://www.conventionalcommits.org/) commit message for the
change, so the developer can review and commit it. Do not commit on the developer's
behalf unless explicitly asked.

## How to Work on This Project

1. Read `docs/ARCHITECTURE.md` for the full technical design.
Expand All @@ -169,3 +183,5 @@ Use [Conventional Commits](https://www.conventionalcommits.org/):
4. Run `./mvnw clean verify` with JDK 21 after every change to ensure tests, Checkstyle, SpotBugs, and
coverage run locally.
5. Keep dependencies minimal. Don't add a library for something that can be done in 20 lines.
6. After every change, update the `[Unreleased]` section of `CHANGELOG.md` and propose a
commit message (see [Changelog and Commit Workflow](#changelog-and-commit-workflow)).
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

<groupId>io.github.decentralized-identity</groupId>
<artifactId>didwebvh-java</artifactId>
<version>0.3.0-SNAPSHOT</version>
<version>0.3.1-SNAPSHOT</version>
<packaging>pom</packaging>

<name>didwebvh-java</name>
Expand Down
Loading