Self-Sovereign Identity: Implementing Decentralized Identity (DID) in Java

Decentralized Identity (DID) is a new paradigm that gives individuals and organizations control over their digital identities without relying on central authorities. Built on blockchain and distributed ledger technology, DIDs enable verifiable, self-sovereign identity that is portable, privacy-preserving, and interoperable. For Java developers, this represents an emerging frontier in security, identity management, and Web3 applications.

This article provides a comprehensive guide to understanding DIDs and implementing DID-related functionality in Java applications.


1. Core Concepts of Decentralized Identity

What is a DID?

A Decentralized Identifier (DID) is a new type of identifier that is:

  • Self-sovereign: Controlled by the identity owner
  • Decentralized: No central registration authority
  • Persistent: Never reassigned to another entity
  • Cryptographically verifiable: Using public key cryptography
  • Resolvable: To DID Documents containing public keys and service endpoints

DID Components:

  • DID: did:method:method-specific-identifier
  • DID Document: JSON-LD document containing public keys, authentication methods, and service endpoints
  • Verifiable Credentials: Digital equivalents of physical credentials (driver's license, diploma)
  • Verifiable Presentations: Data derived from credentials shared with verifiers

2. Java Libraries for DID Implementation

Several Java libraries support DID and Verifiable Credentials:

Dependencies:

<dependencies>
<!-- For cryptographic operations -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk15on</artifactId>
<version>1.70</version>
</dependency>
<!-- JSON processing -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.2</version>
</dependency>
<!-- For JWT (if using JWT-VC) -->
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>9.31</version>
</dependency>
</dependencies>

3. Creating DIDs and DID Documents

Here's how to create DID documents and manage keys in Java:

Basic DID Document Structure:

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.util.UUID;
public class DIDCreator {
private static final ObjectMapper mapper = new ObjectMapper();
public static class DIDDocument {
private final String did;
private final KeyPair keyPair;
private final ObjectNode document;
public DIDDocument(String method, String methodSpecificId) {
this.did = "did:" + method + ":" + methodSpecificId;
this.keyPair = generateKeyPair();
this.document = createDIDDocument();
}
private KeyPair generateKeyPair() {
try {
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("Ed25519");
return keyGen.generateKeyPair();
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("Ed25519 not supported", e);
}
}
private ObjectNode createDIDDocument() {
ObjectNode doc = mapper.createObjectNode();
// Core DID document structure
doc.put("@context", "https://www.w3.org/ns/did/v1");
doc.put("id", this.did);
// Add verification methods
ArrayNode verificationMethods = mapper.createArrayNode();
ObjectNode vm = mapper.createObjectNode();
vm.put("id", this.did + "#keys-1");
vm.put("type", "Ed25519VerificationKey2020");
vm.put("controller", this.did);
vm.put("publicKeyMultibase", encodePublicKey(keyPair.getPublic()));
verificationMethods.add(vm);
doc.set("verificationMethod", verificationMethods);
// Add authentication
ArrayNode authentication = mapper.createArrayNode();
authentication.add(this.did + "#keys-1");
doc.set("authentication", authentication);
return doc;
}
private String encodePublicKey(java.security.PublicKey publicKey) {
// Simplified encoding - in production use proper multibase encoding
byte[] encoded = publicKey.getEncoded();
return "z" + java.util.Base64.getUrlEncoder().withoutPadding().encodeToString(encoded);
}
public String getDid() { return did; }
public KeyPair getKeyPair() { return keyPair; }
public String getDocumentJson() { return document.toString(); }
public ObjectNode getDocument() { return document; }
}
public static DIDDocument createExampleDID() {
String methodSpecificId = UUID.randomUUID().toString().replace("-", "").substring(0, 16);
return new DIDDocument("example", methodSpecificId);
}
}

4. Verifiable Credentials Implementation

Verifiable Credentials are tamper-evident credentials that can be cryptographically verified:

VC Data Model:

import com.fasterxml.jackson.databnode.ObjectNode;
import java.time.Instant;
import java.util.*;
public class VerifiableCredential {
private final ObjectNode credential;
private final ObjectMapper mapper = new ObjectMapper();
public VerifiableCredential(String issuerDid, String subjectDid, Map<String, Object> claims) {
this.credential = createCredential(issuerDid, subjectDid, claims);
}
private ObjectNode createCredential(String issuerDid, String subjectDid, Map<String, Object> claims) {
ObjectNode vc = mapper.createObjectNode();
// Core VC structure
ArrayNode contexts = mapper.createArrayNode();
contexts.add("https://www.w3.org/2018/credentials/v1");
contexts.add("https://www.w3.org/2018/credentials/examples/v1");
vc.set("@context", contexts);
vc.put("id", "urn:uuid:" + UUID.randomUUID().toString());
vc.put("type", "VerifiableCredential");
vc.put("issuer", issuerDid);
vc.put("issuanceDate", Instant.now().toString());
// Credential subject
ObjectNode credentialSubject = mapper.createObjectNode();
credentialSubject.put("id", subjectDid);
// Add claims
for (Map.Entry<String, Object> entry : claims.entrySet()) {
if (entry.getValue() instanceof String) {
credentialSubject.put(entry.getKey(), (String) entry.getValue());
} else if (entry.getValue() instanceof Integer) {
credentialSubject.put(entry.getKey(), (Integer) entry.getValue());
} else if (entry.getValue() instanceof Boolean) {
credentialSubject.put(entry.getKey(), (Boolean) entry.getValue());
}
}
vc.set("credentialSubject", credentialSubject);
return vc;
}
public ObjectNode getCredential() { return credential; }
public String toJson() { return credential.toString(); }
// Create a university degree credential example
public static VerifiableCredential createDegreeCredential(String universityDid, String studentDid) {
Map<String, Object> claims = new HashMap<>();
claims.put("degree", "Bachelor of Science");
claims.put("major", "Computer Science");
claims.put("university", "Example University");
claims.put("graduationYear", 2024);
claims.put("honors", true);
return new VerifiableCredential(universityDid, studentDid, claims);
}
}

5. Cryptographic Signing and Verification

Cryptographic operations are essential for DID functionality:

Digital Signatures for VCs:

import java.security.*;
import java.util.Base64;
public class CryptoService {
private final KeyPair keyPair;
public CryptoService(KeyPair keyPair) {
this.keyPair = keyPair;
}
/**
* Sign a Verifiable Credential
*/
public String signCredential(String credentialJson) {
try {
Signature signature = Signature.getInstance("Ed25519");
signature.initSign(keyPair.getPrivate());
signature.update(credentialJson.getBytes());
byte[] digitalSignature = signature.sign();
return Base64.getUrlEncoder().withoutPadding().encodeToString(digitalSignature);
} catch (Exception e) {
throw new RuntimeException("Signing failed", e);
}
}
/**
* Verify a signed credential
*/
public boolean verifySignature(String credentialJson, String signatureBase64, PublicKey publicKey) {
try {
Signature signature = Signature.getInstance("Ed25519");
signature.initVerify(publicKey);
signature.update(credentialJson.getBytes());
byte[] signatureBytes = Base64.getUrlDecoder().decode(signatureBase64);
return signature.verify(signatureBytes);
} catch (Exception e) {
throw new RuntimeException("Verification failed", e);
}
}
/**
* Create a signed Verifiable Credential with proof
*/
public ObjectNode createSignedCredential(VerifiableCredential vc, String did) {
ObjectNode credential = vc.getCredential().deepCopy();
// Create proof
ObjectNode proof = mapper.createObjectNode();
proof.put("type", "Ed25519Signature2020");
proof.put("created", Instant.now().toString());
proof.put("verificationMethod", did + "#keys-1");
proof.put("proofPurpose", "assertionMethod");
// Sign the credential (without proof)
String credentialWithoutProof = credential.toString();
String signature = signCredential(credentialWithoutProof);
proof.put("signatureValue", signature);
// Add proof to credential
credential.set("proof", proof);
return credential;
}
}

6. Complete DID System Implementation

Here's a complete implementation that ties everything together:

DID Manager:

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.security.KeyPair;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
public class DIDManager {
private final Map<String, DIDCreator.DIDDocument> didRegistry;
private final ObjectMapper mapper;
private final CryptoService cryptoService;
public DIDManager() {
this.didRegistry = new HashMap<>();
this.mapper = new ObjectMapper();
}
/**
* Create a new DID for an entity
*/
public String createDID(String method, String entityName) {
String methodSpecificId = generateMethodSpecificId(entityName);
DIDCreator.DIDDocument didDoc = new DIDCreator.DIDDocument(method, methodSpecificId);
didRegistry.put(didDoc.getDid(), didDoc);
this.cryptoService = new CryptoService(didDoc.getKeyPair());
return didDoc.getDid();
}
private String generateMethodSpecificId(String entityName) {
String cleanName = entityName.replaceAll("[^a-zA-Z0-9]", "").toLowerCase();
String uuid = UUID.randomUUID().toString().replace("-", "").substring(0, 8);
return cleanName + "-" + uuid;
}
/**
* Resolve a DID to its document
*/
public String resolveDID(String did) {
DIDCreator.DIDDocument doc = didRegistry.get(did);
return doc != null ? doc.getDocumentJson() : null;
}
/**
* Issue a verifiable credential
*/
public ObjectNode issueCredential(String issuerDid, String subjectDid, 
Map<String, Object> claims) {
VerifiableCredential vc = new VerifiableCredential(issuerDid, subjectDid, claims);
return cryptoService.createSignedCredential(vc, issuerDid);
}
/**
* Verify a verifiable credential
*/
public boolean verifyCredential(ObjectNode signedCredential) {
try {
// Extract proof and remove it for verification
ObjectNode proof = (ObjectNode) signedCredential.remove("proof");
String verificationMethod = proof.get("verificationMethod").asText();
String signature = proof.get("signatureValue").asText();
// Resolve issuer's public key
String issuerDid = verificationMethod.split("#")[0];
DIDCreator.DIDDocument issuerDoc = didRegistry.get(issuerDid);
if (issuerDoc == null) {
throw new RuntimeException("Issuer DID not found: " + issuerDid);
}
// Verify signature
String credentialWithoutProof = signedCredential.toString();
return cryptoService.verifySignature(credentialWithoutProof, signature, 
issuerDoc.getKeyPair().getPublic());
} catch (Exception e) {
throw new RuntimeException("Credential verification failed", e);
}
}
/**
* Create a verifiable presentation (selective disclosure)
*/
public ObjectNode createPresentation(String holderDid, ObjectNode... credentials) {
ObjectNode vp = mapper.createObjectNode();
ArrayNode contexts = mapper.createArrayNode();
contexts.add("https://www.w3.org/2018/credentials/v1");
vp.set("@context", contexts);
vp.put("type", "VerifiablePresentation");
vp.put("holder", holderDid);
ArrayNode verifiableCredential = mapper.createArrayNode();
for (ObjectNode credential : credentials) {
verifiableCredential.add(credential);
}
vp.set("verifiableCredential", verifiableCredential);
return cryptoService.createSignedCredential(
new VerifiableCredential(holderDid, holderDid, Map.of()), holderDid);
}
}

7. Usage Example: University Credential System

Here's a practical example of using DIDs for academic credentials:

public class UniversityDIDExample {
public static void main(String[] args) {
DIDManager didManager = new DIDManager();
// Create DIDs for university and student
String universityDid = didManager.createDID("example", "ExampleUniversity");
String studentDid = didManager.createDID("example", "JohnDoe");
System.out.println("University DID: " + universityDid);
System.out.println("Student DID: " + studentDid);
// Issue a degree credential
Map<String, Object> degreeClaims = new HashMap<>();
degreeClaims.put("degreeType", "Bachelor");
degreeClaims.put("fieldOfStudy", "Computer Science");
degreeClaims.put("graduationDate", "2024-05-15");
degreeClaims.put("gpa", 3.8);
ObjectNode degreeCredential = didManager.issueCredential(
universityDid, studentDid, degreeClaims);
System.out.println("Degree Credential: " + degreeCredential.toPrettyString());
// Verify the credential
boolean isValid = didManager.verifyCredential(degreeCredential);
System.out.println("Credential valid: " + isValid);
// Student creates a verifiable presentation for job application
ObjectNode presentation = didManager.createPresentation(studentDid, degreeCredential);
System.out.println("Verifiable Presentation: " + presentation.toPrettyString());
}
}

8. Integration with Blockchain (Conceptual)

For production systems, DIDs are typically anchored on blockchain:

Ethereum DID Registry (Concept):

import org.web3j.crypto.Credentials;
import org.web3j.protocol.Web3j;
import org.web3j.protocol.http.HttpService;
import org.web3j.tx.gas.DefaultGasProvider;
public class EthereumDIDRegistry {
private final Web3j web3j;
private final Credentials credentials;
public EthereumDIDRegistry(String ethereumUrl, String privateKey) {
this.web3j = Web3j.build(new HttpService(ethereumUrl));
this.credentials = Credentials.create(privateKey);
}
/**
* Register a DID document hash on Ethereum
*/
public String registerDID(String did, String documentHash) {
// This would interact with a DID registry smart contract
// Simplified example:
try {
// DIDRegistry contract = DIDRegistry.deploy(web3j, credentials, 
//                                         new DefaultGasProvider()).send();
// String txHash = contract.register(did, documentHash).send().getTransactionHash();
// return txHash;
return "0x" + "mock-transaction-hash";
} catch (Exception e) {
throw new RuntimeException("DID registration failed", e);
}
}
/**
* Resolve DID from blockchain
*/
public String resolveDIDFromBlockchain(String did) {
// Query the smart contract for DID document hash
// Then fetch the actual document from IPFS or other decentralized storage
return "resolved-did-document";
}
}

9. Best Practices and Security Considerations

  1. Key Management: Use Hardware Security Modules (HSM) for production private keys
  2. Key Rotation: Implement regular key rotation procedures
  3. Privacy: Use zero-knowledge proofs for selective disclosure
  4. Interoperability: Follow W3C DID and VC specifications
  5. Revocation: Implement credential revocation mechanisms
  6. Storage: Use secure, decentralized storage for DID documents

Security Service:

public class DIDSecurityService {
public static void validateDIDFormat(String did) {
if (did == null || !did.matches("^did:[a-z0-9]+:[a-zA-Z0-9._-]+$")) {
throw new IllegalArgumentException("Invalid DID format: " + did);
}
}
public static void ensureSecureContext() {
// Check if running in secure environment
if (System.getenv("INSECURE_ENVIRONMENT") != null) {
throw new SecurityException("DID operations not allowed in insecure environment");
}
}
}

10. Testing DID Implementations

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class DIDManagerTest {
@Test
public void testDIDCreationAndResolution() {
DIDManager manager = new DIDManager();
String did = manager.createDID("test", "alice");
assertNotNull(did);
assertTrue(did.startsWith("did:test:alice-"));
String document = manager.resolveDID(did);
assertNotNull(document);
assertTrue(document.contains("\"id\":\"" + did + "\""));
}
@Test
public void testCredentialVerification() {
DIDManager manager = new DIDManager();
String issuerDid = manager.createDID("test", "university");
String subjectDid = manager.createDID("test", "student");
ObjectNode credential = manager.issueCredential(issuerDid, subjectDid, 
Map.of("degree", "BSc Computer Science"));
assertTrue(manager.verifyCredential(credential));
}
}

Conclusion

Decentralized Identity represents a fundamental shift in how we manage digital identity, moving from centralized authorities to self-sovereign, user-controlled identities. Java provides excellent capabilities for implementing DID systems through its robust cryptographic libraries, JSON processing capabilities, and strong typing.

While this article covers the fundamentals, the DID ecosystem is rapidly evolving with new methods, standards, and best practices. For production systems, consider using established libraries like DID Java or Verifiable Credentials Java that provide more comprehensive implementations of the W3C standards.

The combination of DIDs, Verifiable Credentials, and Java's enterprise capabilities creates a powerful foundation for building the next generation of secure, privacy-preserving identity systems.


Further Reading:

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper