# ex_did
[](https://hex.pm/packages/ex_did)
[](https://hexdocs.pm/ex_did)
[](https://github.com/bawolf/ex_did/actions/workflows/ci.yml)
[](https://github.com/bawolf/ex_did/blob/main/LICENSE)
`ex_did` is a typed DID resolution library for Elixir.
Quick links: [Hex package](https://hex.pm/packages/ex_did) | [Hex docs](https://hexdocs.pm/ex_did) | [Changelog](https://github.com/bawolf/ex_did/blob/main/CHANGELOG.md) | [Fixture policy](https://github.com/bawolf/ex_did/blob/main/FIXTURE_POLICY.md) | [CI](https://github.com/bawolf/ex_did/actions/workflows/ci.yml)
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:
- `did:key` strict is Multikey-first for every supported multicodec
- `did:jwk` strict is JWK-native for every supported JWK family
- `validation: :compat` exists for legacy/interoperability quirks, not as a
second equal output family
## Status
Current support:
- typed DID / DID URL parsing
- typed result structs for resolve, representation, and dereference operations
- method registry with first-class `did:web`, `did:key`, and `did:jwk`
- strict validation by default with opt-in `validation: :compat`
- deterministic local resolution for `did:key` and `did:jwk`
- `did:web` URL mapping, fetching, validation, and dereferencing
- verification method extraction across current and legacy shapes
- maintainer-only parity corpora for both JavaScript resolvers and `ssi-dids`
Not implemented yet:
- additional DID methods
- more advanced HTTP caching policy for `did:web`
- broader DID document normalization rules beyond the current strict/compat set
- broader `ssi` disagreement handling beyond the currently documented fixtures
## Installation
Add `ex_did` to your dependencies:
```elixir
def deps do
[
{:ex_did, "~> 0.1.1"},
{: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:
```elixir
{:ok, did_url} = ExDid.parse("did:web:example.com#key-1")
did_url.method
# => "web"
```
Resolve a DID:
```elixir
result = ExDid.resolve("did:key:z6MkeXCES4onVW4up9Qgz1KRnZsKmGufcaZxF6Zpv2w5QwUK")
document = result.did_document
verification_method = hd(document["verificationMethod"])
```
Resolve a representation:
```elixir
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:
```elixir
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:
```elixir
result =
ExDid.resolve("did:jwk:...", validation: :compat)
```
Override the registry or representation per call:
```elixir
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:
```elixir
{: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:
```elixir
{:ok, did} = ExDid.did_web("greenfield.gov")
{:ok, verification_method} =
ExDid.verification_method_id("did:key:z6MkeXCES4onVW4up9Qgz1KRnZsKmGufcaZxF6Zpv2w5QwUK")
```
## Supported Method Matrix
`did:web`
- HTTPS document resolution
- DID document dereferencing for fragments and explicit `service` parameter path/query handling
`did:key`
- supported multicodec prefixes: Ed25519, X25519, secp256k1, P-256, P-384, P-521
- fixture-covered examples: Ed25519, X25519, secp256k1, P-256, P-384
- strict profile: `Multikey` / `publicKeyMultibase`
- compat profile: legacy JS shapes for Ed25519/X25519 where fixture-backed
`did:jwk`
- supported public key shapes: OKP, EC, RSA
- fixture-covered examples: OKP, EC P-256, RSA
- strict profile: JWK-native `publicKeyJwk`
- compat profile: same output family, with leniency such as private-material stripping
## 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:
- strict: `application/did+json`, `application/did+ld+json`
- compat: strict types plus `application/json`
If the response media type falls outside those rules, resolution returns
`invalidDidDocument`.
## did:web Rules
`ex_did` follows the standard `did:web` mapping:
- `did:web:example.com` -> `https://example.com/.well-known/did.json`
- `did:web:example.com:user:alice` -> `https://example.com/user/alice/did.json`
- `did:web:localhost%3A8443` -> `https://localhost:8443/.well-known/did.json`
URI-encoded DID path components are decoded before building the HTTPS URL.
## Testing And Parity
The library is tested with:
- vendored fixture documents under `test/fixtures/`
- JavaScript resolver parity corpora under `test/fixtures/upstream/`
- `ssi-dids` parity corpora under `test/fixtures/upstream/ssi/`
- fixture provenance manifests for both local deterministic fixtures and
upstream-recorded fixtures
- property tests for DID parsing and deterministic local DID generation
- strict / compat coverage for `did:web`, `did:key`, and `did:jwk`
- dereferencing coverage for `did:web`, `did:key`, and `did:jwk`
- injected fetch functions so `did:web` resolution remains deterministic
- downstream validation against current `apps/delegate` DID usage
Refresh upstream parity fixtures with the maintainer-only recorder:
```bash
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:
```bash
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`.
- `upstream/released` is contractual and should back CI
- `upstream/main` is advisory drift detection
- `upstream/ssi/released` is contractual for overlapping DID-only `ssi` behavior
- `upstream/ssi/main` is advisory `ssi` drift detection
- scratch captures and debug output should not be committed
- compat behavior must be backed by committed upstream fixture evidence
When the JavaScript and `ssi` ecosystems disagree, `ex_did` documents the
disagreement in tests and keeps strict mode aligned to the library's
method-specific canonical behavior rather than silently switching output
shapes. In practice, that means `did:key` strict follows the library's
Multikey-first contract while `did:jwk` strict remains JWK-native even though
`ssi` may render that method differently.
## Open Source Notes
- License: MIT
- Changelog: `CHANGELOG.md`
- Fixture policy: `FIXTURE_POLICY.md`
- The current stable public facade is `ExDid`; method modules are implementation details even though they are user-overridable through the method registry.
- Canonical package repository: [github.com/bawolf/ex_did](https://github.com/bawolf/ex_did)
## Maintainer Workflow
`ex_did` currently lives in the `delegate` monorepo and is mirrored into the
standalone `ex_did` repository for publishing and external consumption.
The intended workflow is:
1. make library changes in `libs/ex_did`
2. run `mix test`
3. sync the package into a clean checkout of `github.com/bawolf/ex_did`
4. review and push from the standalone repo
A helper script for the sync step lives at `scripts/sync_standalone_repo.sh`.
The standalone repository also carries GitHub Actions workflows for:
- CI on push and pull request
- manual Hex publishing through `workflow_dispatch`
The publish workflow expects a `HEX_API_KEY` repository secret in the standalone
`ex_did` repository.