Secure, In-Memory Blockchain Signing Service for Ethereum & Solana
Version 0.1.0
- Overview
- Architecture
- Installation & Setup
- Configuration
- Key Management
- Policy Engine
- gRPC API Reference
- Security Considerations
- Docker Deployment
- Troubleshooting
- Project Structure
- Roadmap
locksign is an open-source, self-hosted signing service designed for security-first blockchain operations. It provides a secure way to manage and use private keys for Ethereum and Solana blockchains without ever exposing the raw keys to connected applications.
- Encrypted at Rest - Private keys are AES-256-GCM encrypted on disk using Argon2id key derivation
- In-Memory Only - Keys are decrypted only at startup and held exclusively in memory
- Memory Protection - Uses
mlockto prevent keys from being swapped to disk,zeroizeensures secure memory cleanup - gRPC Interface - Modern, efficient API for all signing operations
- Policy Engine - Enforce transaction limits, address allowlists, rate limits, and 2FA requirements
- Multi-Chain Support - Native support for Ethereum (secp256k1) and Solana (Ed25519)
- Container Ready - Docker & Unix socket friendly with security-hardened deployment options
| Use Case | Description |
|---|---|
| Hot Wallet Management | Secure signing for exchange hot wallets with policy controls |
| Payment Processing | Automated transaction signing with value limits |
| DeFi Operations | Programmatic interaction with smart contracts |
| Custodial Services | Multi-tenant key management with isolation |
| Trading Bots | High-frequency signing with rate limiting |
locksign follows the principle of defense in depth:
- Keys never exist unencrypted on disk
- Decrypted keys are locked in memory (no swap)
- All key memory is zeroed on drop
- Policy layer prevents unauthorized operations
- Audit logging for all signing requests
- No API endpoint ever returns private keys
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Wallet App │ │ Exchange Engine │ │ Payment System │
└────────┬────────┘ └────────┬────────┘ └────────┬────────┘
│ │ │
└───────────────────────┼───────────────────────┘
│
gRPC / TLS
│
┌────────────▼────────────┐
│ Auth Layer │
│ (API Keys / Tokens) │
└────────────┬────────────┘
│
┌────────────▼────────────┐
│ Policy Engine │
│ • Address allowlists │
│ • Value limits │
│ • Rate limits │
│ • 2FA requirements │
│ • Time windows │
└────────────┬────────────┘
│
┌────────────▼────────────┐
│ Signer Core │
│ Ethereum │ Solana │
│ secp256k1│ Ed25519 │
└────────────┬────────────┘
│
┌────────────▼────────────┐
│ In-Memory Key Store │
│ • mlock (no swap) │
│ • zeroize on drop │
└────────────┬────────────┘
│
┌────────────▼────────────┐
│ Encrypted Storage │
│ • AES-256-GCM │
│ • Argon2id KDF │
└─────────────────────────┘
| Component | Purpose | Key Technologies |
|---|---|---|
| Auth Layer | Request authentication and authorization | API keys, JWT tokens |
| Policy Engine | Enforce signing rules and limits | Configurable rules, real-time evaluation |
| Signer Core | Chain-specific signing logic | secp256k1, Ed25519 |
| Memory Key Store | Secure in-memory key storage | mlock, zeroize |
| Encrypted Storage | Persistent encrypted key files | AES-256-GCM, Argon2id |
- Startup: Master password decrypts keys → loaded into locked memory
- Request: gRPC request → Auth validation → Policy check → Sign → Response
- Shutdown: All key memory zeroed → graceful termination
| Requirement | Version | Purpose |
|---|---|---|
| Rust | 1.75+ | Build toolchain |
| protoc | 3.x | Protocol Buffers compiler |
| OpenSSL | 1.1+ | TLS support |
Install protoc:
# Ubuntu/Debian
sudo apt install protobuf-compiler
# macOS
brew install protobuf
# Arch Linux
sudo pacman -S protobuf# Clone repository
git clone https://github.com/yourorg/locksign.git
cd locksign
# Build release binary
cargo build --release
# Run tests
cargo test
# Binary location
./target/release/locksign# 1. Create data directories
mkdir -p ./data/keys ./config
# 2. Set master password
export LOCKSIGN_MASTER_PASSWORD="your-secure-master-password"
# 3. Start the server
./target/release/locksign
# Server starts on 127.0.0.1:50051 by default# Check server is running
grpcurl -plaintext localhost:50051 list
# Expected output:
# locksign.ethereum.EthereumSigner
# locksign.solana.SolanaSignerCreate config.toml in the working directory or at /etc/locksign/config.toml:
#
# locksign Configuration File
#
[server]
# Network binding
listen_addr = "127.0.0.1"
port = 50051
# Unix socket (alternative to TCP)
use_unix_socket = false
unix_socket = "/var/run/locksign/locksign.sock"
# TLS Configuration
tls_enabled = false
tls_cert = "/etc/locksign/server.crt"
tls_key = "/etc/locksign/server.key"
# Connection limits
max_connections = 100
[storage]
# Path to encrypted key files
keystore_path = "./data/keys"
# Encryption settings
encryption_algorithm = "aes-256-gcm"
kdf_iterations = 100000
[security]
# Memory protection
enable_mlock = true
disable_core_dumps = true
# Key rotation (0 = disabled)
max_key_age_days = 0
# Startup requirements
require_master_password = true
[policy]
# Enable policy enforcement
enabled = true
# Path to policy rules file
rules_path = "./config/policies.json"
# Default limits
default_daily_limit = 1000000000000000000 # 1 ETH in wei
[logging]
# Log level: trace, debug, info, warn, error
level = "info"
# Log format: pretty, json
format = "pretty"
# Log file (optional)
# file = "/var/log/locksign/locksign.log"All configuration options can be overridden via environment variables:
| Variable | Description | Example |
|---|---|---|
LOCKSIGN_MASTER_PASSWORD |
Master decryption password | "secure-password" |
LOCKSIGN__SERVER__PORT |
Server port | 50052 |
LOCKSIGN__SERVER__LISTEN_ADDR |
Bind address | "0.0.0.0" |
LOCKSIGN__STORAGE__KEYSTORE_PATH |
Key storage path | "/data/keys" |
LOCKSIGN__POLICY__ENABLED |
Enable policies | true |
LOCKSIGN__LOGGING__LEVEL |
Log level | "debug" |
Note: Use double underscore __ to separate nested config sections.
Configuration is loaded in the following order (later overrides earlier):
- Built-in defaults
/etc/locksign/config.toml./config.toml- Custom path via
--configflag - Environment variables
Keys are stored as encrypted JSON files in the keystore directory:
{
"version": 1,
"key_id": "hot-wallet-1",
"chain": "ethereum",
"salt": "base64-encoded-16-byte-salt",
"nonce": "base64-encoded-12-byte-nonce",
"ciphertext": "base64-encoded-encrypted-key",
"public_key": "0x742d35Cc6634C0532925a3b844Bc454e4438f44E",
"created_at": 1704067200,
"metadata": {
"description": "Main hot wallet",
"created_by": "admin"
}
}| Parameter | Value | Description |
|---|---|---|
| Algorithm | AES-256-GCM | Authenticated encryption |
| Key Derivation | Argon2id | Memory-hard password hashing |
| Memory | 64 MB | Argon2 memory parameter |
| Iterations | 3 | Argon2 time parameter |
| Parallelism | 4 | Argon2 lanes |
| Salt | 16 bytes | Random per-key |
| Nonce | 12 bytes | Random per-encryption |
| Chain | Algorithm | Key Size | Public Key Format |
|---|---|---|---|
| Ethereum | secp256k1 | 32 bytes | Checksummed address (0x...) |
| Solana | Ed25519 | 32 bytes (seed) | Base58 public key |
use locksign::keystore::KeyLoader;
// Create loader
let loader = KeyLoader::new("./data/keys", memory_store)?;
// Import Ethereum key (32 bytes)
let eth_private_key = hex::decode("your-64-char-hex-key")?;
let info = loader.import_key(
"my-eth-wallet", // Unique key ID
ð_private_key, // Private key bytes
"ethereum", // Chain type
"master-password" // Encryption password
)?;
println!("Imported: {} -> {}", info.key_id, info.public_key);
// Import Solana key (32 bytes seed)
let sol_seed = hex::decode("your-64-char-hex-seed")?;
let info = loader.import_key(
"my-sol-wallet",
&sol_seed,
"solana",
"master-password"
)?;// Generate new Ethereum key
let info = loader.generate_key(
"new-eth-wallet",
"ethereum",
"master-password"
)?;
println!("Generated ETH key: {}", info.public_key);
// Generate new Solana key
let info = loader.generate_key(
"new-sol-wallet",
"solana",
"master-password"
)?;
println!("Generated SOL key: {}", info.public_key);// List all loaded keys
let keys = loader.list_loaded_keys();
for key in keys {
println!("{}: {} ({})", key.key_id, key.public_key, key.chain.as_str());
}// Remove from memory and delete encrypted file
loader.delete_key("old-wallet")?;Backup Strategy:
- Backup the entire
keystore_pathdirectory - Store backups encrypted (they're already encrypted, but add another layer)
- Keep backups in multiple secure locations
- Test recovery periodically
Recovery:
- Restore encrypted key files to
keystore_path - Start locksign with the correct master password
- Verify keys loaded correctly via API
The policy engine evaluates every signing request against configured rules before allowing the operation. This provides defense-in-depth even if other security measures are bypassed.
Create config/policies.json:
{
"version": 1,
"global_rules": [
{
"type": "rate_limit",
"id": "global-rate-limit",
"max_count": 1000,
"period_seconds": 3600
}
],
"key_policies": {
"hot-wallet": {
"key_id": "hot-wallet",
"enabled": true,
"rules": [
{
"type": "value_limit",
"id": "per-tx-limit",
"max_value": 100000000000000000,
"period_seconds": 0
},
{
"type": "value_limit",
"id": "daily-limit",
"max_value": 1000000000000000000,
"period_seconds": 86400
},
{
"type": "allowlist",
"id": "allowed-destinations",
"addresses": [
"0x742d35Cc6634C0532925a3b844Bc454e4438f44E",
"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
],
"allow_empty": false
}
]
},
"cold-wallet": {
"key_id": "cold-wallet",
"enabled": true,
"rules": [
{
"type": "require_2fa",
"id": "always-2fa",
"threshold": 0,
"method": "totp"
},
{
"type": "time_window",
"id": "business-hours",
"start_hour": 9,
"start_minute": 0,
"end_hour": 17,
"end_minute": 0,
"days": [1, 2, 3, 4, 5]
}
]
}
}
}Only allows transactions to pre-approved addresses.
{
"type": "allowlist",
"id": "unique-rule-id",
"addresses": [
"0x742d35Cc6634C0532925a3b844Bc454e4438f44E"
],
"allow_empty": false
}| Parameter | Type | Description |
|---|---|---|
addresses |
array | List of allowed destination addresses |
allow_empty |
bool | If true, allow any address when list is empty |
Restricts transaction values per-transaction or over a time period.
{
"type": "value_limit",
"id": "unique-rule-id",
"max_value": 1000000000000000000,
"period_seconds": 86400
}| Parameter | Type | Description |
|---|---|---|
max_value |
u128 | Maximum value in smallest unit (wei/lamports) |
period_seconds |
u64 | Time period (0 = per-transaction limit) |
Common Values:
| Amount | Wei Value |
|---|---|
| 0.1 ETH | 100000000000000000 |
| 1 ETH | 1000000000000000000 |
| 10 ETH | 10000000000000000000 |
Limits the number of signing operations over time.
{
"type": "rate_limit",
"id": "unique-rule-id",
"max_count": 100,
"period_seconds": 3600
}| Parameter | Type | Description |
|---|---|---|
max_count |
u32 | Maximum operations allowed |
period_seconds |
u64 | Time window in seconds |
Requires two-factor authentication for high-value operations.
{
"type": "require_2fa",
"id": "unique-rule-id",
"threshold": 500000000000000000,
"method": "totp"
}| Parameter | Type | Description |
|---|---|---|
threshold |
u128 | Value above which 2FA is required |
method |
string | 2FA method: "totp", "webauthn" |
Restricts signing to specific hours and days.
{
"type": "time_window",
"id": "unique-rule-id",
"start_hour": 9,
"start_minute": 0,
"end_hour": 17,
"end_minute": 0,
"days": [1, 2, 3, 4, 5]
}| Parameter | Type | Description |
|---|---|---|
start_hour |
u8 | Start hour (0-23 UTC) |
start_minute |
u8 | Start minute (0-59) |
end_hour |
u8 | End hour (0-23 UTC) |
end_minute |
u8 | End minute (0-59) |
days |
array | Days of week (0=Sun, 6=Sat), empty=all days |
- Check if key is enabled
- Evaluate global rules (all must pass)
- Evaluate key-specific rules (all must pass)
- Check 2FA requirement
- Allow or deny request
| Service | Description |
|---|---|
EthereumSigner |
Ethereum signing operations |
SolanaSigner |
Solana signing operations |
Signs an arbitrary message using EIP-191 (personal_sign).
rpc SignMessage(EthSignMessageRequest) returns (Signature);
message EthSignMessageRequest {
string key_id = 1; // Key identifier
string request_id = 2; // Unique request ID for audit
bytes message = 3; // Raw message bytes
}
message Signature {
bytes signature = 1; // 65-byte signature (r || s || v)
string public_key = 2; // Signer address
}Example (grpcurl):
grpcurl -plaintext -d '{
"key_id": "my-eth-wallet",
"request_id": "req-001",
"message": "SGVsbG8gV29ybGQ="
}' localhost:50051 locksign.ethereum.EthereumSigner/SignMessageSigns a raw Ethereum transaction.
rpc SignTransaction(EthSignTransactionRequest) returns (EthSignTransactionResponse);
message EthSignTransactionRequest {
string key_id = 1;
string request_id = 2;
oneof transaction {
EthTransaction legacy_tx = 3;
EthEIP1559Transaction eip1559_tx = 4;
}
}
message EthSignTransactionResponse {
bytes signed_transaction = 1; // RLP-encoded signed tx
string tx_hash = 2; // Transaction hash
uint32 v = 3;
bytes r = 4;
bytes s = 5;
}Returns the Ethereum address for a key.
rpc GetAddress(EthGetAddressRequest) returns (EthGetAddressResponse);
message EthGetAddressRequest {
string key_id = 1;
}
message EthGetAddressResponse {
string address = 1; // Checksummed address (0x...)
string public_key = 2; // Uncompressed public key
}Signs an arbitrary message.
rpc SignMessage(SolSignMessageRequest) returns (Signature);
message SolSignMessageRequest {
string key_id = 1;
string request_id = 2;
bytes message = 3;
}Signs a serialized Solana transaction.
rpc SignTransaction(SolSignTransactionRequest) returns (SolSignTransactionResponse);
message SolSignTransactionRequest {
string key_id = 1;
string request_id = 2;
bytes transaction = 3; // Serialized transaction message
string recent_blockhash = 4; // Optional override
}
message SolSignTransactionResponse {
bytes signed_transaction = 1;
bytes signature = 2; // 64-byte signature
}Returns the Solana public key for a key.
rpc GetPublicKey(SolGetPublicKeyRequest) returns (SolGetPublicKeyResponse);
message SolGetPublicKeyRequest {
string key_id = 1;
}
message SolGetPublicKeyResponse {
string public_key = 1; // Base58-encoded
bytes public_key_bytes = 2; // Raw 32 bytes
}| Code | Status | Description |
|---|---|---|
| 3 | INVALID_ARGUMENT |
Invalid request parameters |
| 5 | NOT_FOUND |
Key not found |
| 6 | ALREADY_EXISTS |
Key already exists |
| 7 | PERMISSION_DENIED |
Policy violation |
| 9 | FAILED_PRECONDITION |
Key is disabled |
| 12 | UNIMPLEMENTED |
Feature not implemented |
| 13 | INTERNAL |
Internal server error |
| 16 | UNAUTHENTICATED |
Authentication failed |
use locksign::proto::ethereum::ethereum_signer_client::EthereumSignerClient;
use locksign::proto::ethereum::EthSignMessageRequest;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Connect to server
let mut client = EthereumSignerClient::connect("http://127.0.0.1:50051").await?;
// Sign a message
let request = tonic::Request::new(EthSignMessageRequest {
key_id: "my-eth-wallet".to_string(),
request_id: uuid::Uuid::new_v4().to_string(),
message: b"Hello, Ethereum!".to_vec(),
});
let response = client.sign_message(request).await?;
let sig = response.into_inner();
println!("Signature: 0x{}", hex::encode(&sig.signature));
println!("Signer: {}", sig.public_key);
Ok(())
}import grpc
from locksign import ethereum_pb2, ethereum_pb2_grpc
# Connect
channel = grpc.insecure_channel('localhost:50051')
stub = ethereum_pb2_grpc.EthereumSignerStub(channel)
# Sign message
request = ethereum_pb2.EthSignMessageRequest(
key_id="my-eth-wallet",
request_id="req-001",
message=b"Hello, Ethereum!"
)
response = stub.SignMessage(request)
print(f"Signature: 0x{response.signature.hex()}")
print(f"Signer: {response.public_key}")const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');
const packageDef = protoLoader.loadSync('proto/ethereum.proto');
const proto = grpc.loadPackageDefinition(packageDef);
const client = new proto.locksign.ethereum.EthereumSigner(
'localhost:50051',
grpc.credentials.createInsecure()
);
client.SignMessage({
key_id: 'my-eth-wallet',
request_id: 'req-001',
message: Buffer.from('Hello, Ethereum!')
}, (err, response) => {
if (err) throw err;
console.log('Signature:', response.signature.toString('hex'));
console.log('Signer:', response.public_key);
});Memory containing decrypted keys is locked using mlock() system call to prevent swapping to disk.
// Keys are automatically locked when loaded
let locked_memory = LockedMemory::from_vec(key_bytes)?;
// Memory is unlocked and zeroed on dropRequirements:
- Linux: May need
CAP_IPC_LOCKcapability or increasedRLIMIT_MEMLOCK - Docker: Add
--cap-add=IPC_LOCKor increase memlock limit
All sensitive memory is zeroed when no longer needed:
// SecureBytes automatically zeros on drop
let key = SecureBytes::new(private_key_vec);
// When `key` goes out of scope, memory is zeroedCore dumps are disabled at startup to prevent key leakage:
// Automatically called at startup
setup_memory_protection()?;
// Sets RLIMIT_CORE to 0Always enable TLS in production:
[server]
tls_enabled = true
tls_cert = "/etc/locksign/server.crt"
tls_key = "/etc/locksign/server.key"Generate self-signed certificate (testing only):
openssl req -x509 -newkey rsa:4096 -keyout server.key -out server.crt -days 365 -nodesFor local applications, prefer Unix sockets over TCP:
[server]
use_unix_socket = true
unix_socket = "/var/run/locksign/locksign.sock"| Practice | Importance | Description |
|---|---|---|
| ✅ Enable TLS | Critical | Encrypt all network traffic |
| ✅ Use Unix sockets | High | For local apps, avoid network exposure |
| ✅ Enable mlock | High | Prevent key swapping |
| ✅ Disable core dumps | High | Prevent key leakage |
| ✅ Configure policies | High | Limit blast radius |
| ✅ Use API authentication | High | Control access |
| ✅ Enable audit logging | Medium | Track all operations |
| ✅ Rotate keys | Medium | Limit key exposure time |
| ✅ Backup encrypted keys | Medium | Disaster recovery |
| ✅ Run as non-root | Medium | Principle of least privilege |
| Threat | Mitigation |
|---|---|
| Network eavesdropping | TLS encryption |
| Disk theft | AES-256-GCM encryption |
| Memory dump | mlock, zeroize, no core dumps |
| Unauthorized signing | Policy engine, authentication |
| Insider threat | Audit logging, 2FA |
| Key exhaustion | Rate limiting |
| Phishing | Address allowlists |
# Build stage
FROM rust:1.75-alpine AS builder
RUN apk add --no-cache musl-dev protobuf-dev openssl-dev
WORKDIR /app
COPY . .
RUN cargo build --release
# Runtime stage
FROM alpine:3.19
RUN apk add --no-cache ca-certificates libgcc
# Create non-root user
RUN addgroup -g 1000 locksign && \
adduser -D -u 1000 -G locksign locksign
# Copy binary
COPY --from=builder /app/target/release/locksign /usr/local/bin/
# Create directories
RUN mkdir -p /data/keys /etc/locksign && \
chown -R locksign:locksign /data /etc/locksign
USER locksign
WORKDIR /data
EXPOSE 50051
ENTRYPOINT ["locksign"]version: '3.8'
services:
locksign:
build: .
container_name: locksign
restart: unless-stopped
# Security hardening
cap_drop:
- ALL
cap_add:
- IPC_LOCK # For mlock
read_only: true
security_opt:
- no-new-privileges:true
# Environment
environment:
- LOCKSIGN_MASTER_PASSWORD=${MASTER_PASSWORD}
- LOCKSIGN__SERVER__PORT=50051
- LOCKSIGN__LOGGING__LEVEL=info
# Volumes
volumes:
- ./data/keys:/data/keys:ro
- ./config:/etc/locksign:ro
- locksign-tmp:/tmp
# Network
ports:
- "127.0.0.1:50051:50051"
# Resources
deploy:
resources:
limits:
memory: 256M
reservations:
memory: 64M
# Health check
healthcheck:
test: ["CMD", "grpcurl", "-plaintext", "localhost:50051", "list"]
interval: 30s
timeout: 10s
retries: 3
volumes:
locksign-tmp:| Flag | Purpose |
|---|---|
cap_drop: ALL |
Remove all Linux capabilities |
cap_add: IPC_LOCK |
Allow memory locking |
read_only: true |
Read-only root filesystem |
no-new-privileges |
Prevent privilege escalation |
user: 1000:1000 |
Run as non-root |
# 1. Create directories
mkdir -p ./data/keys ./config
# 2. Set password securely
export MASTER_PASSWORD=$(cat /path/to/secure/password)
# 3. Start with docker-compose
docker-compose up -d
# 4. Check logs
docker-compose logs -f locksign
# 5. Verify health
docker-compose exec locksign grpcurl -plaintext localhost:50051 listCause: Master password environment variable is missing.
Solution:
export LOCKSIGN_MASTER_PASSWORD="your-password"Cause: Insufficient permissions for mlock.
Solutions:
- Increase memlock limit:
# Add to /etc/security/limits.conf
locksign soft memlock unlimited
locksign hard memlock unlimited- Or run with capability:
sudo setcap cap_ipc_lock=+ep ./locksign- In Docker, add:
cap_add:
- IPC_LOCKCause: Key ID doesn't exist or wasn't loaded.
Solutions:
- Check key files exist in keystore_path
- Verify master password is correct
- Check server logs for load errors
Cause: Request blocked by policy rules.
Solutions:
- Check policy configuration
- Verify address is in allowlist
- Check value/rate limits
- Ensure within time window
Enable debug logging:
export RUST_LOG=debug
./locksignOr in config:
[logging]
level = "debug"# Check if server is responding
grpcurl -plaintext localhost:50051 list
# Get server health (if implemented)
grpcurl -plaintext localhost:50051 locksign.health.Health/Checklocksign/
├── Cargo.toml # Dependencies and metadata
├── build.rs # Protobuf compilation
├── README.md # Quick start guide
├── proto/ # Protocol Buffer definitions
│ ├── common.proto # Shared types
│ ├── ethereum.proto # Ethereum service
│ └── solana.proto # Solana service
└── src/
├── main.rs # Entry point, server bootstrap
├── errors.rs # Error types and handling
├── config/
│ └── mod.rs # Configuration loading
├── api/
│ ├── mod.rs # API module
│ ├── grpc.rs # gRPC service implementations
│ └── auth.rs # Authentication middleware
├── policy/
│ ├── mod.rs # Policy module
│ ├── engine.rs # Policy evaluation engine
│ └── rules.rs # Rule type definitions
├── keystore/
│ ├── mod.rs # Keystore module
│ ├── encrypted.rs # Encrypted file storage
│ ├── memory.rs # In-memory key store
│ └── loader.rs # Key loading utilities
├── signer/
│ ├── mod.rs # Signer module
│ ├── ethereum.rs # Ethereum signing
│ └── solana.rs # Solana signing
├── crypto/
│ ├── mod.rs # Crypto module
│ ├── eth.rs # secp256k1 operations
│ └── sol.rs # Ed25519 operations
└── security/
├── mod.rs # Security module
├── zeroize.rs # Secure memory zeroing
└── mlock.rs # Memory locking
- ✅ Encrypted key storage
- ✅ In-memory key management
- ✅ Ethereum signing (secp256k1)
- ✅ Solana signing (Ed25519)
- ✅ Policy engine
- ✅ gRPC API
- Multi-account management
- Balance tracking
- Transaction history
- HD wallet derivation
- Order signing
- Batch operations
- Webhook notifications
- Audit log export
- Tenant isolation
- RBAC (Role-Based Access Control)
- API key management
- Usage quotas
- HSM integration
- MPC (Multi-Party Computation)
- Threshold signatures
- Hardware key support
MIT License - See LICENSE file for details.
- GitHub Issues: Report bugs and feature requests
This documentation is for locksign version 0.1.0