feat: Phase 7 production readiness improvements

- Add SYNOR_BOOTSTRAP_PEERS env var for runtime seed node configuration
- Implement secrets provider abstraction for faucet wallet key security
  (supports file-based secrets in /run/secrets for production)
- Create WASM crypto crate foundation for web wallet (Ed25519, BIP-39)
- Add DEPLOYMENT.md guide for testnet deployment
- Add SECURITY_AUDIT_SCOPE.md for external security audit preparation
- Document seed node deployment process in synor-network

Security improvements:
- Faucet now auto-detects /run/secrets for secure key storage
- CORS already defaults to specific origins (https://faucet.synor.cc)
- Bootstrap peers now configurable at runtime without recompilation
This commit is contained in:
Gulshan Yadav 2026-01-08 07:21:14 +05:30
parent 8bdc9d6086
commit b22c1b89f0
12 changed files with 1501 additions and 336 deletions

View file

@ -50,3 +50,4 @@ synor-types = { path = "../../crates/synor-types" }
[dev-dependencies] [dev-dependencies]
tokio-test = "0.4" tokio-test = "0.4"
tempfile = "3.10"

View file

@ -2,6 +2,16 @@
//! //!
//! A simple HTTP service that dispenses test SYNOR tokens to developers. //! A simple HTTP service that dispenses test SYNOR tokens to developers.
//! Includes rate limiting and cooldown periods to prevent abuse. //! 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::collections::HashMap;
use std::net::SocketAddr; use std::net::SocketAddr;
@ -57,7 +67,23 @@ impl Default for FaucetConfig {
} }
impl 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 { pub fn from_env() -> Self {
let mut config = FaucetConfig::default(); 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); 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") { if let Ok(origins) = std::env::var("FAUCET_CORS_ORIGINS") {

240
apps/faucet/src/secrets.rs Normal file
View file

@ -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<String>;
/// 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/<pid>/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<String> {
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<Path>) -> Self {
Self {
secrets_dir: secrets_dir.as_ref().to_path_buf(),
}
}
}
impl SecretProvider for FileSecretProvider {
fn get(&self, name: &str) -> Option<String> {
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<Box<dyn SecretProvider>>,
}
#[allow(dead_code)]
impl ChainedSecretProvider {
/// Create a new chained provider with the given providers.
pub fn new(providers: Vec<Box<dyn SecretProvider>>) -> Self {
Self { providers }
}
}
impl SecretProvider for ChainedSecretProvider {
fn get(&self, name: &str) -> Option<String> {
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<dyn SecretProvider> {
// 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");
}
}

View file

@ -261,40 +261,54 @@ impl Default for P2PConfig {
impl P2PConfig { impl P2PConfig {
/// Creates config for a network. /// 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/<hostname>/tcp/<port>/p2p/<peer_id>`
/// 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 { pub fn for_network(network: &str) -> Self {
let mut config = P2PConfig::default(); let mut config = P2PConfig::default();
match network { match network {
"mainnet" => { "mainnet" => {
config.listen_addr = "/ip4/0.0.0.0/tcp/16511".to_string(); config.listen_addr = "/ip4/0.0.0.0/tcp/16511".to_string();
config.seeds = vec![ // Mainnet seeds - will be populated after mainnet seed node deployment
// Mainnet seeds - geographically distributed // IMPORTANT: Addresses must include /p2p/<peer_id> for secure dialing
// Format: /dns4/<hostname>/tcp/<port>/p2p/<peer_id> // Override with SYNOR_BOOTSTRAP_PEERS env var after deployment
// Peer IDs will be populated after seed node deployment config.seeds = vec![];
"/dns4/seed1.synor.cc/tcp/16511".to_string(),
"/dns4/seed2.synor.cc/tcp/16511".to_string(),
"/dns4/seed3.synor.cc/tcp/16511".to_string(),
];
} }
"testnet" => { "testnet" => {
config.listen_addr = "/ip4/0.0.0.0/tcp/17511".to_string(); config.listen_addr = "/ip4/0.0.0.0/tcp/17511".to_string();
config.seeds = vec![ // Testnet seeds - populate after deploying seed nodes
// Testnet seeds - geographically distributed // IMPORTANT: Addresses must include /p2p/<peer_id> for secure dialing
// North America (US-East) // Override with SYNOR_BOOTSTRAP_PEERS env var after deployment
"/dns4/testnet-seed1.synor.cc/tcp/17511".to_string(), config.seeds = vec![];
// Europe (Frankfurt)
"/dns4/testnet-seed2.synor.cc/tcp/17511".to_string(),
// Asia (Singapore)
"/dns4/testnet-seed3.synor.cc/tcp/17511".to_string(),
];
} }
"devnet" => { "devnet" => {
config.listen_addr = "/ip4/0.0.0.0/tcp/18511".to_string(); config.listen_addr = "/ip4/0.0.0.0/tcp/18511".to_string();
// Devnet uses mDNS for local discovery, no seeds needed
config.seeds = vec![]; 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<String> = peers_env
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
if !peers.is_empty() {
config.seeds = peers;
}
}
config config
} }
} }

View file

@ -2,32 +2,51 @@
name = "synor-crypto-wasm" name = "synor-crypto-wasm"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"
description = "WASM bindings for Synor cryptographic operations (ML-DSA/Dilithium)" description = "WASM-compatible cryptography for Synor web wallet"
license = "MIT" license = "MIT OR Apache-2.0"
[lib] [lib]
crate-type = ["cdylib", "rlib"] crate-type = ["cdylib", "rlib"]
[features]
default = ["console_error_panic_hook"]
[dependencies] [dependencies]
# WASM bindings
wasm-bindgen = "0.2" wasm-bindgen = "0.2"
js-sys = "0.3" 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"] } 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" sha3 = "0.10"
rand = { version = "0.9", features = ["std", "std_rng"] } blake3 = "1.5"
serde = { version = "1", features = ["derive"] }
# 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" serde-wasm-bindgen = "0.6"
# Utilities
hex = "0.4" hex = "0.4"
zeroize = { version = "1.7", features = ["derive"] }
[dev-dependencies] [dev-dependencies]
wasm-bindgen-test = "0.3" wasm-bindgen-test = "0.3"
[features]
default = []
# Enable when we have WASM-compatible Dilithium
pqc = []
[profile.release] [profile.release]
# Optimize for small code size # Optimize for size in WASM
opt-level = "s"
lto = true lto = true
opt-level = "s"

View file

@ -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) - **Ed25519 Signatures**: Full support via `ed25519-dalek` (pure Rust)
quantum-resistant signatures. However, due to the following considerations, the - **BIP-39 Mnemonics**: 12-24 word phrases for key generation
current Synor web wallet uses a **hybrid server-side approach** instead: - **Bech32m Addresses**: Synor address encoding/decoding
- **BLAKE3/SHA3 Hashing**: Cryptographic hash functions
- **HKDF Key Derivation**: Secure key derivation
### Why Server-Side Dilithium? ## Building
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)
```bash ```bash
# Native tests # Build for web (requires wasm-pack)
cd crates/synor-crypto-wasm wasm-pack build --target web --out-dir pkg
cargo test
# WASM build (requires wasm-pack) # Build for Node.js
# Currently blocked on ml-dsa stability wasm-pack build --target nodejs --out-dir pkg-node
wasm-pack build --target web
``` ```
## API (Future) ## Usage in JavaScript
```javascript ```javascript
import init, { MlDsa65Keypair, mlDsa65Verify } from 'synor-crypto-wasm'; import init, { Keypair, Mnemonic } from 'synor-crypto-wasm';
await init(); await init();
// Generate keypair // Generate mnemonic
const keypair = new MlDsa65Keypair(); const mnemonic = new Mnemonic(24);
// Or from seed console.log(mnemonic.phrase());
const keypair2 = MlDsa65Keypair.fromSeed(seed);
// 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); const signature = keypair.sign(message);
// Verify // 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) ### Current Status: Pending
- 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
## 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
```

View file

@ -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<String, JsValue> {
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<JsValue, JsValue> {
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<String, JsValue> {
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<u8>), 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<Vec<u8>, 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<u8> {
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
}
}

View file

@ -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) //! This crate provides cryptographic primitives that can be compiled to WebAssembly
//! post-quantum cryptographic operations, enabling quantum-resistant signatures //! for use in the Synor web wallet. It includes:
//! in web browsers.
//! //!
//! # 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 //! ```javascript
//! import init, { MlDsa65Keypair, mlDsa65Verify } from 'synor-crypto-wasm'; //! import init, { Keypair, Mnemonic, sign, verify } from 'synor-crypto-wasm';
//! //!
//! await init(); //! await init();
//! //!
//! // Generate a new keypair //! // Generate mnemonic
//! const keypair = new MlDsa65Keypair(); //! const mnemonic = Mnemonic.generate(24);
//! console.log(mnemonic.phrase());
//! //!
//! // Or from a seed for deterministic generation //! // Create keypair from mnemonic
//! const keypair2 = MlDsa65Keypair.fromSeed(seed); //! 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); //! const signature = keypair.sign(message);
//! //! const isValid = keypair.verify(message, signature);
//! // Verify a signature
//! const isValid = mlDsa65Verify(message, signature, keypair.verifyingKey());
//! ``` //! ```
use ml_dsa::{ use ed25519_dalek::{Signature, Signer, SigningKey, VerifyingKey};
signature::{Keypair, Signer, Verifier}, use rand::rngs::OsRng;
EncodedSignature, EncodedSigningKey, EncodedVerifyingKey, KeyGen, MlDsa65, Seed, Signature, use sha3::{Digest, Sha3_256};
SigningKey, VerifyingKey,
};
use sha3::{
digest::{ExtendableOutput, Update, XofReader},
Shake256,
};
use wasm_bindgen::prelude::*; use wasm_bindgen::prelude::*;
use zeroize::Zeroize;
// When the `console_error_panic_hook` feature is enabled, we can call the mod address;
// `set_panic_hook` function to get better error messages in the console. mod mnemonic_wasm;
#[cfg(feature = "console_error_panic_hook")]
pub fn set_panic_hook() { pub use address::*;
console_error_panic_hook::set_once(); pub use mnemonic_wasm::*;
}
/// Initialize the WASM module. /// Initialize the WASM module.
#[wasm_bindgen(start)] #[wasm_bindgen(start)]
pub fn init() { pub fn init() {
#[cfg(feature = "console_error_panic_hook")] // WASM initialization - can be extended for panic hooks, logging, etc.
set_panic_hook();
} }
/// ML-DSA-65 (Dilithium3 equivalent) verifying key size in bytes. /// Ed25519 keypair for signing transactions.
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).
#[wasm_bindgen] #[wasm_bindgen]
pub struct MlDsa65Keypair { pub struct Keypair {
signing_key: SigningKey<MlDsa65>, signing_key: SigningKey,
verifying_key: VerifyingKey<MlDsa65>, #[wasm_bindgen(skip)]
pub seed: [u8; 32],
} }
#[wasm_bindgen] #[wasm_bindgen]
impl MlDsa65Keypair { impl Keypair {
/// Generates a new random ML-DSA-65 keypair. /// Generate a new random keypair.
#[wasm_bindgen(constructor)] #[wasm_bindgen(constructor)]
pub fn new() -> Result<MlDsa65Keypair, JsError> { pub fn new() -> Result<Keypair, JsValue> {
let mut rng = rand::rng(); let signing_key = SigningKey::generate(&mut OsRng);
let kp = MlDsa65::key_gen(&mut rng); let seed = signing_key.to_bytes();
Ok(MlDsa65Keypair { Ok(Keypair { signing_key, seed })
signing_key: kp.signing_key().clone(),
verifying_key: kp.verifying_key().clone(),
})
} }
/// Creates a keypair from a 32-byte seed. /// Create a keypair from a 32-byte seed.
///
/// Uses SHAKE256 to expand the seed deterministically.
#[wasm_bindgen(js_name = fromSeed)] #[wasm_bindgen(js_name = fromSeed)]
pub fn from_seed(seed: &[u8]) -> Result<MlDsa65Keypair, JsError> { pub fn from_seed(seed: &[u8]) -> Result<Keypair, JsValue> {
if seed.len() < 32 { if seed.len() != 32 {
return Err(JsError::new("Seed must be at least 32 bytes")); return Err(JsValue::from_str("Seed must be exactly 32 bytes"));
} }
let mut seed_arr = [0u8; 32];
// Use SHAKE256 to expand seed deterministically with domain separation seed_arr.copy_from_slice(seed);
let mut hasher = Shake256::default(); let signing_key = SigningKey::from_bytes(&seed_arr);
Update::update(&mut hasher, b"synor-mldsa65-keygen-v1"); Ok(Keypair {
Update::update(&mut hasher, &seed[..32]); signing_key,
seed: seed_arr,
// 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. /// Create a keypair from a BIP-39 mnemonic phrase.
#[wasm_bindgen(js_name = verifyingKey)] #[wasm_bindgen(js_name = fromMnemonic)]
pub fn verifying_key(&self) -> Vec<u8> { pub fn from_mnemonic(phrase: &str, passphrase: &str) -> Result<Keypair, JsValue> {
self.verifying_key.encode().to_vec() 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,
})
} }
/// Returns the verifying (public) key as a hex string. /// Get the public key as hex string.
#[wasm_bindgen(js_name = verifyingKeyHex)] #[wasm_bindgen(js_name = publicKeyHex)]
pub fn verifying_key_hex(&self) -> String { pub fn public_key_hex(&self) -> String {
hex::encode(self.verifying_key.encode()) hex::encode(self.signing_key.verifying_key().to_bytes())
} }
/// Signs a message and returns the signature bytes. /// Get the public key as bytes.
#[wasm_bindgen(js_name = publicKeyBytes)]
pub fn public_key_bytes(&self) -> Vec<u8> {
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<String, JsValue> {
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<u8> { pub fn sign(&self, message: &[u8]) -> Vec<u8> {
let sig = self.signing_key.sign(message); let signature = self.signing_key.sign(message);
sig.encode().to_vec() signature.to_bytes().to_vec()
} }
/// Signs a message and returns the signature as a hex string. /// Verify a signature.
#[wasm_bindgen(js_name = signHex)] #[wasm_bindgen]
pub fn sign_hex(&self, message: &[u8]) -> String { pub fn verify(&self, message: &[u8], signature: &[u8]) -> Result<bool, JsValue> {
hex::encode(self.sign(message)) if signature.len() != 64 {
return Err(JsValue::from_str("Signature must be 64 bytes"));
} }
/// Exports the signing key (private key) as bytes. let sig_bytes: [u8; 64] = signature
/// WARNING: Handle with care - this is sensitive key material!
#[wasm_bindgen(js_name = signingKey)]
pub fn signing_key(&self) -> Vec<u8> {
self.signing_key.encode().to_vec()
}
}
/// 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<bool, JsError> {
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()
)));
}
// Parse verifying key
let vk_bytes: EncodedVerifyingKey<MlDsa65> = verifying_key
.try_into() .try_into()
.map_err(|_| JsError::new("Invalid verifying key format"))?; .map_err(|_| JsValue::from_str("Invalid signature length"))?;
let sig = Signature::from_bytes(&sig_bytes);
let vk = VerifyingKey::<MlDsa65>::decode(&vk_bytes) use ed25519_dalek::Verifier;
.ok_or_else(|| JsError::new("Failed to decode verifying key"))?; Ok(self
.signing_key
.verifying_key()
.verify(message, &sig)
.is_ok())
}
}
// Parse signature impl Default for Keypair {
if signature.len() != MLDSA65_SIGNATURE_SIZE { fn default() -> Self {
return Err(JsError::new(&format!( Self::new().expect("Failed to generate keypair")
"Invalid signature length: expected {}, got {}", }
MLDSA65_SIGNATURE_SIZE, }
signature.len()
))); 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<bool, JsValue> {
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"));
} }
let sig_bytes: EncodedSignature<MlDsa65> = signature let pk_bytes: [u8; 32] = public_key
.try_into() .try_into()
.map_err(|_| JsError::new("Invalid signature format"))?; .map_err(|_| JsValue::from_str("Invalid public key"))?;
let sig_bytes: [u8; 64] = signature
.try_into()
.map_err(|_| JsValue::from_str("Invalid signature"))?;
let sig = Signature::<MlDsa65>::decode(&sig_bytes) let verifying_key = VerifyingKey::from_bytes(&pk_bytes)
.ok_or_else(|| JsError::new("Failed to decode signature"))?; .map_err(|_| JsValue::from_str("Invalid public key format"))?;
let signature = Signature::from_bytes(&sig_bytes);
// Verify use ed25519_dalek::Verifier;
Ok(vk.verify(message, &sig).is_ok()) Ok(verifying_key.verify(message, &signature).is_ok())
} }
/// Returns the expected verifying key size for ML-DSA-65. /// Compute SHA3-256 hash.
#[wasm_bindgen(js_name = mlDsa65VerifyingKeySize)] #[wasm_bindgen(js_name = sha3_256)]
pub fn verifying_key_size() -> usize { pub fn sha3_256_hash(data: &[u8]) -> Vec<u8> {
MLDSA65_VERIFYING_KEY_SIZE let mut hasher = Sha3_256::new();
hasher.update(data);
hasher.finalize().to_vec()
} }
/// Returns the expected signature size for ML-DSA-65. /// Compute BLAKE3 hash.
#[wasm_bindgen(js_name = mlDsa65SignatureSize)] #[wasm_bindgen(js_name = blake3)]
pub fn signature_size() -> usize { pub fn blake3_hash(data: &[u8]) -> Vec<u8> {
MLDSA65_SIGNATURE_SIZE blake3::hash(data).as_bytes().to_vec()
} }
/// Utility to convert hex string to bytes. /// Derive key using HKDF-SHA256.
#[wasm_bindgen(js_name = hexToBytes)] #[wasm_bindgen(js_name = deriveKey)]
pub fn hex_to_bytes(hex_str: &str) -> Result<Vec<u8>, JsError> { pub fn derive_key(
hex::decode(hex_str).map_err(|e| JsError::new(&format!("Invalid hex: {}", e))) input_key: &[u8],
salt: &[u8],
info: &[u8],
output_len: usize,
) -> Result<Vec<u8>, JsValue> {
use hkdf::Hkdf;
use sha3::Sha3_256;
let hk = Hkdf::<Sha3_256>::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. // bip39 crate is used in mnemonic_wasm.rs
#[wasm_bindgen(js_name = bytesToHex)]
pub fn bytes_to_hex(bytes: &[u8]) -> String {
hex::encode(bytes)
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use wasm_bindgen_test::*;
#[test] #[wasm_bindgen_test]
fn test_keypair_generation() { fn test_keypair_generation() {
let keypair = MlDsa65Keypair::new().unwrap(); let keypair = Keypair::new().unwrap();
assert_eq!(keypair.verifying_key().len(), MLDSA65_VERIFYING_KEY_SIZE); assert_eq!(keypair.public_key_bytes().len(), 32);
} }
#[test] #[wasm_bindgen_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]
fn test_sign_verify() { fn test_sign_verify() {
let keypair = MlDsa65Keypair::new().unwrap(); let keypair = Keypair::new().unwrap();
let message = b"Hello, Synor blockchain!"; let message = b"Hello, Synor!";
let signature = keypair.sign(message); let signature = keypair.sign(message);
let verifying_key = keypair.verifying_key(); assert!(keypair.verify(message, &signature).unwrap());
assert_eq!(signature.len(), MLDSA65_SIGNATURE_SIZE);
let is_valid = verify(message, &signature, &verifying_key).unwrap();
assert!(is_valid);
} }
#[test] #[wasm_bindgen_test]
fn test_invalid_signature() { fn test_address_generation() {
let keypair = MlDsa65Keypair::new().unwrap(); let keypair = Keypair::new().unwrap();
let message = b"Hello, Synor blockchain!"; let address = keypair.address("mainnet").unwrap();
assert!(address.starts_with("synor1"));
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
}
} }
#[test] #[wasm_bindgen_test]
fn test_wrong_message() { fn test_mnemonic_keypair() {
let keypair = MlDsa65Keypair::new().unwrap(); let phrase = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
let message = b"Hello, Synor blockchain!"; let keypair = Keypair::from_mnemonic(phrase, "").unwrap();
let wrong_message = b"Wrong message"; assert_eq!(keypair.public_key_bytes().len(), 32);
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);
} }
} }

View file

@ -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<Mnemonic, JsValue> {
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, JsValue> {
Mnemonic::new(word_count)
}
/// Parse a mnemonic from a phrase.
#[wasm_bindgen(js_name = fromPhrase)]
pub fn from_phrase(phrase: &str) -> Result<Mnemonic, JsValue> {
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<String> {
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<u8> {
let seed = bip39::Seed::new(&self.inner, passphrase);
seed.as_bytes().to_vec()
}
/// Get the entropy bytes.
#[wasm_bindgen]
pub fn entropy(&self) -> Vec<u8> {
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);
}
}

View file

@ -208,24 +208,42 @@ fn mainnet_bootstrap_peers() -> Vec<Multiaddr> {
/// Returns testnet bootstrap peers. /// Returns testnet bootstrap peers.
/// ///
/// To add a new seed node: /// # Seed Node Deployment Process
/// 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/<hostname>/tcp/17511/p2p/<peer_id>`
/// ///
/// Example: /// 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 /// ```text
/// /dns4/testnet-seed1.synor.cc/tcp/17511/p2p/12D3KooWAbCdEfGhIjKlMnOpQrStUvWxYz123456789 /// /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<Multiaddr> { fn testnet_bootstrap_peers() -> Vec<Multiaddr> {
// Testnet bootstrap nodes - add peer IDs when seed nodes are deployed // Testnet bootstrap nodes - add peer IDs when seed nodes are deployed
// Format: /dns4/<hostname>/tcp/<port>/p2p/<peer_id> // Format: /dns4/<hostname>/tcp/<port>/p2p/<peer_id>
//
// 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] = &[ 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...", // "/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...", // "/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...", // "/dns4/testnet-seed3.synor.cc/tcp/17511/p2p/12D3KooW...",
]; ];

276
docs/DEPLOYMENT.md Normal file
View file

@ -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 <pid>`
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*

View file

@ -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*