Decentralized Identity (DID)
Resolving Identifiers: How to Implement W3C DID Document Discovery
Learn how to transform a DID string into a standardized JSON-LD document using resolution methods like did:web, did:key, and did:ethr.
In this article
The Architecture of Decentralized Identity Resolution
Digital identity has historically relied on central authorities to map a human-readable identifier to its underlying technical metadata. In a traditional system, your email address or username serves as a pointer to a record in a private database managed by a service provider. When that provider goes offline or decides to revoke your access, your identity effectively ceases to exist within that ecosystem.
Decentralized Identifiers (DIDs) solve this by decoupling the identifier from the storage provider. A DID is a simple string that serves as a globally unique URI that does not require a central registration authority. However, a raw string like did:example:123456789 is not useful on its own for cryptographic operations.
The resolution process is the bridge between this static identifier and the actionable cryptographic material required to verify signatures or encrypt data. Resolution transforms a DID string into a standardized JSON-LD object known as a DID Document. This document contains public keys, service endpoints, and verification relationships that define how the identity can be used.
Understanding resolution requires looking at the system as a specialized form of discovery, similar to how DNS maps domain names to IP addresses. Unlike DNS, however, DID resolution focuses on establishing a foundation of trust through cryptographic proofs rather than administrative records. This shift ensures that the identity holder remains in control of their digital presence across different platforms.
Resolution is the most critical security boundary in decentralized systems because if an attacker can manipulate the resolution process, they can substitute their own public keys for those of the legitimate identity owner.
A DID Document must follow strict W3C standards to ensure interoperability across different software libraries and programming languages. By adhering to a common JSON-LD structure, developers can build applications that interact with identities created on entirely different blockchain networks. This interoperability is what allows a user to carry their reputation from a professional network to a financial service without creating a new account.
Anatomy of a DID Document
Every resolved DID Document must contain several core components to be functional within a production environment. The most important field is the id property, which must match the original DID string that was resolved to prevent document injection attacks. Following this, the verificationMethod array lists the public keys associated with the identity and specifies their format, such as JsonWebKey2020.
Beyond raw keys, the document defines verification relationships like authentication or assertionMethod. These relationships tell the relying party which keys are authorized for specific actions, such as logging in or signing a legal document. This granular control allows an identity owner to use different keys for different devices while maintaining a single identifier.
Service endpoints are another vital part of the document, providing URLs where the identity owner can be reached for further interaction. This might include an encrypted messaging inbox or a credential issuance service. By including these in the resolution result, developers can build automated workflows that discover how to communicate with an entity just by knowing its DID.
The Role of the Resolver and Driver
The software responsible for performing the transformation from string to document is called a DID Resolver. A resolver acts as an orchestrator that identifies the specific method used in the DID string and routes the request to the appropriate logic. For example, a resolver seeing the prefix did:web will handle the request differently than one seeing did:ethr.
Inside the resolver, specific plugins called Drivers handle the heavy lifting for each individual method. Each driver contains the logic necessary to interact with a specific ledger, database, or network protocol. This modular architecture allows a single application to support dozens of different identity methods without changing its core business logic.
When an application requests a resolution, the resolver returns more than just the DID Document. It also provides resolution metadata, which includes information about the timing of the request and the version of the document retrieved. This metadata is essential for debugging and for ensuring that the application is working with the most recent state of the identity.
Implementing Method-Specific Resolution Logic
Each DID method defines its own unique path to discovery and document generation based on its underlying infrastructure. Methods like did:key are entirely self-contained and require no external network calls, making them ideal for ephemeral sessions or simple encryption tasks. In contrast, methods like did:web rely on the existing security of the Domain Name System and Transport Layer Security.
Choosing the right resolution method depends on the specific requirements for persistence, security, and cost. If your application requires high-speed resolution with zero infrastructure costs, did:key is the strongest candidate. If you need to anchor identity in a legally recognized corporate domain, did:web provides a familiar path for IT departments to manage identity through web servers.
The implementation of a resolver typically involves initializing a library with various drivers. Developers must ensure that these drivers are configured to point to reliable network nodes or gateways. A failure in the underlying network connection for a driver will result in a resolution error, which can prevent users from accessing your application.
1import { Resolver } from 'did-resolver';
2import { getResolver as getWebResolver } from 'web-did-resolver';
3import { getResolver as getKeyResolver } from 'key-did-resolver';
4
5// Initialize the resolver with multiple drivers for different methods
6const webResolver = getWebResolver();
7const keyResolver = getKeyResolver();
8
9const universalResolver = new Resolver({
10 ...webResolver,
11 ...keyResolver
12});
13
14async function resolveIdentity(didString) {
15 try {
16 // The resolve method determines which driver to use based on the DID prefix
17 const result = await universalResolver.resolve(didString);
18
19 if (result.didDocument === null) {
20 throw new Error(`Resolution failed: ${result.didResolutionMetadata.error}`);
21 }
22
23 return result.didDocument;
24 } catch (error) {
25 console.error('Network or parsing error during DID resolution', error);
26 return null;
27 }
28}The code above demonstrates how a single entry point can handle multiple identity formats seamlessly. By wrapping different resolvers into a unified interface, the application remains decoupled from the evolution of specific identity protocols. This pattern is essential for maintaining long-term software stability as new DID methods emerge and older ones are deprecated.
Resolution performance is a significant consideration for enterprise applications serving thousands of users. Methods that require fetching data from a blockchain can introduce latency that impacts the user experience. Implementing a caching layer at the resolver level can mitigate these delays, provided that the cache respect the Time-To-Live values and revocation statuses of the identity documents.
Static Resolution with did:key
The did:key method is unique because it is purely algorithmic and does not involve a registry. The DID string itself contains a multibase-encoded public key, which the resolver expands into a full DID Document. This makes it impossible to revoke or update a did:key identifier, as any change to the key results in an entirely different DID.
Developers often use did:key for short-lived interactions where the overhead of blockchain transactions is unnecessary. For instance, a peer-to-peer chat application might generate a did:key for each session to facilitate end-to-end encryption. Because there is no external dependency, resolution is instantaneous and offline-capable.
Web-Based Resolution with did:web
The did:web method repurposes the internet's existing infrastructure by mapping a domain name to a DID Document hosted at a specific path. A DID like did:web:example.com resolves to a JSON-LD file located at https://example.com/.well-known/did.json. This allows organizations to leverage their existing security policies and domain ownership to manage their digital identity.
While did:web is easy to implement, it reintroduces some centralization because the domain owner has ultimate control over the document. If the domain name expires or the server is compromised, the identity's integrity is at risk. It serves as a pragmatic bridge for companies transitioning from traditional certificate-based identity to decentralized models.
Anchoring Trust in Smart Contracts: did:ethr
Ethereum-based identity methods like did:ethr move the source of truth from private servers to a public, immutable ledger. In this model, the DID Document is not stored as a static file but is dynamically reconstructed by querying a smart contract on the blockchain. The contract acts as a decentralized registry where users can register their public keys and service endpoints.
The primary advantage of did:ethr is its resilience and the ability to update the identity without changing the identifier. A user can rotate their signing keys or add new verification methods by sending a transaction to the Ethereum network. This ensures that the identity can evolve over time while maintaining a consistent history of interactions.
When a resolver encounters a did:ethr identifier, it communicates with an Ethereum node to fetch the current state of that identity from the registry contract. It processes the log of transactions associated with that specific address to build the final document. This process guarantees that the information retrieved is globally consistent and has not been tampered with by an intermediary.
1import { Resolver } from 'did-resolver';
2import { getResolver } from 'ethr-did-resolver';
3
4// Configuration for the Ethereum provider and registry
5const providerConfig = {
6 networks: [
7 {
8 name: 'mainnet',
9 rpcUrl: 'https://mainnet.infura.io/v3/YOUR_PROJECT_ID',
10 registry: '0xdca7ef34e1f171997d3498380fb6ca739bab926b' // Canonical Registry Address
11 }
12 ]
13};
14
15// Register the ethr driver
16const ethrResolver = getResolver(providerConfig);
17const resolver = new Resolver(ethrResolver);
18
19async function getEthereumIdentity(ethAddress) {
20 const did = `did:ethr:${ethAddress}`;
21 const result = await resolver.resolve(did);
22
23 // Accessing the verification methods from the resolved document
24 const keys = result.didDocument.verificationMethod;
25 console.log('Public keys for this Ethereum identity:', keys);
26 return result.didDocument;
27}Working with did:ethr requires developers to handle the complexities of blockchain networking, such as gas fees for updates and block confirmation times. However, for the purpose of resolution, the process is read-only and does not require the user to spend any cryptocurrency. This makes it a powerful tool for verifiable data exchange in financial and supply chain applications.
- Scalability: High, as resolution is a read-only operation against a blockchain node.
- Persistence: Permanent, since the identifier is tied to an Ethereum address that never expires.
- Control: Absolute, as only the owner of the Ethereum private key can modify the document metadata.
Handling Event Logs and State
The ethr-did-resolver library does more than just fetch a single value; it must interpret a series of events emitted by the smart contract. These events include key additions, attribute changes, and ownership transfers. By replaying these events in chronological order, the resolver determines the precise state of the DID Document at the current block height.
This event-based model allows for complex identity management features like key rotation. If a user loses their phone, they can use a recovery key to revoke the compromised device's public key on the blockchain. Subsequent resolutions will automatically reflect this revocation, preventing the old key from being used for unauthorized authentication.
Production Considerations and Security Patterns
Moving identity resolution from a development environment to a production system introduces several challenges related to reliability and data integrity. A resolver is a high-traffic component that sits on the critical path of every user interaction. If resolution fails, your users cannot log in, and your system cannot verify any incoming data.
Security is paramount when configuring your resolver's drivers. For web-based methods, you must enforce strict HTTPS and validate the TLS certificate chain to prevent man-in-the-middle attacks. For blockchain methods, you should rely on your own managed nodes or trusted providers like Infura or Alchemy rather than public gateways that might inject malicious data.
Caching is a double-edged sword in decentralized identity. While it significantly improves performance and reduces network costs, an aggressive cache might serve an outdated DID Document that contains revoked keys. Developers should implement a tiered caching strategy where documents are cached for a short duration, but high-value transactions trigger a mandatory refresh to ensure the latest keys are used.
Error handling in resolution must be robust enough to distinguish between different failure modes. A DID that does not exist should return a 'notFound' error, while a network failure should be treated as a temporary issue. Providing clear feedback to the user or the calling system allows for better retry logic and improved system resilience during network outages.
Metadata management is often overlooked but provides essential context for the resolution result. The DID Resolution Metadata should include a timestamp and a content hash of the document retrieved. This allows the application to maintain an audit trail of which version of an identity was used to verify a specific piece of data, which is vital for legal compliance and forensics.
Mitigating Resolution Latency
Network latency is the most common bottleneck in DID resolution, especially when dealing with distributed ledgers. Developers can use techniques like pre-fetching, where the application resolves identities in the background before they are needed. This is particularly effective in workflows with predictable steps, such as a multi-stage approval process.
Another approach is to use a Universal Resolver service, which is a hosted environment that manages multiple drivers and provides a single API endpoint. While this simplifies client-side code, it introduces a new dependency and a potential point of failure. Teams must weigh the convenience of a hosted resolver against the privacy and security benefits of running a local instance.
Verifying Document Integrity
Once a document is resolved, the application should perform a final integrity check before using the keys. This includes verifying that the document is well-formed JSON-LD and that all mandatory fields are present. Some advanced DID methods include cryptographic proofs within the document itself, allowing the resolver to verify that the registry actually produced the returned data.
Implementing a policy engine can further enhance security by defining which DIDs your application is willing to trust. For example, you might only accept resolutions from specific methods like did:ethr and did:web while rejecting did:key for high-value transactions. This layer of abstraction ensures that your business logic remains protected from the risks associated with less secure identity protocols.
