Sorcha Wallet Service
Version: 1.0.0 Status: 98% Complete Framework: .NET 10.0 Architecture: Microservice
Overview
The Wallet Service provides enterprise-grade cryptographic wallet management with Hierarchical Deterministic (HD) wallet support, enabling secure key generation, transaction signing, and payload encryption/decryption. It implements BIP32/BIP39/BIP44 standards for deterministic key derivation and supports multiple cryptographic algorithms (ED25519, NISTP256, RSA-4096).
This service acts as the cryptographic foundation for:
- Secure key management with encrypted private keys
- HD wallet architecture enabling unlimited addresses from a single mnemonic
- Client-side address derivation (BIP44) for privacy and scalability
- Multi-algorithm cryptography supporting different blockchain requirements
- Access delegation with granular permission control
Key Features
- HD Wallet Creation: BIP39 mnemonic generation (12/15/18/21/24 words) with optional passphrase
- Wallet Recovery: Restore wallets from mnemonic phrase (disaster recovery)
- Client-Side BIP44 Address Derivation: Privacy-preserving address generation without server communication
- Multi-Algorithm Support: ED25519 (fast signatures), NISTP256 (secp256r1), RSA-4096 (legacy compatibility)
- Transaction Signing: Cryptographically sign transactions for blockchain submission
- Payload Encryption/Decryption: Selective data disclosure with asymmetric encryption
- Access Delegation: Grant read/write access to other identities (Owner, ReadWrite, ReadOnly roles)
- Private Key Encryption at Rest: AES-256-GCM encryption for stored keys
- Multi-Account Support: BIP44 account hierarchy (m/44'/coin'/account'/change/index)
- Gap Limit Enforcement: BIP44-compliant 20-address limit for receive/change chains
- Address Management: Track used/unused addresses, mark as used, query by account
Architecture
HD Wallet Structure (BIP44)
Mnemonic (12-24 words)
└── Seed (512 bits)
└── Master Key (m/)
└── Purpose (m/44')
└── Coin Type (m/44'/coin')
└── Account (m/44'/coin'/0', m/44'/coin'/1', ...)
├── External Chain (m/44'/coin'/0'/0) [Receive addresses]
│ ├── Address 0 (m/44'/coin'/0'/0/0)
│ ├── Address 1 (m/44'/coin'/0'/0/1)
│ └── ...
└── Internal Chain (m/44'/coin'/0'/1) [Change addresses]
├── Address 0 (m/44'/coin'/0'/1/0)
└── ...Components
Wallet Service
├── API Endpoints
│ ├── Wallets (CRUD, sign, encrypt, decrypt)
│ ├── HD Addresses (register, list, mark-used)
│ ├── Accounts (list BIP44 accounts)
│ ├── Access Control (grant, revoke, check)
│ └── Gap Status (BIP44 compliance)
├── Cryptography Layer
│ ├── Sorcha.Cryptography (multi-algorithm)
│ ├── NBitcoin (BIP32/BIP39/BIP44)
│ └── AES-256-GCM (key encryption)
├── Repositories
│ ├── Wallet Repository (in-memory, EF Core pending)
│ └── Address Repository (in-memory)
└── External Integrations
├── Azure Key Vault (planned for production)
└── Tenant Service (authentication - planned)Data Flow
Client → Wallet API → [Create Wallet]
↓
Generate Mnemonic (BIP39) → Derive Master Key (BIP32) → Encrypt Private Key (AES-256-GCM)
↓
Store in Repository → Return Public Key & Address
↓
Client-Side Derivation → [Derive Child Address] → Register with Wallet Service
↓
Transaction Signing → Wallet Service → [Sign Transaction] → Return SignatureQuick Start
Prerequisites
- .NET 10 SDK or later
- Docker Desktop (optional, for production dependencies)
- Git
1. Clone and Navigate
git clone https://github.com/yourusername/Sorcha.git
cd Sorcha/src/Services/Sorcha.Wallet.Service2. Set Up Configuration
The service uses appsettings.json for configuration. For local development, defaults are pre-configured.
3. Run the Service
dotnet runService will start at:
- HTTPS:
https://localhost:7084 - HTTP:
http://localhost:5084 - Scalar API Docs:
https://localhost:7084/scalar
Configuration
appsettings.json Structure
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"Wallet": {
"EncryptionProvider": "Local",
"KeyDerivationIterations": 100000,
"EnableClientSideDerivation": true,
"GapLimit": 20
},
"OpenTelemetry": {
"ServiceName": "Sorcha.Wallet.Service",
"ZipkinEndpoint": "http://localhost:9411"
}
}Environment Variables
For production deployment:
# Encryption provider (Local, AzureKeyVault, AwsKms)
WALLET__ENCRYPTIONPROVIDER="AzureKeyVault"
# Azure Key Vault settings (if using AzureKeyVault)
AZURE__KEYVAULTURL="https://your-vault.vault.azure.net/"
# Database connection (when EF Core is implemented)
CONNECTIONSTRINGS__WALLETDB="Server=.;Database=SorchaWallet;Trusted_Connection=True;"
# Observability
OPENTELEMETRY__ZIPKINENDPOINT="https://zipkin.yourcompany.com"API Endpoints
Wallet Management
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/v1/wallets/ | Create a new HD wallet |
| POST | /api/v1/wallets/recover | Recover wallet from mnemonic |
| GET | /api/v1/wallets/ | List wallets for current user |
| GET | /api/v1/wallets/{address} | Get wallet by address |
| PATCH | /api/v1/wallets/{address} | Update wallet metadata (name, tags) |
| DELETE | /api/v1/wallets/{address} | Delete wallet (soft delete) |
| POST | /api/v1/wallets/{address}/sign | Sign a transaction |
| POST | /api/v1/wallets/{address}/decrypt | Decrypt a payload |
| POST | /api/v1/wallets/{address}/encrypt | Encrypt a payload |
HD Address Management (BIP44)
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/v1/wallets/{address}/addresses | Register client-derived address |
| GET | /api/v1/wallets/{address}/addresses | List wallet addresses |
| GET | /api/v1/wallets/{address}/addresses/{id:guid} | Get specific address details |
| PATCH | /api/v1/wallets/{address}/addresses/{id:guid} | Update address metadata |
| POST | /api/v1/wallets/{address}/addresses/{id:guid}/mark-used | Mark address as used |
| GET | /api/v1/wallets/{address}/accounts | List BIP44 accounts |
| GET | /api/v1/wallets/{address}/gap-status | Get gap limit compliance status |
Access Control (Delegation)
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/v1/wallets/{walletAddress}/access | Grant access to another identity |
| GET | /api/v1/wallets/{walletAddress}/access | List active access grants |
| DELETE | /api/v1/wallets/{walletAddress}/access/{subject} | Revoke access |
| GET | /api/v1/wallets/{walletAddress}/access/{subject}/check | Check if subject has access |
For full API documentation with request/response schemas, open Scalar UI at https://localhost:7084/scalar.
Org Key Derivation (Feature 083)
Organisation-level HD key derivation using Sorcha-specific BIP32 paths (m/0x534F52'/org'/dept'/user'/usage/index). Enables org-controlled key lifecycle without individual mnemonic management.
Custody Modes
| Mode | Description | Status |
|---|---|---|
| Custodial | Full key in KMS, no device share — service accounts/automation | Implemented |
| Co-signed | Server share + device share — standard user operations | Schema only |
| Self-custody | Full key on device, optional recovery escrow — external wallets | Schema only |
Org Key Derivation Endpoints
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/wallets/org/{orgId}/master-key | Provision org master key (one-shot, returns mnemonic once) |
| POST | /api/wallets/org/{orgId}/derive-key | Derive user key (idempotent) |
| POST | /api/wallets/org/{orgId}/keys/{derivedKeyId}/rotate | Rotate key (new at next index, old decrypt-only) |
| DELETE | /api/wallets/org/{orgId}/keys/{derivedKeyId} | Revoke key (wallet locked, DID event for identity keys) |
Key Usage Types
| Value | Name | Purpose |
|---|---|---|
| 0 | Identity | DID identity keys |
| 1 | VCIssuance | Verifiable credential signing |
| 2 | Governance | Register governance operations |
| 3 | Communications | Encrypted communications |
| 4 | ServiceAuth | Service-to-service authentication |
Key Entities
- OrgMasterKey: Organisation root seed, encrypted at rest via Key Protection Provider (KPP), one per org
- DerivedKeyRecord: User key derived from org master, tracks derivation path, usage type, index, and status (Active/Rotated/Revoked)
Development
Project Structure
Sorcha.Wallet.Service/
├── Program.cs # Service entry point
├── Endpoints/
│ ├── WalletEndpoints.cs # Wallet CRUD, sign, encrypt
│ ├── AddressEndpoints.cs # HD address management
│ ├── AccountEndpoints.cs # BIP44 account operations
│ └── AccessEndpoints.cs # Access delegation
├── Services/
│ ├── WalletService.cs # Business logic
│ ├── CryptographyService.cs # Signing, encryption
│ ├── AddressDerivationService.cs # BIP44 derivation
│ └── AccessControlService.cs # Access management
├── Repositories/
│ ├── IWalletRepository.cs # Repository interfaces
│ ├── WalletRepository.cs # In-memory implementation
│ ├── IAddressRepository.cs
│ └── AddressRepository.cs
├── Models/
│ ├── Wallet.cs # Domain models
│ ├── WalletAddress.cs
│ ├── AccessGrant.cs
│ └── Account.cs
└── appsettings.json # Configuration
External Libraries:
├── Sorcha.Cryptography/ # Multi-algorithm crypto
├── Sorcha.Wallet.Core/ # Core wallet logic (EF Core, repositories, encryption)
├── Sorcha.Wallet.Portable/ # Portable wallet: entities, enums, derivation (NuGet package)
└── NBitcoin/ # BIP32/BIP39/BIP44 (via Wallet.Portable)Running Tests
# Run all Wallet Service tests
dotnet test tests/Sorcha.Wallet.Service.Tests
# Run API integration tests
dotnet test tests/Sorcha.Wallet.Service.Api.Tests
# Run with coverage
dotnet test tests/Sorcha.Wallet.Service.Tests --collect:"XPlat Code Coverage"
# Watch mode
dotnet watch test --project tests/Sorcha.Wallet.Service.TestsCode Coverage
Current Coverage: ~87% Tests: 111 unit tests, 35 integration tests Lines of Code: ~8,000 LOC
# Generate coverage report
dotnet test --collect:"XPlat Code Coverage"
reportgenerator -reports:**/coverage.cobertura.xml -targetdir:coverage -reporttypes:HtmlHD Wallet Workflow
1. Create Wallet
POST /api/v1/wallets/
Content-Type: application/json
{
"name": "My HD Wallet",
"algorithm": "ED25519",
"wordCount": 24,
"passphrase": "optional-bip39-passphrase"
}Response:
{
"address": "did:sorcha:wallet:abc123",
"mnemonic": "word1 word2 word3 ... word24",
"publicKey": "base64-encoded-public-key",
"algorithm": "ED25519"
}⚠️ Important: Save the mnemonic in a secure location. It cannot be retrieved later.
2. Client-Side Address Derivation (BIP44)
import { HDKey } from '@scure/bip32';
import { mnemonicToSeedSync } from '@scure/bip39';
// Derive child address client-side
const mnemonic = "word1 word2 ... word24";
const seed = mnemonicToSeedSync(mnemonic, passphrase);
const masterKey = HDKey.fromMasterSeed(seed);
// BIP44 path: m/44'/0'/0'/0/0 (first receive address)
const childKey = masterKey.derive("m/44'/0'/0'/0/0");
const address = childKey.publicKey;3. Register Address with Wallet Service
POST /api/v1/wallets/{walletAddress}/addresses
Content-Type: application/json
{
"bip44Path": "m/44'/0'/0'/0/0",
"publicKey": "base64-encoded-public-key",
"purpose": 44,
"coinType": 0,
"account": 0,
"change": 0,
"addressIndex": 0,
"label": "First receive address"
}4. Mark Address as Used
POST /api/v1/wallets/{walletAddress}/addresses/{addressId}/mark-used5. Check Gap Limit Status
GET /api/v1/wallets/{walletAddress}/gap-statusResponse:
{
"gapLimit": 20,
"accounts": [
{
"account": 0,
"receiveChain": {
"unusedCount": 5,
"lastUsedIndex": 14,
"isCompliant": true
},
"changeChain": {
"unusedCount": 18,
"lastUsedIndex": 1,
"isCompliant": true
}
}
]
}Integration with Other Services
Blueprint Service Integration
The Wallet Service is called by the Blueprint Service for:
- Transaction Signing: Sign action transactions before blockchain submission
- Payload Encryption: Encrypt selective disclosure payloads
- Payload Decryption: Decrypt received action data
Communication: HTTP REST API
Tenant Service Integration (Planned)
Future integration for:
- JWT Authentication: Validate bearer tokens
- User Context: Retrieve wallet ownership information
- Multi-Tenant Isolation: Wallet scoping by organization
Security Considerations
Credential Presentation Verification (Feature 093)
Since feature 093 (VC & presentation security fixes, HAIP prep), POST /api/v1/presentations/{requestId}/submit cryptographically verifies the submitted vpToken against the issuer's public key before any claim is considered. Claim values in the verification result come from the verified token, not from the server-side credential store. Tampered, forged, or replay tokens are rejected with specific verification errors.
Credential Status Embedding (Feature 093)
When a credential is issued via a Blueprint action, the Blueprint Service's ActionExecutionService pre-allocates a status list index before calling the Wallet Service issue endpoint and forwards the allocation to the issue request. The Wallet Service's CredentialEndpoints.IssueCredential handler then embeds a credentialStatus claim (W3C BitstringStatusListEntry shape) in the signed SD-JWT VC payload in lockstep with the stored CredentialEntity row fields.
The CredentialStatus:EnableEmbedding flag lives in the Blueprint Service's appsettings.json (not here — see src/Services/Sorcha.Blueprint.Service/appsettings.json). When true (default), the Blueprint Service pre-allocates before signing and credentials carry the embedded claim. When false, pre-allocation is skipped and credentials are issued without the claim — dev-environment escape hatch for stacks that do not run the status list manager.
Non-fatal allocation failures: if IStatusListManager is reachable but an allocation call throws, ActionExecutionService logs a warning and proceeds to issue the credential without the embedded claim. The credential is still valid and verifies via the spec 093 FR-010 server-side row fallback. This is a pragmatic scope deviation from spec FR-008 (which called for fail-closed) — documented in specs/093-vc-security-fixes/tasks.md.
The direct HTTP issuance path (POST /api/v1/wallets/{address}/credentials/issue called outside a Blueprint action) does not embed the claim unless the caller explicitly supplies statusListUrl and statusListIndex on the request. Callers that want a HAIP-compliant credential through this path must supply the allocation themselves.
Pre-Feature-093 credentials (no embedded credentialStatus claim) remain valid indefinitely and continue to verify via the server-side CredentialEntity.StatusListUrl / StatusListIndex fallback.
Holder Binding Key (Feature 094)
Every Sorcha wallet has a deterministic holder binding key derived from the wallet seed under the sorcha:credential-holder-binding BIP32 purpose (index 105). This key is used to prove holder possession of a credential via a Key Binding JWT (KB-JWT) in SD-JWT VC presentations.
- One key per wallet, not per credential
- Deterministic: same mnemonic always produces the same binding key
- Automatic: the Wallet Service derives and signs KB-JWTs without callers managing key material
Services: IHolderBindingKeyService provides GetPublicJwkAsync(walletAddress) and SignKbJwtAsync(walletAddress, signingInput).
HAIP Issuer Classical Co-Key (Feature 094)
Wallets whose primary algorithm is PQC (ML-DSA, SLH-DSA) derive a classical co-key (ES256 by default) under the sorcha:haip-issuer-signing BIP32 purpose (index 106) for signing HAIP-conformant SD-JWT VCs. Wallets with a classical primary key (Ed25519, P-256) use their primary key directly.
- Requires the
HaipIssuercapability flag on the wallet entity - Classical-primary wallets: no new key derived, primary key used for HAIP issuance
- PQC-primary wallets: ES256 co-key derived alongside the PQC primary
Services: IHaipIssuerCoKeyService provides GetSigningKeyForHaipIssuanceAsync(walletAddress).
Private Key Protection
- At-Rest Encryption: All private keys encrypted with AES-256-GCM
- Encryption Key Storage:
- Development: Local in-memory encryption (NOT secure - dev/test only)
- Production: Azure Key Vault, AWS KMS, or Google Cloud KMS
- Mnemonic Handling: Never stored; only shown once during wallet creation
- Memory Protection: Sensitive data cleared from memory after use
Encryption Provider Configuration
⚠️ IMPORTANT: The default LocalEncryptionProvider is NOT suitable for production as it stores encryption keys in memory only and they are lost on service restart.
Development/Testing Configuration
The current Docker Compose setup uses LocalEncryptionProvider for development:
# Warning message in logs (expected in development):
warn: Sorcha.Wallet.Core.Encryption.Providers.LocalEncryptionProvider[0]
LocalEncryptionProvider initialized. This provider is for development only.Limitations:
- Keys stored in memory only (lost on restart)
- No key backup or recovery
- Not suitable for production use
- Wallets created in development cannot be migrated to production
Production Configuration Options
Option 1: Azure Key Vault (Recommended for Azure deployments)
{
"Wallet": {
"EncryptionProvider": "AzureKeyVault",
"AzureKeyVault": {
"VaultUrl": "https://your-vault.vault.azure.net/",
"UseManagedIdentity": true
}
}
}Docker Compose configuration:
wallet-service:
environment:
Wallet__EncryptionProvider: "AzureKeyVault"
Wallet__AzureKeyVault__VaultUrl: "https://your-vault.vault.azure.net/"
Wallet__AzureKeyVault__UseManagedIdentity: "true"
AZURE_TENANT_ID: "${AZURE_TENANT_ID}"
AZURE_CLIENT_ID: "${AZURE_CLIENT_ID}"
AZURE_CLIENT_SECRET: "${AZURE_CLIENT_SECRET}"Option 2: AWS KMS
{
"Wallet": {
"EncryptionProvider": "AwsKms",
"AwsKms": {
"Region": "us-east-1",
"KeyId": "arn:aws:kms:us-east-1:123456789012:key/your-key-id",
"UseIamRole": true
}
}
}Option 3: Google Cloud KMS
{
"Wallet": {
"EncryptionProvider": "GcpKms",
"GcpKms": {
"ProjectId": "your-project-id",
"LocationId": "global",
"KeyRingId": "sorcha-keys",
"KeyId": "wallet-encryption-key"
}
}
}Migration from Development to Production
WARNING: Wallets created with LocalEncryptionProvider CANNOT be migrated to production HSM backends because:
- The encryption keys are stored in memory and lost on restart
- There is no key export mechanism for security reasons
- Private keys are encrypted with ephemeral keys
Production Deployment Checklist:
- [ ] Configure Azure Key Vault, AWS KMS, or GCP Cloud KMS
- [ ] Set up Managed Identity or IAM Role for the service
- [ ] Test encryption/decryption with the HSM provider
- [ ] Ensure DataProtection keys are stored in a shared volume
- [ ] Verify audit logging is enabled
- [ ] Test wallet creation and signing operations
- [ ] Confirm private keys are never logged
- [ ] Set up key rotation policies in your HSM provider
For detailed HSM configuration, see: Hardware Cryptographic Storage Feature Spec
Access Control
Three access levels:
- Owner: Full control (sign, encrypt, decrypt, grant access, delete)
- ReadWrite: Sign and encrypt/decrypt (no access management)
- ReadOnly: View wallet information only (no cryptographic operations)
Authentication
- Current: Development mode (no authentication required)
- Production: JWT bearer token authentication (issued by Tenant Service)
Best Practices
- ✅ Always use HTTPS in production
- ✅ Store mnemonics offline (paper wallets, hardware wallets)
- ✅ Use passphrases for additional mnemonic protection
- ✅ Rotate access grants periodically
- ✅ Enable Azure Key Vault or AWS KMS for production key management
Deployment
.NET Aspire (Development)
The Wallet Service is registered in the Aspire AppHost:
var walletService = builder.AddProject<Projects.Sorcha_Wallet_Service>("wallet-service")
.WithReference(redis);Start the entire platform:
dotnet run --project src/Apps/Sorcha.AppHostAccess Aspire Dashboard: http://localhost:15888
Docker
# Build Docker image
docker build -t sorcha-wallet-service:latest -f src/Services/Sorcha.Wallet.Service/Dockerfile .
# Run container
docker run -d \
-p 7084:8080 \
-e Wallet__EncryptionProvider="Local" \
--name wallet-service \
sorcha-wallet-service:latestAzure Deployment
Deploy to Azure Container Apps with:
- Key Management: Azure Key Vault for encryption keys
- Database: Azure SQL Database (when EF Core is implemented)
- Secrets: Managed Identity for Key Vault access
- Observability: Application Insights integration
Observability
Logging (Serilog + Seq)
Structured logging with Serilog:
Log.Information("Wallet {WalletAddress} created with algorithm {Algorithm}", address, algorithm);Log Sinks:
- Console (structured output via Serilog)
- OTLP → Aspire Dashboard (centralized log aggregation)
Security: Private keys and mnemonics are NEVER logged.
Tracing (OpenTelemetry + Zipkin)
Distributed tracing with OpenTelemetry:
# View traces in Zipkin
open http://localhost:9411Traced Operations:
- Wallet creation/recovery
- Signing operations
- Address derivation
Metrics (Prometheus)
Metrics exposed at /metrics:
- Wallet creation rate
- Signing operation latency
- Access grant count
- Address registration rate
Troubleshooting
Common Issues
Issue: Mnemonic recovery fails with "Invalid mnemonic" Solution: Ensure the mnemonic phrase is exactly as generated (correct word order, no typos). Verify passphrase if one was used.
Issue: "Gap limit exceeded" error when registering address Solution: Mark some addresses as used first, or create a new account:
POST /api/v1/wallets/{address}/addresses
{
"account": 1, // New account
"change": 0,
"addressIndex": 0
}Issue: Signing fails with "Wallet not found" Solution: Verify the wallet address and that the wallet hasn't been soft-deleted.
Issue: Encryption key not accessible (Azure Key Vault) Solution: Verify Managed Identity permissions:
# Grant Key Vault access
az keyvault set-policy --name your-vault \
--object-id <managed-identity-id> \
--key-permissions get unwrapKey wrapKeyDebug Mode
Enable detailed logging:
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"Sorcha.Wallet.Service": "Trace",
"Sorcha.Cryptography": "Trace"
}
}
}Cloud KMS Configuration
Feature 082 delivered full Azure Key Vault integration for wallet key storage. The service uses an envelope encryption model: Data Encryption Keys (DEKs) are generated locally per wallet and wrapped by the Key Protection Provider (KPP). The KPP can be a local provider (development) or Azure Key Vault (production).
EncryptionProvider settings
Set EncryptionProvider:Type in appsettings.json (or environment variables):
| Type | When to use |
|---|---|
Local | Development only — keys lost on restart |
WindowsDpapi | Production on Windows hosts |
LinuxSecretService | Production on Linux hosts / Docker |
AzureKeyVault | Production on Azure — recommended |
Azure Key Vault (recommended for production)
{
"EncryptionProvider": {
"Type": "AzureKeyVault",
"DefaultKeyId": "wallet-key-2025",
"AzureKeyVault": {
"VaultUri": "https://your-vault.vault.azure.net/",
"DefaultKeyName": "wallet-encryption-key",
"UseManagedIdentity": true,
"EnableDekCache": true,
"DekCacheTtlMinutes": 60,
"AllowStaleDeksOnOutage": true
}
}
}Docker Compose / Azure Container Apps environment variables:
EncryptionProvider__Type=AzureKeyVault
EncryptionProvider__DefaultKeyId=wallet-key-2025
EncryptionProvider__AzureKeyVault__VaultUri=https://your-vault.vault.azure.net/
EncryptionProvider__AzureKeyVault__UseManagedIdentity=true
# For non-managed-identity (service principal):
AZURE_TENANT_ID=<tenant-id>
AZURE_CLIENT_ID=<client-id>
AZURE_CLIENT_SECRET=<client-secret>Required Key Vault permissions for the Managed Identity / service principal:
keys/get,keys/wrapKey,keys/unwrapKeykeys/create(if auto-creating the KEK on first start)
WalletKeyManagement settings
Controls DEK caching and signing mode policy:
{
"WalletKeyManagement": {
"DefaultKeyId": "wallet-key-2025",
"DekCacheTtlMinutes": 30,
"DekCacheGracePeriodMinutes": 5,
"MaxCachedDeks": 1000,
"SigningPolicy": {
"DefaultMode": "Local",
"AllowLocalToKmsMigration": false,
"AllowedKmsAlgorithms": ["ED25519", "NISTP256"]
}
}
}DefaultMode options:
Local— private keys are stored encrypted by envelope encryption (default).KmsResident— private keys are generated inside Azure Key Vault and never leave the HSM. Signing calls are forwarded to the KMS. Use when regulatory requirements mandate HSM-resident keys.
AllowLocalToKmsMigration — set to true to allow migrating existing Local wallets to KmsResident. The reverse is never permitted.
AllowedKmsAlgorithms — list of algorithm names permitted for KMS-resident keys. Empty list means all algorithms are allowed.
Production Feature Status
Completed (MVD)
- [x] EF Core Repository: PostgreSQL persistence via EF Core
- [x] JWT Authentication: Integration with Tenant Service
- [x] Azure Key Vault Integration: Envelope encryption + KMS-resident signing via
Sorcha.Wallet.Providers.Azure(Feature 082)
Deferred (Post-MVD)
- [ ] AWS KMS / GCP KMS Support: Multi-cloud KMS providers (deferred from Feature 082)
- [ ] Hardware Wallet Integration: Ledger, Trezor support
- [ ] Audit Logging: Comprehensive operation logging
- [ ] Backup/Restore: Encrypted wallet backup system
Contributing
Development Workflow
- Create a feature branch:
git checkout -b feature/your-feature - Make changes: Follow C# coding conventions
- Write tests: Maintain >85% coverage
- Run tests:
dotnet test - Format code:
dotnet format - Commit:
git commit -m "feat: your feature description" - Push:
git push origin feature/your-feature - Create PR: Reference issue number
Code Standards
- Follow C# Coding Conventions
- Use async/await for I/O operations
- Add XML documentation for public APIs
- Include unit tests for all business logic
- Never log sensitive data (keys, mnemonics, passphrases)
Notification Pipeline
The Wallet Service receives inbound transaction notifications from the Register Service and delivers them to users in real-time or via digest batching.
Pipeline Flow
Register Service (docket sealed)
→ gRPC: NotifyInboundTransaction
→ Resolve address → wallet → user
→ Rate limiter check
├── Under limit → Redis pub/sub (real-time)
└── Over limit → Redis sorted set (digest queue)Real-time Delivery
When an inbound transaction is detected, the notification pipeline:
- Resolves the recipient wallet address to a user identity
- Checks the sliding window rate limiter
- Publishes via Redis pub/sub (
wallet:notificationschannel) for the EventsHub SignalR bridge
Rate Limiting
A sliding window rate limiter (Redis-backed) prevents notification flooding. When the rate limit is exceeded, events are queued to the digest for later consolidated delivery. The notification rate limit is configured via the centralised RateLimiting section:
| Setting | Default | Description |
|---|---|---|
RateLimiting:NotificationRealTimePerMinute | 100000 (dev) | Max real-time notifications per user per minute |
Production deployments should tighten this value (e.g. 10) in appsettings.Production.json.
Digest Batching
A BackgroundService processes accumulated notification events, groups them by blueprint, and delivers consolidated summaries. Digest frequency is determined by user preference (hourly or daily).
Configuration (appsettings.json):
| Setting | Default | Description |
|---|---|---|
Notifications:DigestCheckIntervalMinutes | 5 | How often the worker checks for pending digests |
Notifications:DigestHourlyMinute | 0 | Minute of the hour to send hourly digests |
Notifications:DigestDailyHour | 8 | Hour of the day (UTC) to send daily digests |
Notifications:DigestDailyMinute | 0 | Minute of the hour to send daily digests |
{
"RateLimiting": {
"NotificationRealTimePerMinute": 10
},
"Notifications": {
"DigestCheckIntervalMinutes": 5,
"DigestHourlyMinute": 0,
"DigestDailyHour": 8,
"DigestDailyMinute": 0
}
}Notification Preference Provider (Feature 062)
TenantNotificationPreferenceProvider replaces DefaultNotificationPreferenceProvider for resolving per-user notification delivery preferences (delivery mode and frequency). It calls the Tenant Service GET /api/preferences endpoint to retrieve user settings and caches results for 5 minutes per user to reduce cross-service calls.
gRPC Services
WalletNotificationGrpcService — Receives inbound transaction notifications from the Register Service.
| RPC Method | Description |
|---|---|
GetAllLocalAddresses | Returns all locally-registered wallet addresses for bloom filter population |
NotifyInboundTransaction | Notify a single inbound transaction for a local address |
NotifyInboundTransactionBatch | Batch notify for recovery processing (multiple transactions) |
Wallet Recovery (Feature 060)
The Wallet Service supports key recovery through passkey-based and organization-delegated recovery paths, enabling users to regain access to wallet signing keys without requiring mnemonic backup.
Recovery Entities
- RecoveryKeyWrap: Stores an AES-256-GCM recovery key wrapped with an asymmetric public key (passkey or org recovery key). Linked to a wallet via
WalletId. - RecoveryAuditLog: Immutable audit trail of all recovery operations (wrap, unwrap, revocation).
- RecoveryPathType (enum):
Passkey,Organization— identifies the recovery mechanism used. - Wallet extensions:
EncryptedMasterKeyBlob(encrypted master key for recovery) andRecoveryEnabledflag.
Recovery Key Generation Flow
When a wallet is created via WalletManager.CreateWalletAsync, a recovery key is automatically generated:
- An AES-256-GCM recovery key is generated by
IRecoveryKeyService - The wallet's master key is encrypted with this recovery key and stored as
EncryptedMasterKeyBlob - The recovery key is wrapped with the user's passkey public key (retrieved from Tenant Service via
PasskeyServiceClient) - The wrapped key is stored as a
RecoveryKeyWraprecord RecoveryEnabledis set totrueon the wallet
Recovery Endpoints
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/v1/wallets/{address}/recover/passkey | Recover wallet using passkey-wrapped recovery key |
| POST | /api/v1/wallets/{address}/recover/org | Recover wallet using organization recovery delegation |
| POST | /api/v1/wallets/{address}/recover/delegations/preserve | Preserve existing delegations during recovery |
| GET | /api/v1/wallets/{address}/recovery-status | Get wallet recovery status and available recovery paths |
Recovery Services
- IRecoveryKeyService / RecoveryKeyService: Core recovery key operations — AES-256-GCM key generation, asymmetric wrap/unwrap.
- PasskeyRecoveryService: Handles passkey-based recovery flow, retrieves passkey public keys from Tenant Service.
- OrgRecoveryService: Handles organization-delegated recovery with delegation revocation support.
- PasskeyServiceClient: HTTP client for retrieving passkey public keys from the Tenant Service.
Tests
28 unit tests covering recovery key generation, wrap/unwrap operations, passkey and org recovery flows, and delegation preservation.
File Download (Feature 085)
Allows wallet holders to download decrypted file attachments stored in blueprint action transactions.
Endpoint
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/v1/wallets/{address}/files/download | Download a decrypted file attachment |
Query Parameters
| Parameter | Required | Default | Description |
|---|---|---|---|
registerId | Yes | — | Register containing the source transaction |
actionTxId | Yes | — | Transaction ID of the action holding the file |
fieldName | Yes | — | JSON field name of the file attachment |
fileIndex | No | 0 | Index within a multi-file field |
Auth
JWT Bearer required. The calling wallet must be the owner or hold a delegated access grant with at least ReadOnly permission for the encrypted field.
Response
200 OK — binary file stream. Content-Type and Content-Disposition headers reflect the original file MIME type and name.
Resources
- Specification: .specify/specs/sorcha-wallet-service.md
- API Reference: Scalar UI
- Development Status: docs/development-status.md
- Architecture: docs/architecture.md
- BIP39 Standard: Bitcoin BIPs
- BIP44 Standard: Bitcoin BIPs
- OpenAPI Spec:
https://localhost:7084/openapi/v1.json
License
Apache License 2.0 - See LICENSE for details.
Last Updated: 2026-03-03 Maintained By: Sorcha Contributors Status: 98% Complete