How to Build a Digital Credential Wallet
| Audience | Developers building wallet applications for storing, managing, and presenting verifiable credentials. |
| Purpose | Walk through the wallet plugin architecture - credential storage, key management, format plugins, and OID4VC protocol adapters - with step-by-step code for common operations using SdJwt.Net.Wallet. |
| Scope | Wallet setup, credential storage/query, selective disclosure presentation, validation, custom storage/key providers, and OID4VC adapters. Out of scope: EUDIW-specific compliance (see EUDI Wallet Integration). |
| Success criteria | Reader can create a wallet, store credentials, present selective claims to a verifier, implement custom secure storage and HSM key management, and wire OID4VCI/OID4VP adapters. |
Key Decisions
| Decision | Options | Guidance |
|---|---|---|
| Credential storage? | In-memory, Secure storage, Cloud | Secure storage for production |
| Key management? | Software, Hardware, HSM | Hardware-backed for production |
| Credential formats? | SD-JWT VC, mdoc, JWT | SD-JWT VC for selective disclosure |
| Protocol adapters? | OID4VCI, OID4VP, custom | OID4VCI for issuance, OID4VP for present |
Prerequisites
dotnet add package SdJwt.Net.Wallet
Wallet Architecture Overview
The wallet consists of several key components:
- GenericWallet - Main coordinator for wallet operations
- ICredentialStore - Storage abstraction for credentials
- IKeyManager - Cryptographic key management
- ICredentialFormatPlugin - Format-specific handlers (SD-JWT VC, mdoc)
- Protocol Adapters - OID4VCI for issuance, OID4VP for presentation
1. Set Up the Wallet
using SdJwt.Net.Wallet;
using SdJwt.Net.Wallet.Storage;
using SdJwt.Net.Wallet.Formats;
// Create components
var credentialStore = new InMemoryCredentialStore();
var keyManager = new YourKeyManager(); // Implement IKeyManager
var sdJwtPlugin = new SdJwtVcFormatPlugin();
// Create the wallet
var wallet = new GenericWallet(
credentialStore,
keyManager,
new ICredentialFormatPlugin[] { sdJwtPlugin }
);
2. Store Credentials
When receiving credentials from an Issuer (via OID4VCI or other means):
// Create a credential to store
var credential = new StoredCredential
{
Format = "vc+sd-jwt",
RawCredential = sdJwtString,
CredentialType = "IdentityCredential",
IssuerIdentifier = "https://issuer.example.com",
Claims = new Dictionary<string, object>
{
["given_name"] = "John",
["family_name"] = "Doe",
["birth_date"] = "1990-01-15"
}
};
// Store in the wallet
string credentialId = await wallet.StoreCredentialAsync(credential);
Console.WriteLine($"Stored credential: {credentialId}");
3. List and Query Credentials
// List all credentials
var allCredentials = await wallet.ListCredentialsAsync();
foreach (var cred in allCredentials)
{
Console.WriteLine($"ID: {cred.Id}, Type: {cred.CredentialType}");
}
// Query by type
var identityCredentials = await credentialStore.QueryAsync(
new CredentialQuery
{
Types = new[] { "IdentityCredential" }
}
);
// Query by issuer
var fromIssuer = await credentialStore.QueryAsync(
new CredentialQuery
{
Issuers = new[] { "https://issuer.example.com" }
}
);
4. Find Matching Credentials for Presentation
When a Verifier requests specific credential types:
// Find credentials matching a request
var matchingCredentials = await wallet.FindMatchingCredentialsAsync(
types: new[] { "IdentityCredential", "DriversLicenseCredential" },
issuers: new[] { "https://trusted-issuer.gov" }
);
if (matchingCredentials.Any())
{
Console.WriteLine($"Found {matchingCredentials.Count()} matching credentials");
}
5. Create Presentations with Selective Disclosure
Present only the claims needed for a specific interaction:
// Create a selective disclosure presentation
string presentation = await wallet.CreatePresentationAsync(
credentialId: credentialId,
claimsToDisclose: new[] { "given_name", "birth_date" }, // Only these claims
audience: "https://verifier.example.com",
nonce: "unique-nonce-from-verifier"
);
// The presentation only reveals given_name and birth_date
// family_name remains hidden
6. Validate Credentials
// Validate a credential
var validationResult = await wallet.ValidateCredentialAsync(credentialId);
if (validationResult.IsValid)
{
Console.WriteLine("Credential is valid");
}
else
{
Console.WriteLine($"Validation failed: {validationResult.ErrorMessage}");
}
// Check revocation status
var statusResult = await wallet.CheckStatusAsync(credentialId);
if (statusResult.IsActive)
{
Console.WriteLine("Credential is active");
}
7. Implementing Custom Storage
For production use, implement secure storage:
public class SecureCredentialStore : ICredentialStore
{
public async Task<string> StoreAsync(StoredCredential credential)
{
// Encrypt and store securely
var encrypted = await EncryptCredential(credential);
await _secureStorage.SaveAsync(credential.Id, encrypted);
return credential.Id;
}
public async Task<StoredCredential?> GetByIdAsync(string id)
{
var encrypted = await _secureStorage.LoadAsync(id);
if (encrypted == null) return null;
return await DecryptCredential(encrypted);
}
public async Task<IEnumerable<StoredCredential>> QueryAsync(CredentialQuery query)
{
// Query implementation with filtering
var all = await GetAllDecrypted();
return FilterByQuery(all, query);
}
public async Task<bool> DeleteAsync(string id)
{
return await _secureStorage.DeleteAsync(id);
}
public async Task<bool> UpdateAsync(StoredCredential credential)
{
var encrypted = await EncryptCredential(credential);
return await _secureStorage.UpdateAsync(credential.Id, encrypted);
}
}
8. Implementing Key Management
public class HardwareKeyManager : IKeyManager
{
public async Task<KeyInfo> GenerateKeyAsync(KeyGenerationOptions options)
{
// Generate key in hardware security module
var keyId = await _hsm.GenerateKeyPairAsync(options.Algorithm);
return new KeyInfo
{
KeyId = keyId,
Algorithm = options.Algorithm,
PublicKey = await _hsm.GetPublicKeyAsync(keyId)
};
}
public async Task<byte[]> SignAsync(string keyId, byte[] data)
{
return await _hsm.SignAsync(keyId, data);
}
public async Task<KeyInfo?> GetKeyInfoAsync(string keyId)
{
if (!await _hsm.KeyExistsAsync(keyId)) return null;
return new KeyInfo
{
KeyId = keyId,
PublicKey = await _hsm.GetPublicKeyAsync(keyId)
};
}
public async Task<bool> DeleteKeyAsync(string keyId)
{
return await _hsm.DeleteKeyAsync(keyId);
}
}
Plugin Architecture
The wallet uses a plugin system for credential format handling. Each format plugin implements:
public interface ICredentialFormatPlugin
{
string FormatIdentifier { get; }
bool CanHandle(string format);
Task<ParsedCredential> ParseAsync(string rawCredential, ParseOptions? options = null);
Task<string> CreatePresentationAsync(StoredCredential credential, PresentationContext context);
Task<ValidationResult> ValidateAsync(StoredCredential credential, ValidationContext context);
}
Supported Formats
| Format | Plugin Class | Description |
|---|---|---|
vc+sd-jwt |
SdJwtVcFormatPlugin |
SD-JWT Verifiable Credentials |
mso_mdoc |
(Coming soon) | ISO 18013-5 mdoc/mDL |
Best Practices
-
Use Secure Storage - Never store credentials in plain text. Use encrypted, hardware-backed storage in production.
-
Protect Keys - Use hardware security modules or secure enclaves for key management.
-
Minimize Disclosure - Only present the minimum claims required for each interaction.
-
Validate Before Presenting - Always validate credentials before creating presentations.
-
Handle Expiration - Implement automatic cleanup or renewal of expired credentials.
-
Audit Logging - Log all credential operations for security audit trails.
Integration with OID4VCI/OID4VP
The wallet provides adapter interfaces for OpenID4VC protocols:
// OID4VCI adapter for credential issuance
public interface IOid4VciAdapter
{
Task<CredentialOffer> ParseOfferAsync(string offerUri);
Task<TokenResponse> RequestTokenAsync(AuthorizationDetails details);
Task<StoredCredential> RequestCredentialAsync(CredentialRequest request);
}
// OID4VP adapter for credential presentation
public interface IOid4VpAdapter
{
Task<AuthorizationRequest> ParseRequestAsync(string requestUri);
Task<AuthorizationResponse> CreateResponseAsync(
AuthorizationRequest request,
IEnumerable<StoredCredential> selectedCredentials
);
}