This document provides visual representations of the Solana Secure Signer architecture.
┌───────────────────────────────────────────────────────────────────────┐
│ USER APPLICATION LAYER │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Your Python CLI (Coldstar SOL) │ │
│ │ • Transaction building │ │
│ │ • Network communication │ │
│ │ • User interface │ │
│ └───────────────┬────────────────────────────┬─────────────────┘ │
│ │ │ │
│ │ │ │
└──────────────────┼────────────────────────────┼───────────────────────┘
│ │
▼ ▼
┌───────────────────────────────────────────────────────────────────────┐
│ INTEGRATION LAYER │
│ ┌──────────────────────────┐ ┌────────────────────────────────┐ │
│ │ FFI Integration │ │ CLI Subprocess Integration │ │
│ │ (Fast, Direct) │ │ (Portable, Isolated) │ │
│ │ │ │ │ │
│ │ • ctypes bindings │ │ • subprocess.run() │ │
│ │ • Shared library │ │ • stdin/stdout JSON │ │
│ │ • Direct function calls │ │ • Process isolation │ │
│ └────────────┬─────────────┘ └────────────┬───────────────────┘ │
│ │ │ │
└───────────────┼──────────────────────────────┼───────────────────────┘
│ │
└──────────┬───────────────────┘
▼
┌───────────────────────────────────────────────────────────────────────┐
│ RUST SIGNING CORE │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Public API │ │
│ │ • sign_transaction(container, passphrase, tx) │ │
│ │ • create_encrypted_container(key, passphrase) │ │
│ └────────────┬────────────────────────────────────────────────┘ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Signing Logic (signer.rs) │ │
│ │ 1. Derive decryption key (Argon2id) │ │
│ │ 2. Decrypt ciphertext (AES-256-GCM) │ │
│ │ 3. Copy to secure buffer │ │
│ │ 4. Sign with Ed25519 │ │
│ │ 5. Return signature │ │
│ └────────────┬────────────────────────────────────────────────┘ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Secure Memory (secure_memory.rs) │ │
│ │ • SecureBuffer: mlock/VirtualLock │ │
│ │ • Automatic zeroization on drop │ │
│ │ • Panic-safe cleanup │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ Dependencies: ed25519-dalek, aes-gcm, argon2, zeroize, region │
└───────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ PHASE 1: INPUT PREPARATION │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Python Application │
│ ├─ Load encrypted container (JSON file) │
│ ├─ Get passphrase (user input, secure) │
│ └─ Prepare unsigned transaction bytes │
│ │
│ Status: ✅ No sensitive data yet │
└──────────────────────────┬──────────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────────┐
│ PHASE 2: ENTER RUST SECURE ZONE │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ FFI Boundary / CLI Invocation │
│ ├─ Serialize inputs to JSON/bytes │
│ ├─ Call: sign_transaction_ffi() or subprocess │
│ └─ Enter Rust memory space │
│ │
│ Status: ✅ Encrypted data crosses boundary │
└──────────────────────────┬──────────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────────┐
│ PHASE 3: KEY DERIVATION │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Argon2id Key Derivation │
│ ├─ Input: passphrase + salt from container │
│ ├─ Compute: memory-hard hash │
│ ├─ Output: 32-byte AES key │
│ └─ Zeroize: passphrase copy │
│ │
│ Status: ✅ Passphrase zeroized, no plaintext key yet │
└──────────────────────────┬──────────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────────┐
│ PHASE 4: MEMORY ALLOCATION & LOCKING │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ SecureKeyBuffer::new_secret_key() │
│ ├─ Allocate: 32 bytes on heap │
│ ├─ Lock: mlock()/VirtualLock() syscall │
│ ├─ Verify: lock succeeded or return error │
│ └─ Initialize: fill with zeros │
│ │
│ Memory State: │
│ ┌────────────────────────────────────┐ │
│ │ Locked RAM (32 bytes) │ │
│ │ [0, 0, 0, ..., 0] (all zeros) │ │
│ │ Status: LOCKED, not swappable │ │
│ └────────────────────────────────────┘ │
│ │
│ Status: ✅ Secure buffer ready, no sensitive data yet │
└──────────────────────────┬──────────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────────┐
│ PHASE 5: DECRYPTION │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ AES-256-GCM Decryption │
│ ├─ Input: ciphertext + nonce + derived key │
│ ├─ Decrypt: to temporary Vec<u8> │
│ ├─ Verify: authentication tag │
│ └─ Output: plaintext key (32 bytes) │
│ │
│ Temporary State: │
│ ┌────────────────────────────────────┐ │
│ │ Stack/Heap (decrypted_bytes) │ │
│ │ [k₀, k₁, k₂, ..., k₃₁] │ │
│ │ Status: PLAINTEXT KEY! ⚠️ │ │
│ └────────────────────────────────────┘ │
│ │
│ Status: ⚠️ CRITICAL - Plaintext key in temporary buffer │
└──────────────────────────┬──────────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────────┐
│ PHASE 6: SECURE TRANSFER │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Copy to Locked Buffer │
│ ├─ Copy: decrypted_bytes → secure_key buffer │
│ ├─ Zeroize: decrypted_bytes (constant-time) │
│ └─ Verify: no copies remain │
│ │
│ Before: After: │
│ ┌──────────────────────┐ ┌──────────────────────┐ │
│ │ Temporary Buffer │ │ Temporary Buffer │ │
│ │ [k₀, k₁, ..., k₃₁] │ ───────> │ [0, 0, ..., 0] │ │
│ │ (plaintext key) │ │ (zeroized) │ │
│ └──────────────────────┘ └──────────────────────┘ │
│ │
│ ┌──────────────────────┐ ┌──────────────────────┐ │
│ │ Locked Buffer │ │ Locked Buffer │ │
│ │ [0, 0, ..., 0] │ ───────> │ [k₀, k₁, ..., k₃₁] │ │
│ │ (empty) │ │ (plaintext key) │ │
│ └──────────────────────┘ └──────────────────────┘ │
│ │
│ Status: ✅ Key in locked memory, temporary copy zeroized │
└──────────────────────────┬──────────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────────┐
│ PHASE 7: SIGNING │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Ed25519 Signing │
│ ├─ Create: SigningKey from locked buffer (borrow, no copy) │
│ ├─ Sign: signature = signing_key.sign(transaction) │
│ ├─ Output: 64-byte signature │
│ └─ Note: SigningKey borrows, doesn't own the key │
│ │
│ Memory State: │
│ ┌────────────────────────────────────┐ │
│ │ Locked Buffer │ │
│ │ [k₀, k₁, k₂, ..., k₃₁] ◄─────── SigningKey borrows │
│ │ Status: LOCKED, being used │ │
│ └────────────────────────────────────┘ │
│ │
│ Status: ⚠️ Key still in memory, being used for signing │
└──────────────────────────┬──────────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────────┐
│ PHASE 8: RESULT PREPARATION │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Build Signed Transaction │
│ ├─ Allocate: new Vec<u8> for result │
│ ├─ Append: signature (64 bytes, public data) │
│ ├─ Append: original transaction │
│ └─ Return: SignedTransaction struct │
│ │
│ Status: ✅ Public signature ready, key still in locked buffer │
└──────────────────────────┬──────────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────────┐
│ PHASE 9: AUTOMATIC CLEANUP (Drop) │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ SecureKeyBuffer::drop() automatically called │
│ ├─ Trigger: secure_key goes out of scope │
│ ├─ Zeroize: data.zeroize() (constant-time) │
│ ├─ Unlock: munlock()/VirtualUnlock() │
│ └─ Extra: manual write_bytes for paranoia │
│ │
│ Before Drop: After Drop: │
│ ┌──────────────────────┐ ┌──────────────────────┐ │
│ │ Locked Buffer │ │ Unlocked Memory │ │
│ │ [k₀, k₁, ..., k₃₁] │ ───────> │ [0, 0, ..., 0] │ │
│ │ (plaintext key) │ │ (zeroized) │ │
│ │ Status: LOCKED │ │ Status: FREE │ │
│ └──────────────────────┘ └──────────────────────┘ │
│ │
│ Note: This happens even if signing panicked! │
│ │
│ Status: ✅ Key completely erased from memory │
└──────────────────────────┬──────────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────────────┐
│ PHASE 10: RETURN TO PYTHON │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ FFI Boundary / CLI Output │
│ ├─ Serialize: SignedTransaction to JSON │
│ ├─ Return: to Python via FFI or stdout │
│ └─ Python receives: signature + signed transaction │
│ │
│ Data Returned to Python: │
│ • signature: [s₀, s₁, ..., s₆₃] (64 bytes, public) │
│ • signed_tx: signature + transaction (public) │
│ │
│ Status: ✅ Only public data crosses boundary │
└─────────────────────────────────────────────────────────────────────┘
SECURITY SUMMARY:
═══════════════════════════════════════════════════════════════════════
• Plaintext key existed in locked memory for phases 6-8 only
• Key was zeroized immediately after use (phase 9)
• No copies of plaintext key exist outside locked buffer
• Cleanup is guaranteed even on panic
• Only public data (signature) returned to Python
• Private key NEVER exposed to Python memory space
═══════════════════════════════════════════════════════════════════════
INPUT (Python) SECURE ZONE (Rust) OUTPUT (Python)
═══════════════ ═══════════════════ ═══════════════
┌──────────────┐ ┌──────────────┐
│ Encrypted │ │ Signature │
│ Container │────────┐ │ (64 bytes) │
│ (JSON) │ │ │ │
└──────────────┘ │ └──────────────┘
│ ▲
┌──────────────┐ │ ┌─────────────────────┐ │
│ Passphrase │────────┼────────>│ Rust Signing Core │────────────────┤
│ (string) │ │ │ │ │
└──────────────┘ │ │ 1. Decrypt │ ┌──────────────┐
│ │ 2. Lock memory │ │ Signed │
┌──────────────┐ │ │ 3. Sign │ │ Transaction │
│ Unsigned TX │────────┘ │ 4. Zeroize │ │ (bytes) │
│ (bytes) │ │ 5. Return │ └──────────────┘
└──────────────┘ └─────────────────────┘ ▲
│
Private key exists │
here only! Never │
leaves this box. │
│
┌──────────────────┐ │
│ Locked Memory │ │
│ ┌────────────┐ │ │
│ │ Private │ │ │
│ │ Key │ │ │
│ │ (32 bytes) │ │ │
│ └────────────┘ │ │
│ Auto-zeroized! │ │
└──────────────────┘ │
│
══════════════════════════════════════════════════════════════════════════│══
FFI / CLI Boundary │
══════════════════════════════════════════════════════════════════════════╧══
Legend:
────> Data flow
═══> Security boundary
┌───┐ Data container
╔═══════════════════════════════════════════════════════════════════════╗
║ MEMORY SAFETY GUARANTEES ║
╚═══════════════════════════════════════════════════════════════════════╝
Guarantee 1: MEMORY LOCKING
┌─────────────────────────────────────────────────────────────────────┐
│ RAM │
│ ┌───────────────────────────────────┐ │
│ │ Locked Pages (via mlock) │ ◄── Private key here │
│ │ Cannot be swapped to disk │ │
│ └───────────────────────────────────┘ │
│ ║ │
│ ║ BLOCKED (no swap) │
│ ▼ │
│ ┌───────────────────────────────────┐ │
│ │ Disk / Swap File │ ◄── Key never reaches here │
│ │ ✘ Private key CANNOT be here │ │
│ └───────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
Guarantee 2: ZEROIZATION
┌─────────────────────────────────────────────────────────────────────┐
│ Before Drop: After Drop: │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ [k₀, k₁, ..., k₃₁]│ │ [0, 0, ..., 0] │ │
│ │ Private key │ ────────> │ All zeros │ │
│ │ SENSITIVE! ⚠️ │ │ Safe ✅ │ │
│ └─────────────────┘ └─────────────────┘ │
│ │
│ Method: zeroize crate (constant-time, not optimized out) │
└─────────────────────────────────────────────────────────────────────┘
Guarantee 3: PANIC SAFETY
┌─────────────────────────────────────────────────────────────────────┐
│ Normal Flow: Panic Flow: │
│ ┌──────────┐ ┌──────────┐ │
│ │ Signing │ │ Signing │ │
│ │ Success │ │ PANIC! │ │
│ └────┬─────┘ └────┬─────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────┐ ┌──────────┐ │
│ │ Drop │ │ Drop │ ◄── Drop ALWAYS runs │
│ │ Runs │ │ Runs │ │
│ └────┬─────┘ └────┬─────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────┐ ┌──────────┐ │
│ │ Zeroized │ │ Zeroized │ ◄── Key erased in both cases │
│ └──────────┘ └──────────┘ │
└─────────────────────────────────────────────────────────────────────┘
Guarantee 4: NO COPIES
┌─────────────────────────────────────────────────────────────────────┐
│ ONLY ONE instance of plaintext key exists: │
│ │
│ ┌────────────────────────────────────────────────┐ │
│ │ SecureKeyBuffer │ │
│ │ ┌──────────────────────────────────┐ │ │
│ │ │ Private Key (32 bytes) │ ◄─────── Single instance │
│ │ │ [k₀, k₁, k₂, ..., k₃₁] │ │ │
│ │ └──────────────────────────────────┘ │ │
│ │ ▲ │ │
│ │ │ borrow (no copy) │ │
│ │ │ │ │
│ │ ┌─────────┴──────────┐ │ │
│ │ │ SigningKey │ │ │
│ │ │ (borrows data) │ │ │
│ │ └─────────────────────┘ │ │
│ └────────────────────────────────────────────────┘ │
│ │
│ All other buffers (decryption, etc.) are zeroized immediately │
└─────────────────────────────────────────────────────────────────────┘
Guarantee 5: EPHEMERAL LIFECYCLE
┌─────────────────────────────────────────────────────────────────────┐
│ Function Scope: │
│ │
│ fn sign_transaction(...) { │
│ let secure_key = SecureKeyBuffer::new(); ◄── Created │
│ // ... decrypt into secure_key ... │
│ // ... use for signing ... │
│ return signature; │
│ } ◄── secure_key dropped here, automatic cleanup │
│ │
│ Key exists ONLY during function execution │
│ No global variables, no caching, no persistence │
└─────────────────────────────────────────────────────────────────────┘
╔═══════════════════════════════════════════════════════════════════════╗
║ INTEGRATION MODE COMPARISON ║
╚═══════════════════════════════════════════════════════════════════════╝
┌─────────────────────────────────────────────────────────────────────┐
│ FFI Mode (libsolana_secure_signer.so/dll/dylib) │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Python ←──(ctypes)──→ Rust Library │
│ │
│ Pros: │
│ ✅ Fastest (no process overhead) │
│ ✅ Direct function calls │
│ ✅ Minimal latency (~0.1ms overhead) │
│ │
│ Cons: │
│ ⚠️ Requires compiled library for platform │
│ ⚠️ Shared memory space (less isolated) │
│ │
│ Use When: │
│ • Production deployments │
│ • High transaction throughput │
│ • Minimal latency required │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ CLI Mode (solana-signer binary) │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Python ──(subprocess)──→ Rust Binary ──(exit)──→ All memory freed │
│ │
│ Pros: │
│ ✅ Maximum isolation (separate process) │
│ ✅ Platform-agnostic (just needs binary) │
│ ✅ Automatic cleanup (process exit) │
│ ✅ No FFI complexity │
│ │
│ Cons: │
│ ⚠️ Slower (process startup ~10-50ms) │
│ ⚠️ IPC overhead (JSON serialization) │
│ │
│ Use When: │
│ • Development/testing │
│ • Maximum security isolation │
│ • Cross-platform portability │
│ • Occasional signing (not high frequency) │
└─────────────────────────────────────────────────────────────────────┘
Performance Comparison:
┌────────────────┬──────────┬───────────┬──────────────┐
│ Operation │ FFI │ CLI │ Difference │
├────────────────┼──────────┼───────────┼──────────────┤
│ Overhead │ ~0.1ms │ ~10-50ms │ 100-500x │
│ Throughput │ High │ Low │ - │
│ Memory Safety │ Good │ Excellent│ CLI better │
│ Portability │ Medium │ High │ CLI better │
└────────────────┴──────────┴───────────┴──────────────┘
This completes the visual documentation! 🎉