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:
parent
8bdc9d6086
commit
b22c1b89f0
12 changed files with 1501 additions and 336 deletions
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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
240
apps/faucet/src/secrets.rs
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
```
|
||||||
|
|
|
||||||
221
crates/synor-crypto-wasm/src/address.rs
Normal file
221
crates/synor-crypto-wasm/src/address.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
115
crates/synor-crypto-wasm/src/mnemonic_wasm.rs
Normal file
115
crates/synor-crypto-wasm/src/mnemonic_wasm.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
276
docs/DEPLOYMENT.md
Normal 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*
|
||||||
259
docs/SECURITY_AUDIT_SCOPE.md
Normal file
259
docs/SECURITY_AUDIT_SCOPE.md
Normal 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*
|
||||||
Loading…
Add table
Reference in a new issue