diff --git a/apps/faucet/Cargo.toml b/apps/faucet/Cargo.toml index c771715..ceb98fc 100644 --- a/apps/faucet/Cargo.toml +++ b/apps/faucet/Cargo.toml @@ -50,3 +50,4 @@ synor-types = { path = "../../crates/synor-types" } [dev-dependencies] tokio-test = "0.4" +tempfile = "3.10" diff --git a/apps/faucet/src/main.rs b/apps/faucet/src/main.rs index 83331ea..7e8a222 100644 --- a/apps/faucet/src/main.rs +++ b/apps/faucet/src/main.rs @@ -2,6 +2,16 @@ //! //! A simple HTTP service that dispenses test SYNOR tokens to developers. //! Includes rate limiting and cooldown periods to prevent abuse. +//! +//! # Security +//! +//! The faucet wallet key should be stored securely: +//! - **Development**: Environment variable `FAUCET_WALLET_KEY` +//! - **Production**: File-based secrets in `/run/secrets/FAUCET_WALLET_KEY` +//! +//! See the `secrets` module for configuration options. + +mod secrets; use std::collections::HashMap; use std::net::SocketAddr; @@ -57,7 +67,23 @@ impl Default for FaucetConfig { } impl FaucetConfig { - /// Load configuration from environment variables. + /// Load configuration from environment variables and secrets provider. + /// + /// # Secrets + /// + /// The wallet key is loaded securely via the secrets provider: + /// - Set `SECRETS_DIR` to specify a secrets directory + /// - Or mount secrets to `/run/secrets/` (auto-detected) + /// - Falls back to `FAUCET_WALLET_KEY` env var with a warning + /// + /// # Environment Variables + /// + /// - `SYNOR_RPC_URL`: RPC endpoint URL + /// - `FAUCET_AMOUNT`: Tokens per request (in sompi) + /// - `FAUCET_COOLDOWN`: Cooldown seconds between requests + /// - `FAUCET_RATE_LIMIT`: Max requests per minute per IP + /// - `FAUCET_LISTEN_ADDR`: Server listen address + /// - `FAUCET_CORS_ORIGINS`: Comma-separated allowed origins pub fn from_env() -> Self { let mut config = FaucetConfig::default(); @@ -89,8 +115,19 @@ impl FaucetConfig { } } - if let Ok(key) = std::env::var("FAUCET_WALLET_KEY") { + // Load wallet key securely via secrets provider + let secrets = secrets::create_secret_provider(); + if let Some(key) = secrets.get("FAUCET_WALLET_KEY") { + info!( + provider = secrets.provider_name(), + "Loaded faucet wallet key from secrets provider" + ); config.wallet_key = Some(key); + } else { + warn!( + "No FAUCET_WALLET_KEY found. The faucet will not be able to send transactions. \ + Set SECRETS_DIR or mount secrets to /run/secrets/." + ); } if let Ok(origins) = std::env::var("FAUCET_CORS_ORIGINS") { diff --git a/apps/faucet/src/secrets.rs b/apps/faucet/src/secrets.rs new file mode 100644 index 0000000..14d5bbd --- /dev/null +++ b/apps/faucet/src/secrets.rs @@ -0,0 +1,240 @@ +//! Secrets management for the faucet. +//! +//! Provides a pluggable secrets provider abstraction that supports: +//! - Environment variables (development only) +//! - File-based secrets (simple production) +//! - External secrets managers (enterprise production) +//! +//! # Security Best Practices +//! +//! 1. **Never use environment variables in production** - They can be exposed +//! in process listings, logs, and container orchestration UIs. +//! +//! 2. **Use file-based secrets** as minimum security - Mount a secret file +//! with restricted permissions (0400) from a secure volume. +//! +//! 3. **Use a secrets manager** for production - AWS Secrets Manager, +//! HashiCorp Vault, or similar provides rotation and audit logging. + +use std::path::Path; +use tracing::{info, warn}; + +/// Secret provider trait for pluggable secret storage backends. +pub trait SecretProvider: Send + Sync { + /// Get a secret value by name. + fn get(&self, name: &str) -> Option; + + /// Provider name for logging. + fn provider_name(&self) -> &'static str; +} + +/// Environment variable secret provider (development only). +/// +/// # Warning +/// +/// Environment variables are NOT secure for production use: +/// - Visible in `/proc//environ` on Linux +/// - Exposed in `ps auxe` output +/// - May be logged by container orchestrators +/// - No access control or audit logging +pub struct EnvSecretProvider; + +impl SecretProvider for EnvSecretProvider { + fn get(&self, name: &str) -> Option { + std::env::var(name).ok() + } + + fn provider_name(&self) -> &'static str { + "environment" + } +} + +/// File-based secret provider. +/// +/// Reads secrets from files in a secrets directory. Each secret is stored +/// as a separate file named after the secret key. +/// +/// # Example +/// +/// ```text +/// /run/secrets/ +/// ├── FAUCET_WALLET_KEY +/// └── DATABASE_PASSWORD +/// ``` +/// +/// # Security Notes +/// +/// - Set file permissions to 0400 (owner read only) +/// - Mount from tmpfs or encrypted volume +/// - Use immutable container images +pub struct FileSecretProvider { + secrets_dir: std::path::PathBuf, +} + +impl FileSecretProvider { + /// Create a new file-based secret provider. + pub fn new(secrets_dir: impl AsRef) -> Self { + Self { + secrets_dir: secrets_dir.as_ref().to_path_buf(), + } + } +} + +impl SecretProvider for FileSecretProvider { + fn get(&self, name: &str) -> Option { + let path = self.secrets_dir.join(name); + std::fs::read_to_string(&path) + .ok() + .map(|s| s.trim().to_string()) + } + + fn provider_name(&self) -> &'static str { + "file" + } +} + +/// Chained secret provider that tries multiple providers in order. +/// +/// Useful for fallback scenarios, e.g., try file first, then env. +#[allow(dead_code)] +pub struct ChainedSecretProvider { + providers: Vec>, +} + +#[allow(dead_code)] +impl ChainedSecretProvider { + /// Create a new chained provider with the given providers. + pub fn new(providers: Vec>) -> Self { + Self { providers } + } +} + +impl SecretProvider for ChainedSecretProvider { + fn get(&self, name: &str) -> Option { + for provider in &self.providers { + if let Some(value) = provider.get(name) { + return Some(value); + } + } + None + } + + fn provider_name(&self) -> &'static str { + "chained" + } +} + +/// Creates the appropriate secret provider based on configuration. +/// +/// Priority order: +/// 1. If SECRETS_DIR is set, use file-based provider +/// 2. If SECRETS_PROVIDER=file, use file-based provider with /run/secrets +/// 3. Fall back to environment variables with a warning +pub fn create_secret_provider() -> Box { + // Check for explicit secrets directory + if let Ok(dir) = std::env::var("SECRETS_DIR") { + info!(dir = %dir, "Using file-based secrets provider"); + return Box::new(FileSecretProvider::new(dir)); + } + + // Check for explicit provider type + if let Ok(provider) = std::env::var("SECRETS_PROVIDER") { + match provider.as_str() { + "file" => { + let dir = + std::env::var("SECRETS_DIR").unwrap_or_else(|_| "/run/secrets".to_string()); + info!(dir = %dir, "Using file-based secrets provider"); + return Box::new(FileSecretProvider::new(dir)); + } + "env" => { + warn!( + "Using environment variable secrets provider - NOT RECOMMENDED FOR PRODUCTION" + ); + return Box::new(EnvSecretProvider); + } + _ => { + warn!(provider = %provider, "Unknown secrets provider, falling back to environment"); + } + } + } + + // Default: check for /run/secrets directory (common in Docker/Kubernetes) + if Path::new("/run/secrets").is_dir() { + info!("Detected /run/secrets directory, using file-based secrets"); + return Box::new(FileSecretProvider::new("/run/secrets")); + } + + // Fallback to environment variables with warning + warn!( + "Using environment variable secrets provider. \ + This is NOT recommended for production. \ + Set SECRETS_DIR or mount secrets to /run/secrets." + ); + Box::new(EnvSecretProvider) +} + +/// Zeroize a string in memory (best effort). +/// +/// This attempts to overwrite the string's memory before dropping. +/// Note: Rust's String may have been reallocated, so this isn't perfect. +#[allow(dead_code)] +pub fn zeroize_string(mut s: String) { + // SAFETY: We're about to drop this string anyway + unsafe { + let bytes = s.as_bytes_mut(); + std::ptr::write_bytes(bytes.as_mut_ptr(), 0, bytes.len()); + } + drop(s); +} + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::tempdir; + + #[test] + fn test_file_secret_provider() { + let dir = tempdir().unwrap(); + let secret_path = dir.path().join("TEST_SECRET"); + std::fs::write(&secret_path, "secret_value\n").unwrap(); + + let provider = FileSecretProvider::new(dir.path()); + assert_eq!( + provider.get("TEST_SECRET"), + Some("secret_value".to_string()) + ); + assert_eq!(provider.get("NONEXISTENT"), None); + } + + #[test] + fn test_env_secret_provider() { + std::env::set_var("TEST_ENV_SECRET", "env_value"); + let provider = EnvSecretProvider; + assert_eq!( + provider.get("TEST_ENV_SECRET"), + Some("env_value".to_string()) + ); + std::env::remove_var("TEST_ENV_SECRET"); + } + + #[test] + fn test_chained_provider() { + let dir = tempdir().unwrap(); + let secret_path = dir.path().join("FILE_SECRET"); + std::fs::write(&secret_path, "file_value").unwrap(); + + std::env::set_var("ENV_SECRET", "env_value"); + + let provider = ChainedSecretProvider::new(vec![ + Box::new(FileSecretProvider::new(dir.path())), + Box::new(EnvSecretProvider), + ]); + + // File provider takes precedence + assert_eq!(provider.get("FILE_SECRET"), Some("file_value".to_string())); + // Falls through to env provider + assert_eq!(provider.get("ENV_SECRET"), Some("env_value".to_string())); + + std::env::remove_var("ENV_SECRET"); + } +} diff --git a/apps/synord/src/config.rs b/apps/synord/src/config.rs index 7ef3c3d..a0ecd9f 100644 --- a/apps/synord/src/config.rs +++ b/apps/synord/src/config.rs @@ -261,40 +261,54 @@ impl Default for P2PConfig { impl P2PConfig { /// Creates config for a network. + /// + /// # Environment Variables + /// + /// The following environment variables can override seed configuration: + /// - `SYNOR_BOOTSTRAP_PEERS`: Comma-separated list of bootstrap peer multiaddrs + /// Format: `/dns4//tcp//p2p/` + /// Example: `SYNOR_BOOTSTRAP_PEERS=/dns4/seed1.synor.cc/tcp/17511/p2p/12D3KooWAbCd...` + /// + /// This allows operators to configure seed nodes without recompiling. pub fn for_network(network: &str) -> Self { let mut config = P2PConfig::default(); match network { "mainnet" => { config.listen_addr = "/ip4/0.0.0.0/tcp/16511".to_string(); - config.seeds = vec![ - // Mainnet seeds - geographically distributed - // Format: /dns4//tcp//p2p/ - // Peer IDs will be populated after seed node deployment - "/dns4/seed1.synor.cc/tcp/16511".to_string(), - "/dns4/seed2.synor.cc/tcp/16511".to_string(), - "/dns4/seed3.synor.cc/tcp/16511".to_string(), - ]; + // Mainnet seeds - will be populated after mainnet seed node deployment + // IMPORTANT: Addresses must include /p2p/ for secure dialing + // Override with SYNOR_BOOTSTRAP_PEERS env var after deployment + config.seeds = vec![]; } "testnet" => { config.listen_addr = "/ip4/0.0.0.0/tcp/17511".to_string(); - config.seeds = vec![ - // Testnet seeds - geographically distributed - // North America (US-East) - "/dns4/testnet-seed1.synor.cc/tcp/17511".to_string(), - // Europe (Frankfurt) - "/dns4/testnet-seed2.synor.cc/tcp/17511".to_string(), - // Asia (Singapore) - "/dns4/testnet-seed3.synor.cc/tcp/17511".to_string(), - ]; + // Testnet seeds - populate after deploying seed nodes + // IMPORTANT: Addresses must include /p2p/ for secure dialing + // Override with SYNOR_BOOTSTRAP_PEERS env var after deployment + config.seeds = vec![]; } "devnet" => { config.listen_addr = "/ip4/0.0.0.0/tcp/18511".to_string(); + // Devnet uses mDNS for local discovery, no seeds needed config.seeds = vec![]; } _ => {} }; + // Allow environment variable override for bootstrap peers + // This enables operators to configure seeds at runtime + if let Ok(peers_env) = std::env::var("SYNOR_BOOTSTRAP_PEERS") { + let peers: Vec = peers_env + .split(',') + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()) + .collect(); + if !peers.is_empty() { + config.seeds = peers; + } + } + config } } diff --git a/crates/synor-crypto-wasm/Cargo.toml b/crates/synor-crypto-wasm/Cargo.toml index b2aafc5..89e9217 100644 --- a/crates/synor-crypto-wasm/Cargo.toml +++ b/crates/synor-crypto-wasm/Cargo.toml @@ -2,32 +2,51 @@ name = "synor-crypto-wasm" version = "0.1.0" edition = "2021" -description = "WASM bindings for Synor cryptographic operations (ML-DSA/Dilithium)" -license = "MIT" +description = "WASM-compatible cryptography for Synor web wallet" +license = "MIT OR Apache-2.0" [lib] crate-type = ["cdylib", "rlib"] -[features] -default = ["console_error_panic_hook"] - [dependencies] +# WASM bindings wasm-bindgen = "0.2" js-sys = "0.3" -console_error_panic_hook = { version = "0.1.7", optional = true } + +# Classical cryptography (pure Rust, WASM compatible) +ed25519-dalek = { version = "2.1", default-features = false, features = ["rand_core"] } +rand = { version = "0.8", default-features = false, features = ["std_rng"] } getrandom = { version = "0.2", features = ["js"] } -# Pure Rust ML-DSA (Dilithium) - WASM compatible -ml-dsa = { version = "0.1.0-rc.2", features = ["rand_core"] } + +# Hashing (pure Rust) sha3 = "0.10" -rand = { version = "0.9", features = ["std", "std_rng"] } -serde = { version = "1", features = ["derive"] } +blake3 = "1.5" + +# Key derivation (pure Rust) +hkdf = "0.12" +pbkdf2 = { version = "0.12", features = ["simple"] } +hmac = "0.12" + +# BIP-39 +bip39 = { package = "tiny-bip39", version = "1.0" } + +# Serialization +serde = { version = "1.0", features = ["derive"] } serde-wasm-bindgen = "0.6" + +# Utilities hex = "0.4" +zeroize = { version = "1.7", features = ["derive"] } [dev-dependencies] wasm-bindgen-test = "0.3" +[features] +default = [] +# Enable when we have WASM-compatible Dilithium +pqc = [] + [profile.release] -# Optimize for small code size -opt-level = "s" +# Optimize for size in WASM lto = true +opt-level = "s" diff --git a/crates/synor-crypto-wasm/README.md b/crates/synor-crypto-wasm/README.md index 83b46b0..58466ec 100644 --- a/crates/synor-crypto-wasm/README.md +++ b/crates/synor-crypto-wasm/README.md @@ -1,94 +1,88 @@ -# synor-crypto-wasm +# Synor Crypto WASM -WASM bindings for Synor post-quantum cryptographic operations. +WASM-compatible cryptography library for the Synor web wallet. -## Status: Work in Progress +## Current Features -This crate is intended to provide WebAssembly bindings for ML-DSA-65 (Dilithium3) -quantum-resistant signatures. However, due to the following considerations, the -current Synor web wallet uses a **hybrid server-side approach** instead: +- **Ed25519 Signatures**: Full support via `ed25519-dalek` (pure Rust) +- **BIP-39 Mnemonics**: 12-24 word phrases for key generation +- **Bech32m Addresses**: Synor address encoding/decoding +- **BLAKE3/SHA3 Hashing**: Cryptographic hash functions +- **HKDF Key Derivation**: Secure key derivation -### Why Server-Side Dilithium? - -1. **Bundle Size**: The ML-DSA WASM module adds ~2MB to the web bundle, significantly - impacting initial load times and mobile performance. - -2. **Library Stability**: The `ml-dsa` crate is still in release candidate status - (0.1.0-rc.2) with API changes between versions. Production use requires stable APIs. - -3. **C-based Alternatives**: The `pqcrypto-dilithium` crate (which wraps PQClean's C - implementation) doesn't compile to WASM without significant toolchain setup. - -4. **Performance**: Server-side signing is generally faster than WASM execution, - especially on mobile devices. - -### Current Architecture - -The Synor web wallet uses a hybrid approach: - -``` -┌─────────────────────────────────────────────────────────┐ -│ Web Wallet │ -├─────────────────────────────────────────────────────────┤ -│ Client-Side (Browser) │ -│ ├── BIP39 mnemonic generation │ -│ ├── Ed25519 key derivation │ -│ ├── Ed25519 signing (fast, 64-byte signatures) │ -│ ├── Blake3 hashing │ -│ └── AES-GCM encryption for wallet storage │ -├─────────────────────────────────────────────────────────┤ -│ Server-Side (RPC) │ -│ └── ML-DSA-65/Dilithium3 signing via wallet_signDilithium│ -└─────────────────────────────────────────────────────────┘ -``` - -### Future Plans - -Once the `ml-dsa` crate reaches stable release (1.0), this crate will be updated -to provide full client-side ML-DSA-65 signing. This will enable: - -- Fully non-custodial wallet operation -- Offline transaction signing -- Hardware wallet integration - -### Building (Development) +## Building ```bash -# Native tests -cd crates/synor-crypto-wasm -cargo test +# Build for web (requires wasm-pack) +wasm-pack build --target web --out-dir pkg -# WASM build (requires wasm-pack) -# Currently blocked on ml-dsa stability -wasm-pack build --target web +# Build for Node.js +wasm-pack build --target nodejs --out-dir pkg-node ``` -## API (Future) +## Usage in JavaScript ```javascript -import init, { MlDsa65Keypair, mlDsa65Verify } from 'synor-crypto-wasm'; +import init, { Keypair, Mnemonic } from 'synor-crypto-wasm'; await init(); -// Generate keypair -const keypair = new MlDsa65Keypair(); -// Or from seed -const keypair2 = MlDsa65Keypair.fromSeed(seed); +// Generate mnemonic +const mnemonic = new Mnemonic(24); +console.log(mnemonic.phrase()); -// Sign +// Create keypair +const keypair = Keypair.fromMnemonic(mnemonic.phrase(), ""); +console.log(keypair.address("mainnet")); + +// Sign message +const message = new TextEncoder().encode("Hello Synor!"); const signature = keypair.sign(message); // Verify -const isValid = mlDsa65Verify(message, signature, keypair.verifyingKey()); +const valid = keypair.verify(message, signature); ``` -## Security Considerations +## Dilithium3 Post-Quantum Support -- ML-DSA-65 provides NIST Security Level 3 (~AES-192 equivalent) -- Hybrid signatures require BOTH Ed25519 AND Dilithium to verify -- This defense-in-depth means an attacker must break both algorithms -- Server-side signing should only be used with proper authentication +### Current Status: Pending -## License +The native `synor-crypto` crate uses `pqcrypto-dilithium` which relies on C +bindings and does not compile to WASM. Options for WASM-compatible Dilithium3: -MIT +1. **pqc-crystals-dilithium** - Pure Rust, may work with WASM +2. **ML-DSA reference** - FIPS 204 standard (formerly Dilithium) +3. **Emscripten build** - Compile C implementation to WASM + +### Roadmap + +1. [x] Ed25519 basic support +2. [x] BIP-39 mnemonic generation +3. [x] Address encoding +4. [ ] Dilithium3 signatures (requires WASM-compatible library) +5. [ ] Hybrid Ed25519 + Dilithium verification +6. [ ] Kyber key encapsulation (post-quantum key exchange) + +### Workaround + +Until native Dilithium3 WASM is available, the web wallet can: + +1. Use Ed25519-only addresses for now +2. Submit hybrid-signed transactions to a backend that adds Dilithium signatures +3. Or use a WASM module compiled via Emscripten + +## Security Notes + +- Keys are zeroized on drop +- Uses `getrandom` with `js` feature for secure randomness in browsers +- No side-channel resistance in signature timing (use constant-time ops for production) + +## Testing + +```bash +# Run Rust tests +cargo test + +# Run WASM tests in browser +wasm-pack test --headless --chrome +``` diff --git a/crates/synor-crypto-wasm/src/address.rs b/crates/synor-crypto-wasm/src/address.rs new file mode 100644 index 0000000..b8f73ec --- /dev/null +++ b/crates/synor-crypto-wasm/src/address.rs @@ -0,0 +1,221 @@ +//! Address encoding for Synor blockchain. +//! +//! Uses Bech32m encoding with network-specific prefixes. + +use sha3::{Digest, Sha3_256}; +use wasm_bindgen::prelude::*; + +/// Bech32m character set. +const CHARSET: &[u8; 32] = b"qpzry9x8gf2tvdw0s3jn54khce6mua7l"; + +/// Bech32m constant for checksum. +const BECH32M_CONST: u32 = 0x2bc830a3; + +/// Network prefixes. +pub fn network_prefix(network: &str) -> Result<&'static str, JsValue> { + match network.to_lowercase().as_str() { + "mainnet" | "main" => Ok("synor"), + "testnet" | "test" => Ok("tsynor"), + "devnet" | "dev" => Ok("dsynor"), + _ => Err(JsValue::from_str(&format!("Unknown network: {}", network))), + } +} + +/// Encode a public key as a Synor address. +pub fn encode_address(network: &str, pubkey: &[u8]) -> Result { + let prefix = network_prefix(network)?; + + // Hash the public key + let mut hasher = Sha3_256::new(); + hasher.update(pubkey); + let hash = hasher.finalize(); + + // Take first 20 bytes (160 bits) for the address + let data = &hash[..20]; + + // Encode as bech32m + bech32m_encode(prefix, data) +} + +/// Decode a Synor address to get network and public key hash. +#[wasm_bindgen(js_name = decodeAddress)] +pub fn decode_address(address: &str) -> Result { + let (hrp, data) = bech32m_decode(address)?; + + let network = match hrp.as_str() { + "synor" => "mainnet", + "tsynor" => "testnet", + "dsynor" => "devnet", + _ => return Err(JsValue::from_str("Unknown address prefix")), + }; + + // Return as JS object + let result = js_sys::Object::new(); + js_sys::Reflect::set(&result, &"network".into(), &network.into())?; + js_sys::Reflect::set(&result, &"hash".into(), &hex::encode(&data).into())?; + + Ok(result.into()) +} + +/// Validate a Synor address. +#[wasm_bindgen(js_name = validateAddress)] +pub fn validate_address(address: &str) -> bool { + decode_address(address).is_ok() +} + +// Bech32m implementation + +fn bech32m_encode(hrp: &str, data: &[u8]) -> Result { + let mut result = String::with_capacity(hrp.len() + 1 + data.len() * 8 / 5 + 6); + result.push_str(hrp); + result.push('1'); + + // Convert data to 5-bit groups + let data5 = convert_bits(data, 8, 5, true)?; + + // Add data characters + for &b in &data5 { + result.push(CHARSET[b as usize] as char); + } + + // Compute and add checksum + let checksum = create_checksum(hrp, &data5); + for b in checksum { + result.push(CHARSET[b as usize] as char); + } + + Ok(result) +} + +fn bech32m_decode(address: &str) -> Result<(String, Vec), JsValue> { + // Find separator + let sep_pos = address + .rfind('1') + .ok_or_else(|| JsValue::from_str("Missing separator"))?; + + if sep_pos == 0 || sep_pos + 7 > address.len() { + return Err(JsValue::from_str("Invalid address format")); + } + + let hrp = &address[..sep_pos]; + let data_part = &address[sep_pos + 1..]; + + // Convert characters to 5-bit values + let mut data5 = Vec::with_capacity(data_part.len()); + for c in data_part.chars() { + let idx = CHARSET + .iter() + .position(|&x| x as char == c.to_ascii_lowercase()) + .ok_or_else(|| JsValue::from_str("Invalid character in address"))?; + data5.push(idx as u8); + } + + // Verify checksum + if !verify_checksum(hrp, &data5) { + return Err(JsValue::from_str("Invalid checksum")); + } + + // Remove checksum (last 6 bytes) + let data5 = &data5[..data5.len() - 6]; + + // Convert from 5-bit to 8-bit + let data8 = convert_bits(data5, 5, 8, false)?; + + Ok((hrp.to_string(), data8)) +} + +fn convert_bits(data: &[u8], from_bits: u32, to_bits: u32, pad: bool) -> Result, JsValue> { + let mut acc: u32 = 0; + let mut bits: u32 = 0; + let mut result = Vec::new(); + let maxv: u32 = (1 << to_bits) - 1; + + for &value in data { + let v = value as u32; + if v >> from_bits != 0 { + return Err(JsValue::from_str("Invalid data value")); + } + acc = (acc << from_bits) | v; + bits += from_bits; + while bits >= to_bits { + bits -= to_bits; + result.push(((acc >> bits) & maxv) as u8); + } + } + + if pad { + if bits > 0 { + result.push(((acc << (to_bits - bits)) & maxv) as u8); + } + } else if bits >= from_bits || ((acc << (to_bits - bits)) & maxv) != 0 { + return Err(JsValue::from_str("Invalid padding")); + } + + Ok(result) +} + +fn polymod(values: &[u8]) -> u32 { + const GEN: [u32; 5] = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3]; + let mut chk: u32 = 1; + for &v in values { + let b = chk >> 25; + chk = ((chk & 0x1ffffff) << 5) ^ (v as u32); + for (i, &g) in GEN.iter().enumerate() { + if (b >> i) & 1 != 0 { + chk ^= g; + } + } + } + chk +} + +fn hrp_expand(hrp: &str) -> Vec { + let mut result = Vec::with_capacity(hrp.len() * 2 + 1); + for c in hrp.chars() { + result.push((c as u8) >> 5); + } + result.push(0); + for c in hrp.chars() { + result.push((c as u8) & 31); + } + result +} + +fn create_checksum(hrp: &str, data: &[u8]) -> [u8; 6] { + let mut values = hrp_expand(hrp); + values.extend_from_slice(data); + values.extend_from_slice(&[0, 0, 0, 0, 0, 0]); + let poly = polymod(&values) ^ BECH32M_CONST; + let mut result = [0u8; 6]; + for (i, item) in result.iter_mut().enumerate() { + *item = ((poly >> (5 * (5 - i))) & 31) as u8; + } + result +} + +fn verify_checksum(hrp: &str, data: &[u8]) -> bool { + let mut values = hrp_expand(hrp); + values.extend_from_slice(data); + polymod(&values) == BECH32M_CONST +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_address_encode_decode() { + let pubkey = [0u8; 32]; + let address = encode_address("mainnet", &pubkey).unwrap(); + assert!(address.starts_with("synor1")); + + let decoded = decode_address(&address).unwrap(); + // Verify it's valid + } + + #[test] + fn test_validate_address() { + assert!(!validate_address("invalid")); + // Would need valid test vectors + } +} diff --git a/crates/synor-crypto-wasm/src/lib.rs b/crates/synor-crypto-wasm/src/lib.rs index 5d304c2..9086798 100644 --- a/crates/synor-crypto-wasm/src/lib.rs +++ b/crates/synor-crypto-wasm/src/lib.rs @@ -1,287 +1,258 @@ -//! WASM bindings for Synor cryptographic operations. +//! WASM-compatible cryptography for Synor web wallet. //! -//! This crate provides WebAssembly bindings for ML-DSA (formerly Dilithium3) -//! post-quantum cryptographic operations, enabling quantum-resistant signatures -//! in web browsers. +//! This crate provides cryptographic primitives that can be compiled to WebAssembly +//! for use in the Synor web wallet. It includes: //! -//! # Usage +//! - Ed25519 signature generation and verification +//! - Key derivation from BIP-39 mnemonics +//! - Bech32m address encoding +//! - Dilithium3 post-quantum signatures (when available) +//! +//! # Usage in JavaScript //! //! ```javascript -//! import init, { MlDsa65Keypair, mlDsa65Verify } from 'synor-crypto-wasm'; +//! import init, { Keypair, Mnemonic, sign, verify } from 'synor-crypto-wasm'; //! //! await init(); //! -//! // Generate a new keypair -//! const keypair = new MlDsa65Keypair(); +//! // Generate mnemonic +//! const mnemonic = Mnemonic.generate(24); +//! console.log(mnemonic.phrase()); //! -//! // Or from a seed for deterministic generation -//! const keypair2 = MlDsa65Keypair.fromSeed(seed); +//! // Create keypair from mnemonic +//! const keypair = Keypair.fromMnemonic(mnemonic.phrase(), ""); +//! console.log(keypair.address("mainnet")); //! -//! // Sign a message +//! // Sign and verify +//! const message = new TextEncoder().encode("Hello Synor!"); //! const signature = keypair.sign(message); -//! -//! // Verify a signature -//! const isValid = mlDsa65Verify(message, signature, keypair.verifyingKey()); +//! const isValid = keypair.verify(message, signature); //! ``` -use ml_dsa::{ - signature::{Keypair, Signer, Verifier}, - EncodedSignature, EncodedSigningKey, EncodedVerifyingKey, KeyGen, MlDsa65, Seed, Signature, - SigningKey, VerifyingKey, -}; -use sha3::{ - digest::{ExtendableOutput, Update, XofReader}, - Shake256, -}; +use ed25519_dalek::{Signature, Signer, SigningKey, VerifyingKey}; +use rand::rngs::OsRng; +use sha3::{Digest, Sha3_256}; use wasm_bindgen::prelude::*; +use zeroize::Zeroize; -// When the `console_error_panic_hook` feature is enabled, we can call the -// `set_panic_hook` function to get better error messages in the console. -#[cfg(feature = "console_error_panic_hook")] -pub fn set_panic_hook() { - console_error_panic_hook::set_once(); -} +mod address; +mod mnemonic_wasm; + +pub use address::*; +pub use mnemonic_wasm::*; /// Initialize the WASM module. #[wasm_bindgen(start)] pub fn init() { - #[cfg(feature = "console_error_panic_hook")] - set_panic_hook(); + // WASM initialization - can be extended for panic hooks, logging, etc. } -/// ML-DSA-65 (Dilithium3 equivalent) verifying key size in bytes. -pub const MLDSA65_VERIFYING_KEY_SIZE: usize = 1952; - -/// ML-DSA-65 signing key size in bytes. -pub const MLDSA65_SIGNING_KEY_SIZE: usize = 4032; - -/// ML-DSA-65 signature size in bytes. -pub const MLDSA65_SIGNATURE_SIZE: usize = 3309; - -/// An ML-DSA-65 keypair for quantum-resistant signatures. -/// -/// ML-DSA-65 is the NIST-standardized version of Dilithium3, -/// providing NIST security level 3 (roughly equivalent to AES-192). +/// Ed25519 keypair for signing transactions. #[wasm_bindgen] -pub struct MlDsa65Keypair { - signing_key: SigningKey, - verifying_key: VerifyingKey, +pub struct Keypair { + signing_key: SigningKey, + #[wasm_bindgen(skip)] + pub seed: [u8; 32], } #[wasm_bindgen] -impl MlDsa65Keypair { - /// Generates a new random ML-DSA-65 keypair. +impl Keypair { + /// Generate a new random keypair. #[wasm_bindgen(constructor)] - pub fn new() -> Result { - let mut rng = rand::rng(); - let kp = MlDsa65::key_gen(&mut rng); - Ok(MlDsa65Keypair { - signing_key: kp.signing_key().clone(), - verifying_key: kp.verifying_key().clone(), + pub fn new() -> Result { + let signing_key = SigningKey::generate(&mut OsRng); + let seed = signing_key.to_bytes(); + Ok(Keypair { signing_key, seed }) + } + + /// Create a keypair from a 32-byte seed. + #[wasm_bindgen(js_name = fromSeed)] + pub fn from_seed(seed: &[u8]) -> Result { + if seed.len() != 32 { + return Err(JsValue::from_str("Seed must be exactly 32 bytes")); + } + let mut seed_arr = [0u8; 32]; + seed_arr.copy_from_slice(seed); + let signing_key = SigningKey::from_bytes(&seed_arr); + Ok(Keypair { + signing_key, + seed: seed_arr, }) } - /// Creates a keypair from a 32-byte seed. - /// - /// Uses SHAKE256 to expand the seed deterministically. - #[wasm_bindgen(js_name = fromSeed)] - pub fn from_seed(seed: &[u8]) -> Result { - if seed.len() < 32 { - return Err(JsError::new("Seed must be at least 32 bytes")); + /// Create a keypair from a BIP-39 mnemonic phrase. + #[wasm_bindgen(js_name = fromMnemonic)] + pub fn from_mnemonic(phrase: &str, passphrase: &str) -> Result { + let mnemonic = bip39::Mnemonic::from_phrase(phrase, bip39::Language::English) + .map_err(|e| JsValue::from_str(&format!("Invalid mnemonic: {:?}", e)))?; + + // Derive seed from mnemonic + let seed = bip39::Seed::new(&mnemonic, passphrase); + let seed_bytes = seed.as_bytes(); + + // Use first 32 bytes for Ed25519 + let mut ed_seed = [0u8; 32]; + ed_seed.copy_from_slice(&seed_bytes[..32]); + + let signing_key = SigningKey::from_bytes(&ed_seed); + Ok(Keypair { + signing_key, + seed: ed_seed, + }) + } + + /// Get the public key as hex string. + #[wasm_bindgen(js_name = publicKeyHex)] + pub fn public_key_hex(&self) -> String { + hex::encode(self.signing_key.verifying_key().to_bytes()) + } + + /// Get the public key as bytes. + #[wasm_bindgen(js_name = publicKeyBytes)] + pub fn public_key_bytes(&self) -> Vec { + self.signing_key.verifying_key().to_bytes().to_vec() + } + + /// Get the Synor address for this keypair. + #[wasm_bindgen] + pub fn address(&self, network: &str) -> Result { + let pubkey = self.signing_key.verifying_key().to_bytes(); + address::encode_address(network, &pubkey) + } + + /// Sign a message. + #[wasm_bindgen] + pub fn sign(&self, message: &[u8]) -> Vec { + let signature = self.signing_key.sign(message); + signature.to_bytes().to_vec() + } + + /// Verify a signature. + #[wasm_bindgen] + pub fn verify(&self, message: &[u8], signature: &[u8]) -> Result { + if signature.len() != 64 { + return Err(JsValue::from_str("Signature must be 64 bytes")); } - // Use SHAKE256 to expand seed deterministically with domain separation - let mut hasher = Shake256::default(); - Update::update(&mut hasher, b"synor-mldsa65-keygen-v1"); - Update::update(&mut hasher, &seed[..32]); + let sig_bytes: [u8; 64] = signature + .try_into() + .map_err(|_| JsValue::from_str("Invalid signature length"))?; + let sig = Signature::from_bytes(&sig_bytes); - // ML-DSA requires 32 bytes for key generation - let mut expanded: Seed = [0u8; 32]; - let mut reader = hasher.finalize_xof(); - XofReader::read(&mut reader, &mut expanded); - - // Generate keypair from expanded seed - let kp = MlDsa65::key_gen_from_seed(expanded); - Ok(MlDsa65Keypair { - signing_key: kp.signing_key().clone(), - verifying_key: kp.verifying_key().clone(), - }) - } - - /// Returns the verifying (public) key as bytes. - #[wasm_bindgen(js_name = verifyingKey)] - pub fn verifying_key(&self) -> Vec { - self.verifying_key.encode().to_vec() - } - - /// Returns the verifying (public) key as a hex string. - #[wasm_bindgen(js_name = verifyingKeyHex)] - pub fn verifying_key_hex(&self) -> String { - hex::encode(self.verifying_key.encode()) - } - - /// Signs a message and returns the signature bytes. - pub fn sign(&self, message: &[u8]) -> Vec { - let sig = self.signing_key.sign(message); - sig.encode().to_vec() - } - - /// Signs a message and returns the signature as a hex string. - #[wasm_bindgen(js_name = signHex)] - pub fn sign_hex(&self, message: &[u8]) -> String { - hex::encode(self.sign(message)) - } - - /// Exports the signing key (private key) as bytes. - /// WARNING: Handle with care - this is sensitive key material! - #[wasm_bindgen(js_name = signingKey)] - pub fn signing_key(&self) -> Vec { - self.signing_key.encode().to_vec() + use ed25519_dalek::Verifier; + Ok(self + .signing_key + .verifying_key() + .verify(message, &sig) + .is_ok()) } } -/// Verifies an ML-DSA-65 signature. -/// -/// # Arguments -/// * `message` - The original message that was signed -/// * `signature` - The ML-DSA-65 signature bytes -/// * `verifying_key` - The verifying (public) key bytes (1952 bytes) -/// -/// # Returns -/// `true` if the signature is valid, `false` otherwise. -#[wasm_bindgen(js_name = mlDsa65Verify)] -pub fn verify(message: &[u8], signature: &[u8], verifying_key: &[u8]) -> Result { - if verifying_key.len() != MLDSA65_VERIFYING_KEY_SIZE { - return Err(JsError::new(&format!( - "Invalid verifying key length: expected {}, got {}", - MLDSA65_VERIFYING_KEY_SIZE, - verifying_key.len() - ))); +impl Default for Keypair { + fn default() -> Self { + Self::new().expect("Failed to generate keypair") + } +} + +impl Drop for Keypair { + fn drop(&mut self) { + self.seed.zeroize(); + } +} + +/// Verify a signature with a public key. +#[wasm_bindgen(js_name = verifyWithPublicKey)] +pub fn verify_with_public_key( + public_key: &[u8], + message: &[u8], + signature: &[u8], +) -> Result { + if public_key.len() != 32 { + return Err(JsValue::from_str("Public key must be 32 bytes")); + } + if signature.len() != 64 { + return Err(JsValue::from_str("Signature must be 64 bytes")); } - // Parse verifying key - let vk_bytes: EncodedVerifyingKey = verifying_key + let pk_bytes: [u8; 32] = public_key .try_into() - .map_err(|_| JsError::new("Invalid verifying key format"))?; - - let vk = VerifyingKey::::decode(&vk_bytes) - .ok_or_else(|| JsError::new("Failed to decode verifying key"))?; - - // Parse signature - if signature.len() != MLDSA65_SIGNATURE_SIZE { - return Err(JsError::new(&format!( - "Invalid signature length: expected {}, got {}", - MLDSA65_SIGNATURE_SIZE, - signature.len() - ))); - } - - let sig_bytes: EncodedSignature = signature + .map_err(|_| JsValue::from_str("Invalid public key"))?; + let sig_bytes: [u8; 64] = signature .try_into() - .map_err(|_| JsError::new("Invalid signature format"))?; + .map_err(|_| JsValue::from_str("Invalid signature"))?; - let sig = Signature::::decode(&sig_bytes) - .ok_or_else(|| JsError::new("Failed to decode signature"))?; + let verifying_key = VerifyingKey::from_bytes(&pk_bytes) + .map_err(|_| JsValue::from_str("Invalid public key format"))?; + let signature = Signature::from_bytes(&sig_bytes); - // Verify - Ok(vk.verify(message, &sig).is_ok()) + use ed25519_dalek::Verifier; + Ok(verifying_key.verify(message, &signature).is_ok()) } -/// Returns the expected verifying key size for ML-DSA-65. -#[wasm_bindgen(js_name = mlDsa65VerifyingKeySize)] -pub fn verifying_key_size() -> usize { - MLDSA65_VERIFYING_KEY_SIZE +/// Compute SHA3-256 hash. +#[wasm_bindgen(js_name = sha3_256)] +pub fn sha3_256_hash(data: &[u8]) -> Vec { + let mut hasher = Sha3_256::new(); + hasher.update(data); + hasher.finalize().to_vec() } -/// Returns the expected signature size for ML-DSA-65. -#[wasm_bindgen(js_name = mlDsa65SignatureSize)] -pub fn signature_size() -> usize { - MLDSA65_SIGNATURE_SIZE +/// Compute BLAKE3 hash. +#[wasm_bindgen(js_name = blake3)] +pub fn blake3_hash(data: &[u8]) -> Vec { + blake3::hash(data).as_bytes().to_vec() } -/// Utility to convert hex string to bytes. -#[wasm_bindgen(js_name = hexToBytes)] -pub fn hex_to_bytes(hex_str: &str) -> Result, JsError> { - hex::decode(hex_str).map_err(|e| JsError::new(&format!("Invalid hex: {}", e))) +/// Derive key using HKDF-SHA256. +#[wasm_bindgen(js_name = deriveKey)] +pub fn derive_key( + input_key: &[u8], + salt: &[u8], + info: &[u8], + output_len: usize, +) -> Result, JsValue> { + use hkdf::Hkdf; + use sha3::Sha3_256; + + let hk = Hkdf::::new(Some(salt), input_key); + let mut output = vec![0u8; output_len]; + hk.expand(info, &mut output) + .map_err(|_| JsValue::from_str("HKDF expansion failed"))?; + Ok(output) } -/// Utility to convert bytes to hex string. -#[wasm_bindgen(js_name = bytesToHex)] -pub fn bytes_to_hex(bytes: &[u8]) -> String { - hex::encode(bytes) -} +// bip39 crate is used in mnemonic_wasm.rs #[cfg(test)] mod tests { use super::*; + use wasm_bindgen_test::*; - #[test] + #[wasm_bindgen_test] fn test_keypair_generation() { - let keypair = MlDsa65Keypair::new().unwrap(); - assert_eq!(keypair.verifying_key().len(), MLDSA65_VERIFYING_KEY_SIZE); + let keypair = Keypair::new().unwrap(); + assert_eq!(keypair.public_key_bytes().len(), 32); } - #[test] - fn test_keypair_from_seed() { - let seed = [42u8; 32]; - let kp1 = MlDsa65Keypair::from_seed(&seed).unwrap(); - let kp2 = MlDsa65Keypair::from_seed(&seed).unwrap(); - - // Same seed should produce same verifying key - assert_eq!(kp1.verifying_key(), kp2.verifying_key()); - } - - #[test] + #[wasm_bindgen_test] fn test_sign_verify() { - let keypair = MlDsa65Keypair::new().unwrap(); - let message = b"Hello, Synor blockchain!"; - + let keypair = Keypair::new().unwrap(); + let message = b"Hello, Synor!"; let signature = keypair.sign(message); - let verifying_key = keypair.verifying_key(); - - assert_eq!(signature.len(), MLDSA65_SIGNATURE_SIZE); - - let is_valid = verify(message, &signature, &verifying_key).unwrap(); - assert!(is_valid); + assert!(keypair.verify(message, &signature).unwrap()); } - #[test] - fn test_invalid_signature() { - let keypair = MlDsa65Keypair::new().unwrap(); - let message = b"Hello, Synor blockchain!"; - - let mut signature = keypair.sign(message); - let verifying_key = keypair.verifying_key(); - - // Corrupt the signature - signature[0] ^= 0xFF; - - // This should either return false or error due to invalid format - let result = verify(message, &signature, &verifying_key); - match result { - Ok(is_valid) => assert!(!is_valid), - Err(_) => {} // Invalid format error is also acceptable - } + #[wasm_bindgen_test] + fn test_address_generation() { + let keypair = Keypair::new().unwrap(); + let address = keypair.address("mainnet").unwrap(); + assert!(address.starts_with("synor1")); } - #[test] - fn test_wrong_message() { - let keypair = MlDsa65Keypair::new().unwrap(); - let message = b"Hello, Synor blockchain!"; - let wrong_message = b"Wrong message"; - - let signature = keypair.sign(message); - let verifying_key = keypair.verifying_key(); - - let is_valid = verify(wrong_message, &signature, &verifying_key).unwrap(); - assert!(!is_valid); - } - - #[test] - fn test_hex_roundtrip() { - let data = b"test data"; - let hex_str = bytes_to_hex(data); - let recovered = hex_to_bytes(&hex_str).unwrap(); - assert_eq!(data.to_vec(), recovered); + #[wasm_bindgen_test] + fn test_mnemonic_keypair() { + let phrase = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; + let keypair = Keypair::from_mnemonic(phrase, "").unwrap(); + assert_eq!(keypair.public_key_bytes().len(), 32); } } diff --git a/crates/synor-crypto-wasm/src/mnemonic_wasm.rs b/crates/synor-crypto-wasm/src/mnemonic_wasm.rs new file mode 100644 index 0000000..6a4fc73 --- /dev/null +++ b/crates/synor-crypto-wasm/src/mnemonic_wasm.rs @@ -0,0 +1,115 @@ +//! BIP-39 mnemonic support for WASM. + +use bip39::{Language, Mnemonic as Bip39Mnemonic, MnemonicType}; +use wasm_bindgen::prelude::*; + +/// BIP-39 mnemonic phrase wrapper. +#[wasm_bindgen] +pub struct Mnemonic { + inner: Bip39Mnemonic, +} + +#[wasm_bindgen] +impl Mnemonic { + /// Generate a new random mnemonic with the specified word count. + #[wasm_bindgen(constructor)] + pub fn new(word_count: u8) -> Result { + let mnemonic_type = match word_count { + 12 => MnemonicType::Words12, + 15 => MnemonicType::Words15, + 18 => MnemonicType::Words18, + 21 => MnemonicType::Words21, + 24 => MnemonicType::Words24, + _ => { + return Err(JsValue::from_str( + "Invalid word count. Must be 12, 15, 18, 21, or 24", + )) + } + }; + + let mnemonic = Bip39Mnemonic::new(mnemonic_type, Language::English); + Ok(Mnemonic { inner: mnemonic }) + } + + /// Generate a 24-word mnemonic. + #[wasm_bindgen] + pub fn generate(word_count: u8) -> Result { + Mnemonic::new(word_count) + } + + /// Parse a mnemonic from a phrase. + #[wasm_bindgen(js_name = fromPhrase)] + pub fn from_phrase(phrase: &str) -> Result { + let mnemonic = Bip39Mnemonic::from_phrase(phrase, Language::English) + .map_err(|e| JsValue::from_str(&format!("Invalid mnemonic: {:?}", e)))?; + Ok(Mnemonic { inner: mnemonic }) + } + + /// Get the mnemonic phrase as a string. + #[wasm_bindgen] + pub fn phrase(&self) -> String { + self.inner.phrase().to_string() + } + + /// Get the mnemonic words as an array. + #[wasm_bindgen] + pub fn words(&self) -> Vec { + self.inner + .phrase() + .split_whitespace() + .map(String::from) + .collect() + } + + /// Get the word count. + #[wasm_bindgen(js_name = wordCount)] + pub fn word_count(&self) -> usize { + self.inner.phrase().split_whitespace().count() + } + + /// Derive a 64-byte seed from the mnemonic. + #[wasm_bindgen(js_name = toSeed)] + pub fn to_seed(&self, passphrase: &str) -> Vec { + let seed = bip39::Seed::new(&self.inner, passphrase); + seed.as_bytes().to_vec() + } + + /// Get the entropy bytes. + #[wasm_bindgen] + pub fn entropy(&self) -> Vec { + self.inner.entropy().to_vec() + } + + /// Validate a mnemonic phrase. + #[wasm_bindgen] + pub fn validate(phrase: &str) -> bool { + Bip39Mnemonic::validate(phrase, Language::English).is_ok() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use wasm_bindgen_test::*; + + #[wasm_bindgen_test] + fn test_mnemonic_generation() { + let mnemonic = Mnemonic::new(24).unwrap(); + assert_eq!(mnemonic.word_count(), 24); + } + + #[wasm_bindgen_test] + fn test_mnemonic_validation() { + assert!(Mnemonic::validate("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about")); + assert!(!Mnemonic::validate("invalid mnemonic phrase")); + } + + #[wasm_bindgen_test] + fn test_mnemonic_to_seed() { + let mnemonic = Mnemonic::from_phrase( + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" + ).unwrap(); + let seed = mnemonic.to_seed(""); + assert_eq!(seed.len(), 64); + } +} diff --git a/crates/synor-network/src/config.rs b/crates/synor-network/src/config.rs index be68e2b..2442b1d 100644 --- a/crates/synor-network/src/config.rs +++ b/crates/synor-network/src/config.rs @@ -208,24 +208,42 @@ fn mainnet_bootstrap_peers() -> Vec { /// Returns testnet bootstrap peers. /// -/// To add a new seed node: -/// 1. Deploy a synord node on the testnet -/// 2. Get its peer ID from the logs: "Local peer ID: 12D3KooW..." -/// 3. Add the full address: `/dns4//tcp/17511/p2p/` +/// # Seed Node Deployment Process /// -/// Example: -/// ```text -/// /dns4/testnet-seed1.synor.cc/tcp/17511/p2p/12D3KooWAbCdEfGhIjKlMnOpQrStUvWxYz123456789 -/// ``` +/// To add a new seed node: +/// +/// 1. **Deploy synord on a server** with a static IP/hostname: +/// ```bash +/// synord --network testnet --rpc-host 0.0.0.0 +/// ``` +/// +/// 2. **Get the peer ID** from startup logs: +/// ``` +/// INFO synor_network::service: Local peer ID: 12D3KooWAbCdEfGhIjKlMnOpQrStUvWxYz123456789 +/// ``` +/// +/// 3. **Add the full multiaddr** (hostname + port + peer_id): +/// ```text +/// /dns4/testnet-seed1.synor.cc/tcp/17511/p2p/12D3KooWAbCdEfGhIjKlMnOpQrStUvWxYz123456789 +/// ``` +/// +/// # Runtime Configuration +/// +/// Instead of hardcoding, operators can use the `SYNOR_BOOTSTRAP_PEERS` environment +/// variable at the synord application level (comma-separated multiaddrs). fn testnet_bootstrap_peers() -> Vec { // Testnet bootstrap nodes - add peer IDs when seed nodes are deployed // Format: /dns4//tcp//p2p/ + // + // NOTE: Seeds are configured empty here because peer IDs are only known + // after deployment. Use SYNOR_BOOTSTRAP_PEERS env var or config file + // to specify bootstrap peers at runtime. let seeds: &[&str] = &[ - // North America (seed1.synor.cc) + // North America (seed1.synor.cc) - uncomment after deployment: // "/dns4/testnet-seed1.synor.cc/tcp/17511/p2p/12D3KooW...", - // Europe (seed2.synor.cc) + // Europe (seed2.synor.cc) - uncomment after deployment: // "/dns4/testnet-seed2.synor.cc/tcp/17511/p2p/12D3KooW...", - // Asia (seed3.synor.cc) + // Asia (seed3.synor.cc) - uncomment after deployment: // "/dns4/testnet-seed3.synor.cc/tcp/17511/p2p/12D3KooW...", ]; diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md new file mode 100644 index 0000000..4658c6a --- /dev/null +++ b/docs/DEPLOYMENT.md @@ -0,0 +1,276 @@ +# Synor Testnet Deployment Guide + +This guide covers deploying the Synor blockchain testnet, including seed nodes, validators, and supporting infrastructure. + +--- + +## Prerequisites + +- Rust 1.75+ with `wasm32-unknown-unknown` target +- Docker (optional, for containerized deployment) +- 3+ servers with static IPs or DNS hostnames +- Ports: 17511 (P2P), 17110 (RPC), 17111 (WebSocket) + +--- + +## 1. Build from Source + +```bash +# Clone and build +git clone https://github.com/g1-technologies/synor.git +cd synor + +# Build release binaries +cargo build --release -p synord -p synor-cli + +# Binaries will be in target/release/ +``` + +--- + +## 2. Deploy Seed Nodes + +Seed nodes are the first nodes deployed. They provide initial peer discovery for the network. + +### 2.1 Deploy First Seed Node + +```bash +# On testnet-seed1.synor.cc +./synord --network testnet \ + --data-dir /var/lib/synor \ + --rpc-host 0.0.0.0 \ + --rpc-port 17110 \ + --ws-port 17111 \ + --p2p-port 17511 +``` + +**Get the peer ID from startup logs:** +``` +INFO synor_network::service: Local peer ID: 12D3KooWXxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +``` + +Record this peer ID - you'll need it for other nodes. + +### 2.2 Deploy Additional Seed Nodes + +For the second and third seed nodes, configure them to bootstrap from the first: + +```bash +# On testnet-seed2.synor.cc +export SYNOR_BOOTSTRAP_PEERS="/dns4/testnet-seed1.synor.cc/tcp/17511/p2p/12D3KooWXxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + +./synord --network testnet \ + --data-dir /var/lib/synor \ + --rpc-host 0.0.0.0 \ + --rpc-port 17110 +``` + +After all seed nodes are running, record all peer IDs: + +| Seed Node | Hostname | Peer ID | +|-----------|----------|---------| +| Seed 1 (US-East) | testnet-seed1.synor.cc | 12D3KooW... | +| Seed 2 (EU-Frankfurt) | testnet-seed2.synor.cc | 12D3KooW... | +| Seed 3 (Asia-Singapore) | testnet-seed3.synor.cc | 12D3KooW... | + +### 2.3 Configure Bootstrap Peers + +Once all seed nodes are running, update the `SYNOR_BOOTSTRAP_PEERS` environment variable on each node: + +```bash +export SYNOR_BOOTSTRAP_PEERS="\ +/dns4/testnet-seed1.synor.cc/tcp/17511/p2p/12D3KooW...,\ +/dns4/testnet-seed2.synor.cc/tcp/17511/p2p/12D3KooW...,\ +/dns4/testnet-seed3.synor.cc/tcp/17511/p2p/12D3KooW..." +``` + +Or create a config file at `~/.synor/config.toml`: + +```toml +[p2p] +seeds = [ + "/dns4/testnet-seed1.synor.cc/tcp/17511/p2p/12D3KooW...", + "/dns4/testnet-seed2.synor.cc/tcp/17511/p2p/12D3KooW...", + "/dns4/testnet-seed3.synor.cc/tcp/17511/p2p/12D3KooW...", +] +``` + +--- + +## 3. Deploy Faucet + +The faucet provides testnet tokens to developers. + +### 3.1 Generate Faucet Wallet + +```bash +# Generate a new wallet for the faucet +synor-cli wallet create --name faucet + +# Note the address for genesis allocation +synor-cli wallet list +``` + +### 3.2 Configure Faucet + +**IMPORTANT: Store the faucet private key securely!** + +For production, use a secrets manager (AWS Secrets Manager, HashiCorp Vault, etc.): + +```bash +# Development only - NOT for production +export FAUCET_WALLET_KEY="your-private-key-here" +export FAUCET_RPC_URL="http://testnet-seed1.synor.cc:17110" +export FAUCET_DRIP_AMOUNT="1000000000" # 10 SYNOR (8 decimals) +export FAUCET_COOLDOWN="3600" # 1 hour between requests + +# Run faucet +./faucet --port 8080 +``` + +### 3.3 Secure the Faucet Key (Production) + +Instead of environment variables, integrate with a secrets manager: + +```rust +// Example: AWS Secrets Manager integration +// See apps/faucet/src/secrets.rs for implementation +``` + +--- + +## 4. Deploy Block Explorer + +The explorer provides blockchain visibility. + +```bash +# Configure explorer +export EXPLORER_RPC_URL="http://testnet-seed1.synor.cc:17110" +export EXPLORER_WS_URL="ws://testnet-seed1.synor.cc:17111" + +# Run explorer backend +./explorer --port 3000 + +# For production, restrict CORS: +export EXPLORER_CORS_ORIGINS="https://explorer.synor.cc,https://testnet.synor.cc" +``` + +--- + +## 5. Security Checklist + +### Network Security +- [ ] Firewall configured (allow 17511, 17110, 17111) +- [ ] DDoS protection enabled +- [ ] Rate limiting configured on RPC endpoints + +### Node Security +- [ ] Node runs as non-root user +- [ ] Data directory has restricted permissions +- [ ] Log rotation configured + +### Key Management +- [ ] Faucet key stored in secrets manager (not env vars) +- [ ] Validator keys stored securely +- [ ] Key backup procedures documented + +### CORS Configuration +- [ ] Explorer CORS restricted to specific origins +- [ ] Faucet CORS restricted to specific origins +- [ ] RPC CORS configured appropriately + +--- + +## 6. Monitoring + +### Prometheus Metrics + +Enable metrics on synord: + +```bash +./synord --network testnet --metrics --metrics-port 9090 +``` + +Scrape endpoint: `http://localhost:9090/metrics` + +### Key Metrics to Monitor + +- `synor_peer_count` - Number of connected peers +- `synor_block_height` - Current block height +- `synor_sync_progress` - Sync progress percentage +- `synor_mempool_size` - Pending transactions +- `synor_blocks_per_second` - Block production rate + +--- + +## 7. Troubleshooting + +### Node won't connect to peers + +1. Check firewall rules (port 17511) +2. Verify bootstrap peers are reachable +3. Check peer ID format in SYNOR_BOOTSTRAP_PEERS + +```bash +# Test connectivity +nc -zv testnet-seed1.synor.cc 17511 +``` + +### Node stuck syncing + +1. Check disk space +2. Verify network bandwidth +3. Increase sync batch size if needed + +### High memory usage + +1. Adjust cache size in config +2. Enable pruning for non-archive nodes + +--- + +## 8. Updating Nodes + +### Rolling Updates + +1. Stop node gracefully: `kill -SIGTERM ` +2. Wait for shutdown (check logs) +3. Update binary +4. Restart node + +### Breaking Changes + +For consensus changes, coordinate a hard fork: + +1. Announce upgrade block height +2. Deploy new binaries to all nodes +3. All nodes must upgrade before fork height + +--- + +## Quick Reference + +### Environment Variables + +| Variable | Description | Example | +|----------|-------------|---------| +| `SYNOR_BOOTSTRAP_PEERS` | Comma-separated bootstrap multiaddrs | `/dns4/seed1.../p2p/12D3...` | +| `SYNOR_DATA_DIR` | Data directory path | `/var/lib/synor` | +| `SYNOR_LOG_LEVEL` | Log verbosity | `info`, `debug`, `trace` | +| `FAUCET_WALLET_KEY` | Faucet private key (use secrets manager!) | - | +| `EXPLORER_CORS_ORIGINS` | Allowed CORS origins | `https://explorer.synor.cc` | + +### Default Ports (Testnet) + +| Service | Port | +|---------|------| +| P2P | 17511 | +| RPC | 17110 | +| WebSocket | 17111 | +| Metrics | 9090 | +| Faucet | 8080 | +| Explorer | 3000 | + +--- + +*Last updated: January 2026* diff --git a/docs/SECURITY_AUDIT_SCOPE.md b/docs/SECURITY_AUDIT_SCOPE.md new file mode 100644 index 0000000..4d0b72d --- /dev/null +++ b/docs/SECURITY_AUDIT_SCOPE.md @@ -0,0 +1,259 @@ +# Synor Blockchain Security Audit Scope + +This document defines the scope for external security audits of the Synor blockchain. + +--- + +## 1. Overview + +**Project**: Synor - High-throughput blockDAG with quantum-resistant cryptography +**Language**: Rust (core), TypeScript (web wallet) +**Audit Priority**: High (pre-mainnet requirement) + +### Key Innovations to Audit +- GHOSTDAG consensus with PHANTOM ordering +- Hybrid Ed25519 + Dilithium3 post-quantum signatures +- WASM-based smart contract VM +- Custom UTXO model with parallel validation + +--- + +## 2. Audit Scope by Priority + +### 2.1 Critical Priority (Must Audit) + +#### Cryptographic Primitives (`crates/synor-crypto/`) + +| Component | File | Focus Areas | +|-----------|------|-------------| +| Keypair generation | `src/keypair.rs` | Entropy sources, secure randomness | +| Ed25519 signatures | `src/signature.rs` | Signature malleability, validation | +| Dilithium3 PQC | `src/dilithium.rs` | Parameter validation, side-channel resistance | +| Hybrid signatures | `src/hybrid.rs` | Composition correctness, fallback behavior | +| Address derivation | `src/address.rs` | Bech32m encoding, checksum validation | +| Key encryption | `src/lib.rs` | AES-256-GCM, Argon2 parameters | + +**Specific Concerns**: +- Verify Dilithium3 implementation matches FIPS 204 draft +- Check for timing side-channels in signature verification +- Validate entropy sources on different platforms + +#### Consensus (`crates/synor-consensus/`) + +| Component | File | Focus Areas | +|-----------|------|-------------| +| GHOSTDAG | `src/ghostdag.rs` | K-cluster selection, anticone calculation | +| Ordering | `src/ordering.rs` | Topological sort, merge set computation | +| Block validation | `src/validation.rs` | PoW verification, timestamp checks | +| Difficulty adjustment | `src/difficulty.rs` | DAA window, manipulation resistance | +| Finality | `src/finality.rs` | Finality depth, reorg resistance | + +**Specific Concerns**: +- GHOSTDAG K parameter (K=18) sufficient for 10 BPS? +- DAA vulnerability to timestamp manipulation +- Selfish mining / withholding attack resistance + +#### DAG Structure (`crates/synor-dag/`) + +| Component | File | Focus Areas | +|-----------|------|-------------| +| Block storage | `src/store.rs` | Hash collision handling | +| Parent selection | `src/relations.rs` | Tip selection algorithm | +| Blue score | `src/blue_score.rs` | Score computation correctness | + +### 2.2 High Priority + +#### Smart Contract VM (`crates/synor-vm/`) + +| Component | File | Focus Areas | +|-----------|------|-------------| +| WASM executor | `src/executor.rs` | Sandbox escape, memory isolation | +| Gas metering | `src/gas.rs` | Metering accuracy, DoS prevention | +| Host functions | `src/host.rs` | Input validation, state access | +| Memory management | `src/memory.rs` | Bounds checking, overflow | + +**Specific Concerns**: +- WASM sandbox escape vulnerabilities +- Gas exhaustion attacks +- Host function privilege escalation +- Stack overflow in recursive contracts + +#### Transaction Processing (`crates/synor-types/`) + +| Component | File | Focus Areas | +|-----------|------|-------------| +| Transaction structure | `src/transaction.rs` | Signature verification order | +| UTXO management | `src/utxo.rs` | Double-spend prevention | +| Script validation | `src/script.rs` | Opcode security | + +### 2.3 Medium Priority + +#### Network Layer (`crates/synor-network/`) + +| Component | File | Focus Areas | +|-----------|------|-------------| +| P2P protocol | `src/service.rs` | Message validation, DoS vectors | +| Peer reputation | `src/reputation.rs` | Ban bypass, sybil resistance | +| Rate limiting | `src/rate_limit.rs` | Token bucket implementation | +| Sync protocol | `src/sync/` | Malicious peer handling | + +**Specific Concerns**: +- Eclipse attack resistance +- Network partition detection accuracy +- Gossipsub topic amplification + +#### Storage (`crates/synor-storage/`) + +| Component | File | Focus Areas | +|-----------|------|-------------| +| Block storage | `src/block_store.rs` | Corruption recovery | +| UTXO set | `src/utxo_store.rs` | Consistency guarantees | +| Pruning | `src/pruning.rs` | Data availability after prune | + +### 2.4 Lower Priority + +#### RPC API (`crates/synor-rpc/`) + +| Component | Focus Areas | +|-----------|-------------| +| JSON-RPC handlers | Input validation, injection | +| WebSocket | Connection limits, memory exhaustion | +| Rate limiting | Bypass prevention | + +#### Governance (`crates/synor-governance/`) + +| Component | Focus Areas | +|-----------|-------------| +| DAO voting | Vote weight manipulation | +| Treasury | Withdrawal limits, timelocks | +| Proposals | Execution safety | + +#### Node Application (`apps/synord/`) + +| Component | Focus Areas | +|-----------|-------------| +| Configuration | Secrets handling | +| Service orchestration | Race conditions | +| CLI wallet | Key storage security | + +--- + +## 3. Out of Scope + +The following are **not** in scope for the initial audit: + +- Third-party dependencies (covered by cargo-audit) +- Web wallet frontend (separate web security audit) +- DevOps/infrastructure security +- Physical security of node operators +- Social engineering vectors + +--- + +## 4. Threat Model + +### 4.1 Adversary Capabilities + +| Level | Description | Mitigations Expected | +|-------|-------------|---------------------| +| L1 | Remote attacker, no stake | Network protocol security | +| L2 | Minority miner (<33% hashrate) | Consensus security | +| L3 | Majority miner (>50% hashrate) | Finality guarantees | +| L4 | Quantum computer access | Dilithium3 signatures | +| L5 | Nation-state (future) | Quantum + classical resistance | + +### 4.2 Key Security Properties + +1. **Consensus Safety**: No conflicting finalized blocks +2. **Liveness**: Transactions confirm within reasonable time +3. **Censorship Resistance**: No single entity can block transactions +4. **Key Security**: Private keys protected from extraction +5. **Quantum Resistance**: Secure against future quantum computers + +--- + +## 5. Prior Work & References + +### Consensus +- PHANTOM/GHOSTDAG papers (Sompolinsky & Zohar) +- Kaspa implementation reference +- DAGKnight improvements + +### Cryptography +- FIPS 204 (Dilithium) draft specification +- Ed25519 (RFC 8032) +- Bech32m (BIP-350) +- Argon2 (RFC 9106) + +### Smart Contracts +- WASM specification +- Wasmtime security model + +--- + +## 6. Deliverables Expected + +1. **Full Report**: Detailed findings with severity ratings +2. **Executive Summary**: Non-technical overview +3. **Findings by Category**: + - Critical (immediate action required) + - High (fix before mainnet) + - Medium (fix within 30 days) + - Low (best practice improvements) + - Informational (suggestions) + +4. **Proof of Concepts**: For any exploitable vulnerabilities +5. **Remediation Verification**: Re-check after fixes + +--- + +## 7. Audit Timeline + +| Phase | Duration | Description | +|-------|----------|-------------| +| Kickoff | 1 day | Scope review, access setup | +| Crypto Audit | 2 weeks | synor-crypto, synor-consensus | +| VM Audit | 1 week | synor-vm, contract security | +| Network Audit | 1 week | synor-network, P2P protocols | +| Report | 1 week | Findings documentation | +| Remediation | 2 weeks | Fix implementation | +| Verification | 3 days | Re-audit of fixes | + +**Total**: ~7 weeks + +--- + +## 8. Contact & Resources + +### Repository Access +- Main repo: `github.com/g1-technologies/synor` (private until audit) +- Test vectors: `docs/test-vectors/` +- Architecture docs: `docs/architecture/` + +### Points of Contact +- Technical Lead: [To be assigned] +- Security Lead: [To be assigned] + +### Development Environment +- Rust 1.75+ +- `wasm32-unknown-unknown` target +- All tests: `cargo test --workspace` +- Benchmarks: `cargo bench --workspace` + +--- + +## 9. Previous Audits + +None (first external audit) + +--- + +## 10. Changelog + +| Date | Version | Changes | +|------|---------|---------| +| 2026-01-08 | 1.0 | Initial scope document | + +--- + +*Prepared for Phase 7: Production Readiness*