ex_did

Hex.pmHex DocsCILicense

ex_did is a typed DID resolution library for Elixir.

Quick links: Hex package | Hex docs | Changelog | Interop notes | Fixture policy | CI

What Standard Is This?

Decentralized Identifiers (DIDs) are self-describing identifiers like did:web:example.com or did:key:z.... A DID resolves to a DID document, which usually tells other systems which keys or service endpoints belong to that identifier.

ex_did implements the DID Core resolution and dereferencing boundary for Elixir, with first-class support for did:web, did:key, and did:jwk.

If you want the formal standards context, start with:

Why You Might Use It

Use ex_did when you need to:

The library exposes a method-agnostic API for DID parsing, DID resolution, DID representation resolution, DID URL dereferencing, and a small number of method-specific helper functions that remove common string-building mistakes.

ex_did is intentionally DID-only. VC, VP, JWT, JWS, Data Integrity, and other SSI proof features belong in sibling libraries built on top of this DID foundation.

Strict mode is canonical per method:

Status

Current support:

Not implemented yet:

Installation

Add ex_did to your dependencies:

def deps do
  [
    {:ex_did, "~> 0.1.2"},
    {:jose, "~> 1.11"}
  ]
end

Usage

Normal ex_did usage does not require JavaScript, Rust, pnpm, Cargo, or network access. Those are maintainer-only dependencies for rebuilding upstream parity fixtures.

Parse a DID or DID URL:

{:ok, did_url} = ExDid.parse("did:web:example.com#key-1")
did_url.method
# => "web"

Resolve a DID:

result = ExDid.resolve("did:key:z6MkeXCES4onVW4up9Qgz1KRnZsKmGufcaZxF6Zpv2w5QwUK")

document = result.did_document
verification_method = hd(document["verificationMethod"])

Resolve a representation:

result = ExDid.resolve_representation("did:web:example.com", fetch_json: fn _url ->
  {:ok, %{"@context" => ["https://www.w3.org/ns/did/v1"], "id" => "did:web:example.com"}}
end)

Jason.decode!(result.content_stream)

Derefence a DID URL:

result = ExDid.dereference("did:web:example.com#key-1", fetch_json: fn _url ->
  {:ok,
   %{
     "id" => "did:web:example.com",
     "verificationMethod" => [
       %{"id" => "did:web:example.com#key-1", "controller" => "did:web:example.com"}
     ]
   }}
end)

result.content_stream["id"]

Use compatibility mode only when needed for known ecosystem quirks:

result =
  ExDid.resolve("did:jwk:...", validation: :compat)

Override the registry or representation per call:

registry = %{"web" => ExDid.Method.Web}

result =
  ExDid.resolve_representation("did:web:example.com",
    method_registry: registry,
    accept: "application/did+ld+json",
    fetch_json: fn _url ->
      {:ok, %{"@context" => ["https://www.w3.org/ns/did/v1"], "id" => "did:web:example.com"}}
    end
  )

Map a did:web DID to its document URL:

{:ok, url} = ExDid.resolve_url("did:web:example.com:user:alice")
# => "https://example.com/user/alice/did.json"

Build a canonical did:web value or canonical local verification method id:

{:ok, did} = ExDid.did_web("greenfield.gov")
{:ok, verification_method} =
  ExDid.verification_method_id("did:key:z6MkeXCES4onVW4up9Qgz1KRnZsKmGufcaZxF6Zpv2w5QwUK")

Supported Method Matrix

did:web

did:key

did:jwk

Validation Modes

ex_did defaults to validation: :strict.

validation: :compat is opt-in and intentionally narrow. It currently only relaxes behaviors that are explicitly implemented and covered by fixtures and tests, such as normalizing a single service object into a list, preserving legacy JS did:key shapes where documented, and stripping private material from did:jwk inputs before building the public DID document.

Strict mode is the production default. Compat mode is an interoperability tool, not a second equal runtime profile.

did:web Transport Rules

did:web resolution accepts the following response media types:

If the response media type falls outside those rules, resolution returns invalidDidDocument.

did:web Rules

ex_did follows the standard did:web mapping:

URI-encoded DID path components are decoded before building the HTTPS URL.

Testing And Parity

The library is tested with:

Refresh upstream parity fixtures with the maintainer-only recorder:

cd libs/ex_did/scripts/upstream_parity
pnpm install
pnpm run record:released
pnpm run record:main

Refresh ssi parity fixtures with the maintainer-only Rust recorder:

cd libs/ex_did/scripts/ssi_parity
cargo run -- released
cargo run -- main

The committed fixtures are the contract. End users and CI should not need to run either recorder.

Fixture Policy

The fixture policy is documented in FIXTURE_POLICY.md.

When the JavaScript and ssi ecosystems disagree, ex_did records the released divergence set in test/fixtures/divergences/released.json, enforces it in tests, and keeps strict mode aligned to the library’s method-specific canonical behavior rather than silently switching output shapes. See INTEROP_NOTES.md for the current decision log.

Open Source Notes

Maintainer Workflow

ex_did is developed in the delegate monorepo. The public github.com/bawolf/ex_did repository is the mirrored OSS surface for issues, discussions, releases, and Hex publishing.

The monorepo copy is authoritative for:

Direct standalone-repo edits are temporary hotfixes only and must be backported to the monorepo immediately.

The intended workflow is:

  1. make library changes in libs/ex_did
  2. run scripts/release_preflight.sh
  3. sync the package into a clean checkout of github.com/bawolf/ex_did
  4. verify the mirrored required file set with scripts/verify_standalone_repo.sh
  5. review and push from the standalone repo

A helper to sync all public package repos from the monorepo lives at /Users/bryantwolf/workspace/delegate/scripts/sync_public_libs.sh.

The mirrored standalone repository carries GitHub Actions workflows for:

The publish workflow expects a HEX_API_KEY repository secret in the standalone ex_did repository. Once triggered, it publishes to Hex and then creates the matching Git tag and GitHub release automatically.

Releasing From GitHub

Releases are cut from the public github.com/bawolf/ex_did repository, not from the private monorepo checkout.

The shortest safe path is:

  1. finish the change in libs/ex_did
  2. run scripts/release_preflight.sh
  3. sync and verify the standalone repo with scripts/sync_standalone_repo.sh and scripts/verify_standalone_repo.sh
  4. push the mirrored release commit to main in github.com/bawolf/ex_did
  5. in GitHub, go to Actions, choose Publish, and run it with the version from mix.exs

The GitHub workflow is responsible for:

Run the local preflight with:

scripts/release_preflight.sh