ex_vc
ex_vc is a focused Verifiable Credentials 2.0 library for Elixir.
Quick links: Hex package | Hex docs | Supported features | Interop notes | Fixture policy
What Standard Is This?
Verifiable Credentials are a way to package signed claims such as identity, authorization, affiliation, or delegation in a portable format. A credential says something like “issuer X asserts subject Y has property Z”, and a verifier can check the signature and the credential rules.
ex_vc implements the VC Data Model 2.0 core boundary for Elixir, with a
release-1 focus on generic VC validation, Verifiable Presentation boundaries,
and vc+jwt.
If you want the formal standards context, start with:
Why You Might Use It
Use ex_vc when you need to:
- issue or verify signed credentials in an API-friendly way
- validate generic VC envelopes before applying product-specific profile rules
-
verify
vc+jwtcredentials with direct keys or DIDs resolved throughex_did - load and validate Verifiable Presentations without pulling VC rules into your app
Release 1 is intentionally narrow:
- VC 2.0 credential normalization and validation
- Verifiable Presentation loading plus structural verification boundaries
- normalized issuance helpers
vc+jwtissuance and verification-
DID-based verification key resolution through
ex_did
Data Integrity and SD-JWT VC code remains in-tree for later parity work, but they are not part of the release 1 support contract, Hex support posture, or parity claims.
Status
Release 1 supported surface:
ExVc.load/1,ExVc.to_map/1ExVc.validate/2,ExVc.valid?/2ExVc.issue/2ExVc.verify/2ExVc.verify_presentation/2ExVc.sign_jwt_vc/3,ExVc.verify_jwt_vc/2-
DID-based verification through
ex_did, including method-specific key normalization such asdid:jwkand multikey-backeddid:key
Runtime deliberately does not require:
- Node / pnpm
- Rust / Cargo
- network access
Those toolchains are maintainer-only and reserved for refreshing parity fixtures.
Installation
def deps do
[
{:ex_vc, "~> 0.1.1"}
]
endUsage
Validate a VC envelope
credential = %{
"@context" => ["https://www.w3.org/ns/credentials/v2"],
"type" => ["VerifiableCredential"],
"issuer" => "did:web:example.com",
"credentialSubject" => %{"id" => "did:key:z6MkwExample"}
}
{:ok, report} = ExVc.validate(credential)
report.parsed.normalizedIssue a normalized credential
{:ok, credential} =
ExVc.issue(
%{"credentialSubject" => %{"id" => "did:key:z6MkwExample"}},
issuer: "did:web:example.com",
types: ["VerifiableCredential", "ExampleCredential"]
)Sign and verify VC-JWT
{:ok, jwt} = ExVc.sign_jwt_vc(credential, issuer_jwk, alg: "ES256")
{:ok, verification} = ExVc.verify_jwt_vc(jwt, jwk: issuer_public_jwk)
verification.format
verification.credentialYou can also verify through DID resolution instead of passing a raw JWK:
{:ok, verification} = ExVc.verify_jwt_vc(jwt, issuer_did: "did:key:zDna...")Use auto-detected verification
{:ok, verification} = ExVc.verify(jwt, issuer_did: "did:key:zDna...")
verification.verifiedValidate a Verifiable Presentation
presentation = %{
"@context" => ["https://www.w3.org/ns/credentials/v2"],
"type" => ["VerifiablePresentation"],
"holder" => "did:key:z6MkwHolder",
"verifiableCredential" => [credential]
}
{:ok, result} = ExVc.verify_presentation(presentation)
result.verifiedverify_presentation/2 in release 1 is a strict VP boundary:
- validates the VP envelope
- validates embedded credential envelope shape
- fails closed if embedded credential verification is requested and fails
It is not advertised as full proof-bearing VP interoperability yet.
Validation Model
ExVc.validate/2 enforces the generic VC envelope:
- VC 2.0 context presence
VerifiableCredentialtype presence- issuer presence as string or issuer object
credentialSubjectpresence as object or list of objects-
ISO8601 validation for
validFrom,validUntil, andproof.created -
temporal ordering for
validFrom/validUntil - generic status object validation plus Bitstring Status List entry fields when that type is used
Application or profile semantics belong in caller-supplied validators.
Interoperability Strategy
ex_vc uses three evidence layers:
- property tests for invariants
- golden fixtures for deterministic library behavior
- committed released fixtures for oracle-backed surfaces
The parity fixture layout mirrors ex_did:
test/fixtures/upstream/dcb/released/test/fixtures/upstream/dcb/main/test/fixtures/upstream/ssi/released/test/fixtures/upstream/ssi/main/
Release 1 parity claims are intentionally narrow:
- VC envelope validation is backed by committed DCB and Spruce fixtures
vc+jwtand VP boundary behavior are covered by local runtime tests- Data Integrity and SD-JWT VC are deferred from the public parity contract
See:
SUPPORTED_FEATURES.mdINTEROP_NOTES.mdFIXTURE_POLICY.md
Open Source Notes
- License: MIT
-
Changelog:
CHANGELOG.md -
Fixture policy:
FIXTURE_POLICY.md - Canonical package repository: github.com/bawolf/ex_vc
-
The stable public facade for release 1 is
ExVc; deferred proof adapters may exist in-tree without being part of the support contract
Maintainer Workflow
ex_vc is developed in the delegate monorepo. The public
github.com/bawolf/ex_vc repository is the mirrored OSS surface for issues,
discussions, releases, and Hex publishing.
The monorepo copy is authoritative for:
- code
- tests and fixtures
- docs
- GitHub workflows
- release tooling
Direct standalone-repo edits are temporary hotfixes only and must be backported to the monorepo immediately.
The intended workflow is:
-
make library changes in
libs/ex_vc -
run
scripts/release_preflight.sh -
publish the corresponding
ex_diddependency release first whenex_vcdepends on a newerex_didversion -
sync the package into a clean checkout of
github.com/bawolf/ex_vc -
verify the mirrored required file set with
scripts/verify_standalone_repo.sh - review and push from the standalone repo
- trigger the publish workflow 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 standalone repository should carry GitHub Actions workflows for:
- CI on push and pull request
-
manual publish through
workflow_dispatch
The publish workflow expects a HEX_API_KEY repository secret in the standalone
ex_vc repository. Once triggered, it publishes to Hex and then creates the
matching Git tag and GitHub release automatically.
Maintainer Tooling
Refresh TypeScript parity fixtures:
cd libs/ex_vc/scripts/upstream_parity
pnpm install
pnpm run record:released
pnpm run record:mainRefresh Rust parity fixtures:
cd libs/ex_vc/scripts/ssi_parity
cargo run -- released
cargo run -- mainNormal users and release consumers do not need either toolchain.
Release Automation
The standalone ex_vc repository is expected to carry:
- CI on push and pull request
- a manual publish workflow
The publish workflow should be triggered through workflow_dispatch after the
version and changelog are ready. It publishes to Hex first and then creates the
matching Git tag and GitHub release automatically. It expects a HEX_API_KEY
repository secret in the standalone ex_vc repository.
Releasing From GitHub
Releases are cut from the public github.com/bawolf/ex_vc repository, not from
the private monorepo checkout.
The shortest safe path is:
-
finish the change in
libs/ex_vc -
run
scripts/release_preflight.sh -
if
mix.exspoints at a newerex_did, publishex_didfirst -
sync and verify the standalone repo with
scripts/sync_standalone_repo.shandscripts/verify_standalone_repo.sh -
push the mirrored release commit to
mainingithub.com/bawolf/ex_vc -
in GitHub, go to
Actions, choosePublish, and run it with the version frommix.exs
The GitHub workflow is responsible for:
- rerunning the release gate
- publishing to Hex
- creating the matching git tag
- creating the matching GitHub release
Run the local preflight with:
scripts/release_preflight.sh