A specification-compliant .NET library for Decentralized Identifiers (DIDs). NetDid provides a unified interface for creating, resolving, updating, and deactivating DIDs across multiple DID methods.
- DID methods:
did:key,did:peer, anddid:webvh(implemented),did:ethr(planned) - Eight key types: Ed25519, X25519, P-256, P-384, P-521, secp256k1, BLS12-381 G1/G2
- BBS+ signatures: Multi-message signing with selective disclosure proofs (IETF draft-10)
- W3C DID Core 1.0 compliant DID Document model and serialization
- Dual content types:
application/did+ld+json(JSON-LD) andapplication/did+json - Pluggable key storage: Bring your own HSM, vault, or file-based key store via
IKeyStore - Resolver infrastructure: Composite routing, caching, and W3C DID URL dereferencing (fragment, service, serviceType, verificationRelationship)
- JWK conversion: Round-trip between raw key bytes and JSON Web Keys
- DI integration:
services.AddNetDid()for Microsoft.Extensions.DependencyInjection, or use standalone with zero framework opinions - Fluent document builder:
new DidDocumentBuilder(did).AddVerificationMethod(...).Build()
Cryptography is provided by NetCrypto. NetDid carries no cryptographic primitives — key generation, signing, verification, key agreement, BBS+, JWK conversion, and the native crypto payloads all come from NetCrypto (the
crypto-dotnetproject).did:webvhData Integrity proofs are produced and verified by DataProofsDotnet. Types likeKeyType,DefaultKeyGenerator,ISigner, andInMemoryKeyStorelive in theNetCryptonamespace.
dotnet add package NetDid.Core
dotnet add package NetDid.Method.Key # did:key method
dotnet add package NetDid.Method.Peer # did:peer method
dotnet add package NetDid.Method.WebVh # did:webvh method
dotnet add package NetDid.Extensions.DependencyInjection # Microsoft DI integration
dotnet add package NetCrypto # key generation, signing, JWK (NetCrypto namespace)Note: NetDid targets .NET 10. Ensure you have the .NET 10 SDK installed. NetCrypto is pulled in transitively by the NetDid packages; add it explicitly only if you use its types directly (as the examples below do).
using NetCrypto;
var keyGen = new DefaultKeyGenerator();
var keyPair = keyGen.Generate(KeyType.Ed25519);
Console.WriteLine($"Public key (multibase): {keyPair.MultibasePublicKey}");var crypto = new DefaultCryptoProvider();
var signer = new KeyPairSigner(keyPair, crypto);
byte[] data = "Hello, DIDs!"u8.ToArray();
byte[] signature = await signer.SignAsync(data);
bool valid = crypto.Verify(KeyType.Ed25519, keyPair.PublicKey, data, signature);did:key is a deterministic, self-certifying DID method where the public key is encoded directly in the DID string. No network interaction is needed — resolution is purely algorithmic.
using NetCrypto;
using NetDid.Method.Key;
var keyGen = new DefaultKeyGenerator();
var didKey = new DidKeyMethod(keyGen);
// Create with Ed25519 (most common)
var result = await didKey.CreateAsync(new DidKeyCreateOptions
{
KeyType = KeyType.Ed25519
});
Console.WriteLine(result.Did);
// Output: did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doKEd25519 keys automatically derive an X25519 key agreement key, so the DID Document will contain two verification methods: one for signing (Ed25519) and one for encryption (X25519).
var resolved = await didKey.ResolveAsync("did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK");
var doc = resolved.DidDocument!;
Console.WriteLine($"VMs: {doc.VerificationMethod!.Count}"); // 2 (Ed25519 + X25519)
Console.WriteLine($"Auth: {doc.Authentication!.Count}"); // 1
Console.WriteLine($"Key Agreement: {doc.KeyAgreement!.Count}"); // 1var crypto = new DefaultCryptoProvider();
var existingKeyPair = keyGen.Generate(KeyType.P256);
var signer = new KeyPairSigner(existingKeyPair, crypto);
var result = await didKey.CreateAsync(new DidKeyCreateOptions
{
KeyType = KeyType.P256,
ExistingKey = signer // Works with any ISigner — HSM, vault, or in-memory
});var result = await didKey.CreateAsync(new DidKeyCreateOptions
{
KeyType = KeyType.Ed25519,
Representation = VerificationMethodRepresentation.JsonWebKey2020
});
// VM type: "JsonWebKey2020" with "publicKeyJwk" propertyvar result = await didKey.CreateAsync(new DidKeyCreateOptions
{
KeyType = KeyType.Bls12381G2
});
// assertionMethod: yes (credential issuance with BBS+)
// authentication: no (BBS+ not suitable for challenge-response auth)| Key Type | Multicodec | VM Relationships |
|---|---|---|
| Ed25519 | 0xed |
authentication, assertionMethod, capabilityInvocation, capabilityDelegation + X25519 keyAgreement |
| X25519 | 0xec |
keyAgreement only |
| P-256 | 0x8024 |
authentication, assertionMethod, capabilityInvocation, capabilityDelegation |
| P-384 | 0x8124 |
authentication, assertionMethod, capabilityInvocation, capabilityDelegation |
| secp256k1 | 0xe7 |
authentication, assertionMethod, capabilityInvocation, capabilityDelegation |
| BLS12-381 G1 | 0xea |
assertionMethod, capabilityInvocation |
| BLS12-381 G2 | 0xeb |
assertionMethod, capabilityInvocation |
did:peer is designed for peer-to-peer interactions where DIDs don't need to be published to a ledger. Three numalgo variants are supported.
Functionally identical to did:key but with a did:peer:0 prefix. Useful when you want peer DID semantics with a single key.
using NetCrypto;
using NetDid.Method.Peer;
var keyGen = new DefaultKeyGenerator();
var didPeer = new DidPeerMethod(keyGen);
var result = await didPeer.CreateAsync(new DidPeerCreateOptions
{
Numalgo = PeerNumalgo.Zero,
InceptionKeyType = KeyType.Ed25519
});
Console.WriteLine(result.Did);
// Output: did:peer:0z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktHThe most practical variant for DIDComm messaging. Keys and service endpoints are encoded directly in the DID string. Purpose codes follow the DIF peer-DID spec: A=assertion, E=encryption (key agreement), V=verification (authentication), I=capability invocation, D=capability delegation, S=service.
var crypto = new DefaultCryptoProvider();
var authKey = keyGen.Generate(KeyType.Ed25519);
var agreeKey = keyGen.Generate(KeyType.X25519);
var result = await didPeer.CreateAsync(new DidPeerCreateOptions
{
Numalgo = PeerNumalgo.Two,
Keys =
[
new PeerKeyPurpose(new KeyPairSigner(authKey, crypto), PeerPurpose.Authentication),
new PeerKeyPurpose(new KeyPairSigner(agreeKey, crypto), PeerPurpose.KeyAgreement)
],
Services =
[
new Service
{
Id = "#didcomm",
Type = "DIDCommMessaging",
ServiceEndpoint = ServiceEndpointValue.FromUri("https://example.com/didcomm")
}
]
});
Console.WriteLine(result.Did);
// Output: did:peer:2.Vz6Mkf5r...Ez6LSb...SeyJ0IjoiZG0i...Resolution decodes everything from the DID string — no network call needed:
var resolved = await didPeer.ResolveAsync(result.Did.Value);
var doc = resolved.DidDocument!;
Console.WriteLine(doc.Service![0].Type); // "DIDCommMessaging"
Console.WriteLine(doc.Service[0].ServiceEndpoint.Uri); // "https://example.com/didcomm"All five verification relationships are supported — use PeerPurpose.Assertion, PeerPurpose.CapabilityInvocation, or PeerPurpose.CapabilityDelegation to assign keys to additional relationships.
Service types are abbreviated in the DID string per the DIF spec (DIDCommMessaging → dm, type → t, serviceEndpoint → s).
Uses a SHA-256 hash as the short form and encodes the full input document as the long form. The long form is exchanged initially; subsequent interactions use the short form.
var peer4Key = keyGen.Generate(KeyType.Ed25519);
var inputDoc = new DidDocument
{
// Per spec: input document MUST NOT include id — it's assigned during creation
VerificationMethod =
[
new VerificationMethod
{
Id = "#key-0",
Type = "Multikey",
PublicKeyMultibase = peer4Key.MultibasePublicKey
}
],
Authentication =
[
VerificationRelationshipEntry.FromReference("#key-0")
]
};
var result = await didPeer.CreateAsync(new DidPeerCreateOptions
{
Numalgo = PeerNumalgo.Four,
InputDocument = inputDoc
});
// Long-form DID (exchanged initially)
Console.WriteLine(result.Did);
// did:peer:4zQm...:<base64url-encoded-document>
// Resolution verifies the hash matches the encoded document
var resolved = await didPeer.ResolveAsync(result.Did.Value);Short-form-only resolution returns notFound (requires prior long-form exchange).
did:webvh (DID Web with Verifiable History) combines web-based hosting with a cryptographically verifiable log of all changes. Full CRUD with hash chain integrity, pre-rotation, and witness validation.
using NetCrypto;
using NetDid.Core.Model;
using NetDid.Method.WebVh;
var keyGen = new DefaultKeyGenerator();
var crypto = new DefaultCryptoProvider();
var updateKey = keyGen.Generate(KeyType.Ed25519);
var signer = new KeyPairSigner(updateKey, crypto);
var httpClient = new DefaultWebVhHttpClient();
var didWebVh = new DidWebVhMethod(httpClient);
var result = await didWebVh.CreateAsync(new DidWebVhCreateOptions
{
Domain = "example.com",
UpdateKey = signer,
Services =
[
new Service
{
Id = "#pds",
Type = "TurtleShellPds",
ServiceEndpoint = ServiceEndpointValue.FromUri("https://example.com/pds")
}
]
});
Console.WriteLine(result.Did);
// Output: did:webvh:z6Rk8Rx...:example.comThe result includes Artifacts["did.jsonl"] (the verifiable log) and Artifacts["did.json"] (did:web backwards-compatible document). Host these at https://example.com/.well-known/did.jsonl and did.json. When WitnessProofs are provided, a did-witness.json artifact is also produced.
var resolved = await didWebVh.ResolveAsync("did:webvh:z6Rk8Rx...:example.com");
var doc = resolved.DidDocument!;
Console.WriteLine(doc.Service![0].Type); // "TurtleShellPds"
Console.WriteLine(resolved.DocumentMetadata!.VersionId); // "1-z6Rk8Rx..."Resolution fetches the did.jsonl log over HTTPS, validates the hash chain and Data Integrity Proofs, and returns the latest DID Document.
Every supplied controller proof on an entry is processed by DataProofsDotnet's Data Integrity pipeline and authorized against the active updateKeys: NetDid requires an anti-spoofed did:key verification method, Ed25519 eddsa-jcs-2022, assertionMethod, a valid signature, and an active update key. One authorized signer authorizes the entry, but any invalid or unauthorized extra proof rejects the log as invalidDidLog; controller proofs have no threshold semantics. NetDid applies a conservative System.Uri-compatible absolute-URI check to a present proof id (without surrounding whitespace), rejects duplicate proof ids, resolves previousProof references, and treats expires at or before the entry's versionTime as expired. This accepts the DID, URN, and HTTPS forms used by the SDK, but it is not full WHATWG valid-URL-string conformance and can reject other standards-valid forms. Unknown proof members remain signature-bound and their proof-object JSON is preserved, but NetDid does not claim application semantics for every extension. In particular, array-valued domain is not supported by the pinned DataProofsDotnet model. A wire proof may be a single object or an array and created is optional; reserialization preserves each parsed proof object but normalizes a single-object container to an array. Duplicate JSON members, invalid UTF-8, and malformed content are rejected as invalidDidLog.
Fetched entry hashing and controller/witness verification retain every JSON member under parameters and state, including nested extensions that the typed DID model does not surface. This prevents a post-sign extension injection from disappearing during model reconstruction. Update and Deactivate also preserve those members in prior fetched entries; deliberate public-model mutations fall back to modeled serialization instead of stale wire data. An update that preserves the document (NewDocument == null) likewise carries the previous state's signed nested members into the new signed entry rather than dropping them in a modeled rewrite. A supplied NewDocument is deep-copied once at the start of the update, so hashing, signing, the published log, and the returned document all reflect a single snapshot — a caller collection that changes contents between reads cannot publish bytes that differ from what was signed.
Resolution enforces did:webvh's per-entry SCID identity: the SCID segment of every validated entry's state.id must match the DID's SCID (only host/path may differ under portability). A signed log whose genesis or an intermediate entry claims a foreign SCID, or omits state.id, is rejected as invalidDidLog; historical resolution validates only the prefix through the selected version.
Verification work per entry is bounded by a controller-proof limit (default 8). Direct consumers can set it with new DidWebVhMethod(client, logger: null, maxControllerProofsPerEntry: 16) and DI consumers with builder.AddDidWebVh(httpClientOptions: null, maxControllerProofsPerEntry: 16); raising it increases attacker-controlled canonicalization and signature work. The existing two-argument constructor and one-argument registration call remain source- and binary-compatible. With DidResolutionOptions.IncludeLog, latest resolution exposes the fully validated log, while historical resolution exposes only the validated prefix through the selected version.
var updatedDoc = result.DidDocument with
{
Service = [ result.DidDocument.Service![0], new Service
{
Id = $"{result.Did}#api",
Type = "ApiEndpoint",
ServiceEndpoint = ServiceEndpointValue.FromUri("https://api.example.com/v1")
}]
};
var updateResult = await didWebVh.UpdateAsync(result.Did.Value, new DidWebVhUpdateOptions
{
CurrentLogContent = Encoding.UTF8.GetBytes((string)result.Artifacts!["did.jsonl"]),
SigningKey = signer,
NewDocument = updatedDoc
});
// Re-host the updated did.jsonlThe result carries authorization-change evidence for method-agnostic callers.
AuthorizationChange reports whether any authorization material changed
(updateKeys / nextKeyHashes / witness config); UpdateKeyChange
reports whether the effective updateKeys set itself changed, including while the
resulting state keeps pre-rotation active. RevealedUpdateKeys is the complete set
eligible to authorize the entry just appended: the prior effective keys when prior
commitments did not govern that entry (including an entry that activates pre-rotation
for its successor), or the current entry's explicit keys when prior commitments did
govern it and every member passed commitment validation. Eligibility does not mean
every listed key signed; one eligible update key can authorize the proof.
EffectiveUpdateKeys is forward-looking and lists the keys authorized to sign the
next log entry. Do not coalesce the two nullable key properties into a generic
post-change key set; they answer different current-entry and next-entry questions.
For an exclusive rotation or authorization postcondition, require the expected status
and compare the applicable complete key set for equality; membership checks alone
would accept unexpected extra keys. Both statuses and nullable key sets fail closed for
methods that report no evidence. The did:webvh driver reports
UpdateKeyChange == Changed or Unchanged and RevealedUpdateKeys even during
continuous pre-rotation, but keeps EffectiveUpdateKeys null when the resulting state
has non-empty nextKeyHashes: commitments are hashes, so they cannot reveal the keys
that will authorize the next entry. An entry that sets nextKeyHashes: [] ends
pre-rotation after that entry and restores concrete next-entry evidence.
var nextKey = keyGen.Generate(KeyType.Ed25519);
var commitment = PreRotationManager.ComputeKeyCommitment(nextKey.MultibasePublicKey);
var result = await didWebVh.CreateAsync(new DidWebVhCreateOptions
{
Domain = "example.com",
UpdateKey = signer,
PreRotationCommitments = [commitment]
});Pre-rotation commits to the next update key hash at creation time. The next entry must explicitly
place the committed key in updateKeys, carry nextKeyHashes, and be signed by that committed key.
This prevents a compromised current key from rotating control to an uncommitted key. Commitments
are did:webvh v1.0 bare-base58btc encodings of complete SHA-256 multihashes (Qm..., with no
multibase z prefix).
await didWebVh.DeactivateAsync(result.Did.Value, new DidWebVhDeactivateOptions
{
CurrentLogContent = logContent,
SigningKey = signer
});using NetDid.Core.Serialization;
// JSON-LD (includes @context)
string jsonLd = DidDocumentSerializer.Serialize(doc, DidContentTypes.JsonLd);
// Plain JSON (omits @context)
string json = DidDocumentSerializer.Serialize(doc, DidContentTypes.Json);
// Deserialize
DidDocument restored = DidDocumentSerializer.Deserialize(jsonLd, DidContentTypes.JsonLd);using NetCrypto;
var store = new InMemoryKeyStore(keyGen, crypto);
var info = await store.GenerateAsync("my-signing-key", KeyType.Ed25519);
ISigner signer = await store.CreateSignerAsync("my-signing-key");
byte[] sig = await signer.SignAsync("payload"u8.ToArray());Build DID Documents programmatically with the fluent API:
using NetDid.Core.Model;
var doc = new DidDocumentBuilder("did:example:123")
.AddVerificationMethod(vm => vm
.WithId("#key-1")
.WithType("Multikey")
.WithMultibasePublicKey("z6MkSigningKey"))
.AddVerificationMethod(vm => vm
.WithId("#key-2")
.WithType("Multikey")
.WithMultibasePublicKey("z6LSKeyAgree"))
.AddAuthentication("#key-1")
.AddAssertionMethod("#key-1")
.AddKeyAgreement("#key-2")
.AddService(svc => svc
.WithId("#pds")
.WithType("PersonalDataStore")
.WithEndpoint("https://example.com/pds"))
.Build();The builder auto-sets controller to the document id when not explicitly specified. Validates required fields (Id, Type) at Build() time.
For ASP.NET Core or any Microsoft DI host, use the builder pattern to register all methods in one call:
using NetDid.Extensions.DependencyInjection;
services.AddNetDid(builder =>
{
builder.AddDidKey();
builder.AddDidPeer();
builder.AddDidWebVh();
builder.AddCaching(TimeSpan.FromMinutes(15));
});Then inject IDidManager or IDidResolver:
public class MyService(IDidManager manager)
{
public async Task CreateIdentity()
{
var result = await manager.CreateAsync(new DidKeyCreateOptions
{
KeyType = KeyType.Ed25519
});
// Resolve any DID — auto-routes to the correct method
var resolved = await manager.ResolveAsync(result.Did.Value);
}
}NetDid is built around a small set of core interfaces (in NetDid.Core):
| Interface | Purpose |
|---|---|
IDidManager |
Unified DID lifecycle manager — routes CRUD operations across registered methods |
IDidMethod |
Single DID method implementation (create, resolve, update, deactivate) |
IDidResolver |
Standalone DID resolution (for consumers who only need to resolve) |
The cryptographic interfaces are provided by NetCrypto (the NetCrypto namespace):
| Interface | Purpose |
|---|---|
IKeyStore |
Pluggable key storage — swap in HSM, vault, or cloud KMS |
ISigner |
Signing abstraction — works with in-memory keys or secure enclaves |
IKeyGenerator |
Key pair generation and derivation for all supported key types |
ICryptoProvider |
Low-level sign, verify, and key agreement operations |
IBbsCryptoProvider |
BBS+ multi-message signatures with selective disclosure |
DID string
--> CompositeDidResolver (routes by method name)
--> CachingDidResolver (IMemoryCache + TTL)
--> IDidMethod.ResolveAsync()
--> DidDocument
DefaultDidUrlDereferencer implements the W3C DID Core section 7.2 algorithm: parse URL, resolve the base DID, then select resources by fragment, service ID or type query, or path. Supports verificationRelationship filtering and text/uri-list redirect with RFC 3986 URL resolution.
netdid/
├── src/
│ ├── NetDid.Core/ # Core abstractions, DID model, encoding, serialization
│ ├── NetDid.Method.Key/ # did:key method
│ ├── NetDid.Method.Peer/ # did:peer method (numalgo 0, 2, 4)
│ ├── NetDid.Method.WebVh/ # did:webvh method (full CRUD)
│ └── NetDid.Extensions.DependencyInjection/ # Microsoft DI integration
├── tests/
│ ├── NetDid.Core.Tests/ # 370 unit tests
│ ├── NetDid.Method.Key.Tests/ # 44 tests
│ ├── NetDid.Method.Peer.Tests/ # 40 tests
│ ├── NetDid.Method.WebVh.Tests/ # 130 tests
│ ├── NetDid.Tests.W3CConformance/ # 175 W3C conformance tests
│ └── NetDid.Extensions.DependencyInjection.Tests/ # 11 tests
├── samples/
│ ├── NetDid.Samples.DidKey/ # did:key usage examples
│ ├── NetDid.Samples.DidPeer/ # did:peer usage examples
│ ├── NetDid.Samples.DidWebVh/ # did:webvh CRUD examples
│ └── NetDid.Samples.DependencyInjection/ # DI registration pattern
└── netdid.sln
dotnet builddotnet testdotnet run --project samples/NetDid.Samples.DidKey
dotnet run --project samples/NetDid.Samples.DidPeer
dotnet run --project samples/NetDid.Samples.DidWebVh
dotnet run --project samples/NetDid.Samples.DependencyInjectionNetDid is developed in four phases (see NetDidPRD.md for full details):
| Phase | Scope | Status |
|---|---|---|
| I | Core Foundation — DID Document model, crypto primitives, encoding, serialization, resolver infrastructure | Complete |
| II | did:key and did:peer method implementations |
Complete |
| III | did:webvh method implementation |
Complete |
| IV | did:ethr method implementation |
Planned |
NetDid targets the following specifications:
| Specification | Version | Status | Reference |
|---|---|---|---|
| W3C Decentralized Identifiers (DIDs) | v1.0 | W3C Recommendation (2022-07-19) | w3.org/TR/did-core |
| did:key | Latest | W3C CCG Final | w3c-ccg.github.io/did-method-key |
| did:peer | 2.0 | DIF Spec | identity.foundation/peer-did-method-spec |
| did:webvh | 1.0 | DIF Recommended | identity.foundation/didwebvh |
| Data Integrity (eddsa-jcs-2022) | — | W3C Candidate Recommendation | w3.org/TR/vc-di-eddsa |
| BBS Signatures | draft-10 | IETF CFRG Draft | draft-irtf-cfrg-bbs-signatures |
| JSON Canonicalization (JCS) | RFC 8785 | IETF Proposed Standard | rfc-editor.org/rfc/rfc8785 |
NetDid is fully conformant with W3C Decentralized Identifiers (DIDs) v1.0 (W3C Recommendation, 2022-07-19). All 182 conformance tests pass across the three implemented methods:
| Method | Tests |
|---|---|
| did:key | 57/57 |
| did:peer | 67/67 |
| did:webvh | 58/58 |
See w3c-conformance-report.md for the full report.
See CONTRIBUTING.md for setup instructions, code conventions, and how to add new DID methods or key types.
See SECURITY.md for the security policy and how to report vulnerabilities.
Licensed under the Apache License 2.0.