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
9 changes: 6 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ on:
pull_request:
branches: [ main ]

env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true

jobs:
build:
runs-on: ubuntu-latest
Expand All @@ -14,10 +17,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
Expand All @@ -28,7 +31,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
Expand Down
9 changes: 6 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,19 @@ on:
tags:
- 'v*'

env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true

jobs:
release:
runs-on: ubuntu-latest
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
Expand Down Expand Up @@ -66,7 +69,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: |
Expand Down
107 changes: 87 additions & 20 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:<multikey>`
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
Expand All @@ -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.
Expand Down Expand Up @@ -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
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ A Java 11+ library for the [did:webvh](https://didwebvh.info/) (DID Web + Verifi

```xml
<dependency>
<groupId>io.github.decentralizedidentity</groupId>
<groupId>io.github.decentralized-identity</groupId>
<artifactId>didwebvh-java</artifactId>
<version>0.2.0</version>
<version>0.3.0</version>
</dependency>
```

Expand All @@ -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
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.2.0-SNAPSHOT</version>
<version>0.3.0-SNAPSHOT</version>
</parent>

<artifactId>didwebvh-core</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
}

Expand All @@ -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:<scid>: with did:web: across the whole document.
String replaced = doc.toString().replace(scidPrefix, "did:web:");
Expand All @@ -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<String> seen = new LinkedHashSet<>();
Expand Down
Loading
Loading