mirror of
https://github.com/jmcorgan/fips.git
synced 2026-08-09 00:04:54 +00:00
Add YAML configuration system and nsec encoding
Configuration system: - New config module with cascading file search - Priority: ./fips.yaml > ~/.config/fips/ > ~/.fips.yaml > /etc/fips/ - Identity section with optional nsec (bech32 or hex format) - Generate new keypair if nsec not configured Identity module additions: - encode_nsec() and decode_nsec() for NIP-19 bech32 format - decode_secret() accepts both nsec and hex formats - Identity::from_secret_str() constructor Dependencies: serde, serde_yaml, dirs, hex 41 tests passing (20 identity + 15 config + 6 nsec)
This commit is contained in:
+366
@@ -0,0 +1,366 @@
|
||||
//! FIPS Configuration System
|
||||
//!
|
||||
//! Loads configuration from YAML files with a cascading priority system:
|
||||
//! 1. `./fips.yaml` (current directory - highest priority)
|
||||
//! 2. `~/.config/fips/fips.yaml` (user config directory)
|
||||
//! 3. `/etc/fips/fips.yaml` (system - lowest priority)
|
||||
//!
|
||||
//! Values from higher priority files override those from lower priority files.
|
||||
|
||||
use crate::{Identity, IdentityError};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::{Path, PathBuf};
|
||||
use thiserror::Error;
|
||||
|
||||
/// Default config filename.
|
||||
const CONFIG_FILENAME: &str = "fips.yaml";
|
||||
|
||||
/// Errors that can occur during configuration loading.
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ConfigError {
|
||||
#[error("failed to read config file {path}: {source}")]
|
||||
ReadFile {
|
||||
path: PathBuf,
|
||||
source: std::io::Error,
|
||||
},
|
||||
|
||||
#[error("failed to parse config file {path}: {source}")]
|
||||
ParseYaml {
|
||||
path: PathBuf,
|
||||
source: serde_yaml::Error,
|
||||
},
|
||||
|
||||
#[error("identity error: {0}")]
|
||||
Identity(#[from] IdentityError),
|
||||
}
|
||||
|
||||
/// Identity configuration section.
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct IdentityConfig {
|
||||
/// Node secret key in nsec (bech32) or hex format.
|
||||
/// If not specified, a new keypair will be generated.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub nsec: Option<String>,
|
||||
}
|
||||
|
||||
/// Root configuration structure.
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct Config {
|
||||
/// Identity configuration.
|
||||
#[serde(default)]
|
||||
pub identity: IdentityConfig,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
/// Create a new empty configuration.
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Load configuration from the standard search paths.
|
||||
///
|
||||
/// Files are loaded in reverse priority order and merged:
|
||||
/// 1. `/etc/fips/fips.yaml` (loaded first, lowest priority)
|
||||
/// 2. `~/.config/fips/fips.yaml` (user config)
|
||||
/// 3. `./fips.yaml` (loaded last, highest priority)
|
||||
///
|
||||
/// Returns a tuple of (config, paths_loaded) where paths_loaded contains
|
||||
/// the paths that were successfully loaded.
|
||||
pub fn load() -> Result<(Self, Vec<PathBuf>), ConfigError> {
|
||||
let search_paths = Self::search_paths();
|
||||
Self::load_from_paths(&search_paths)
|
||||
}
|
||||
|
||||
/// Load configuration from specific paths.
|
||||
///
|
||||
/// Paths are processed in order, with later paths overriding earlier ones.
|
||||
pub fn load_from_paths(paths: &[PathBuf]) -> Result<(Self, Vec<PathBuf>), ConfigError> {
|
||||
let mut config = Config::default();
|
||||
let mut loaded_paths = Vec::new();
|
||||
|
||||
for path in paths {
|
||||
if path.exists() {
|
||||
let file_config = Self::load_file(path)?;
|
||||
config.merge(file_config);
|
||||
loaded_paths.push(path.clone());
|
||||
}
|
||||
}
|
||||
|
||||
Ok((config, loaded_paths))
|
||||
}
|
||||
|
||||
/// Load configuration from a single file.
|
||||
pub fn load_file(path: &Path) -> Result<Self, ConfigError> {
|
||||
let contents = std::fs::read_to_string(path).map_err(|e| ConfigError::ReadFile {
|
||||
path: path.to_path_buf(),
|
||||
source: e,
|
||||
})?;
|
||||
|
||||
serde_yaml::from_str(&contents).map_err(|e| ConfigError::ParseYaml {
|
||||
path: path.to_path_buf(),
|
||||
source: e,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the standard search paths in priority order (lowest to highest).
|
||||
pub fn search_paths() -> Vec<PathBuf> {
|
||||
let mut paths = Vec::new();
|
||||
|
||||
// System config (lowest priority)
|
||||
paths.push(PathBuf::from("/etc/fips").join(CONFIG_FILENAME));
|
||||
|
||||
// User config directory
|
||||
if let Some(config_dir) = dirs::config_dir() {
|
||||
paths.push(config_dir.join("fips").join(CONFIG_FILENAME));
|
||||
}
|
||||
|
||||
// Home directory (legacy location)
|
||||
if let Some(home_dir) = dirs::home_dir() {
|
||||
paths.push(home_dir.join(".fips.yaml"));
|
||||
}
|
||||
|
||||
// Current directory (highest priority)
|
||||
paths.push(PathBuf::from(".").join(CONFIG_FILENAME));
|
||||
|
||||
paths
|
||||
}
|
||||
|
||||
/// Merge another configuration into this one.
|
||||
///
|
||||
/// Values from `other` override values in `self` when present.
|
||||
pub fn merge(&mut self, other: Config) {
|
||||
// Merge identity section
|
||||
if other.identity.nsec.is_some() {
|
||||
self.identity.nsec = other.identity.nsec;
|
||||
}
|
||||
}
|
||||
|
||||
/// Create an Identity from this configuration.
|
||||
///
|
||||
/// If an nsec is configured, uses that to create the identity.
|
||||
/// Otherwise, generates a new random identity.
|
||||
pub fn create_identity(&self) -> Result<Identity, ConfigError> {
|
||||
match &self.identity.nsec {
|
||||
Some(nsec) => Ok(Identity::from_secret_str(nsec)?),
|
||||
None => Ok(Identity::generate()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if an identity is configured (vs. will be generated).
|
||||
pub fn has_identity(&self) -> bool {
|
||||
self.identity.nsec.is_some()
|
||||
}
|
||||
|
||||
/// Serialize this configuration to YAML.
|
||||
pub fn to_yaml(&self) -> Result<String, serde_yaml::Error> {
|
||||
serde_yaml::to_string(self)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::fs;
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[test]
|
||||
fn test_empty_config() {
|
||||
let config = Config::new();
|
||||
assert!(config.identity.nsec.is_none());
|
||||
assert!(!config.has_identity());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_yaml_with_nsec() {
|
||||
let yaml = r#"
|
||||
identity:
|
||||
nsec: nsec1qyqsqypqxqszqg9qyqsqypqxqszqg9qyqsqypqxqszqg9qyqsqypqxfnm5g9
|
||||
"#;
|
||||
let config: Config = serde_yaml::from_str(yaml).unwrap();
|
||||
assert!(config.identity.nsec.is_some());
|
||||
assert!(config.has_identity());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_yaml_with_hex() {
|
||||
let yaml = r#"
|
||||
identity:
|
||||
nsec: "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20"
|
||||
"#;
|
||||
let config: Config = serde_yaml::from_str(yaml).unwrap();
|
||||
assert!(config.identity.nsec.is_some());
|
||||
|
||||
let identity = config.create_identity().unwrap();
|
||||
assert!(!identity.npub().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_yaml_empty() {
|
||||
let yaml = "";
|
||||
let config: Config = serde_yaml::from_str(yaml).unwrap();
|
||||
assert!(config.identity.nsec.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_yaml_partial() {
|
||||
let yaml = r#"
|
||||
identity: {}
|
||||
"#;
|
||||
let config: Config = serde_yaml::from_str(yaml).unwrap();
|
||||
assert!(config.identity.nsec.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_merge_configs() {
|
||||
let mut base = Config::new();
|
||||
base.identity.nsec = Some("base_nsec".to_string());
|
||||
|
||||
let override_config = Config {
|
||||
identity: IdentityConfig {
|
||||
nsec: Some("override_nsec".to_string()),
|
||||
},
|
||||
};
|
||||
|
||||
base.merge(override_config);
|
||||
assert_eq!(base.identity.nsec, Some("override_nsec".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_merge_preserves_base_when_override_empty() {
|
||||
let mut base = Config::new();
|
||||
base.identity.nsec = Some("base_nsec".to_string());
|
||||
|
||||
let override_config = Config::new();
|
||||
|
||||
base.merge(override_config);
|
||||
assert_eq!(base.identity.nsec, Some("base_nsec".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_identity_from_nsec() {
|
||||
let config = Config {
|
||||
identity: IdentityConfig {
|
||||
nsec: Some(
|
||||
"0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20".to_string(),
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
let identity = config.create_identity().unwrap();
|
||||
assert!(!identity.npub().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_identity_generates_new() {
|
||||
let config = Config::new();
|
||||
let identity = config.create_identity().unwrap();
|
||||
assert!(!identity.npub().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_load_from_file() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let config_path = temp_dir.path().join("fips.yaml");
|
||||
|
||||
let yaml = r#"
|
||||
identity:
|
||||
nsec: "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20"
|
||||
"#;
|
||||
fs::write(&config_path, yaml).unwrap();
|
||||
|
||||
let config = Config::load_file(&config_path).unwrap();
|
||||
assert!(config.identity.nsec.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_load_from_paths_merges() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
|
||||
// Create two config files
|
||||
let low_priority = temp_dir.path().join("low.yaml");
|
||||
let high_priority = temp_dir.path().join("high.yaml");
|
||||
|
||||
fs::write(
|
||||
&low_priority,
|
||||
r#"
|
||||
identity:
|
||||
nsec: "low_priority_nsec"
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
fs::write(
|
||||
&high_priority,
|
||||
r#"
|
||||
identity:
|
||||
nsec: "high_priority_nsec"
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let paths = vec![low_priority.clone(), high_priority.clone()];
|
||||
let (config, loaded) = Config::load_from_paths(&paths).unwrap();
|
||||
|
||||
assert_eq!(loaded.len(), 2);
|
||||
assert_eq!(config.identity.nsec, Some("high_priority_nsec".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_load_skips_missing_files() {
|
||||
let temp_dir = TempDir::new().unwrap();
|
||||
let existing = temp_dir.path().join("exists.yaml");
|
||||
let missing = temp_dir.path().join("missing.yaml");
|
||||
|
||||
fs::write(
|
||||
&existing,
|
||||
r#"
|
||||
identity:
|
||||
nsec: "existing_nsec"
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let paths = vec![missing, existing.clone()];
|
||||
let (config, loaded) = Config::load_from_paths(&paths).unwrap();
|
||||
|
||||
assert_eq!(loaded.len(), 1);
|
||||
assert_eq!(loaded[0], existing);
|
||||
assert_eq!(config.identity.nsec, Some("existing_nsec".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_search_paths_includes_expected() {
|
||||
let paths = Config::search_paths();
|
||||
|
||||
// Should include current directory
|
||||
assert!(paths.iter().any(|p| p.ends_with("fips.yaml")));
|
||||
|
||||
// Should include /etc/fips
|
||||
assert!(paths
|
||||
.iter()
|
||||
.any(|p| p.starts_with("/etc/fips") && p.ends_with("fips.yaml")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_to_yaml() {
|
||||
let config = Config {
|
||||
identity: IdentityConfig {
|
||||
nsec: Some("test_nsec".to_string()),
|
||||
},
|
||||
};
|
||||
|
||||
let yaml = config.to_yaml().unwrap();
|
||||
assert!(yaml.contains("identity:"));
|
||||
assert!(yaml.contains("nsec:"));
|
||||
assert!(yaml.contains("test_nsec"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_to_yaml_empty_nsec_omitted() {
|
||||
let config = Config::new();
|
||||
let yaml = config.to_yaml().unwrap();
|
||||
|
||||
// Empty nsec should not be serialized
|
||||
assert!(!yaml.contains("nsec:"));
|
||||
}
|
||||
}
|
||||
+141
@@ -15,6 +15,9 @@ use thiserror::Error;
|
||||
/// Human-readable part for npub (NIP-19).
|
||||
const NPUB_HRP: Hrp = Hrp::parse_unchecked("npub");
|
||||
|
||||
/// Human-readable part for nsec (NIP-19).
|
||||
const NSEC_HRP: Hrp = Hrp::parse_unchecked("nsec");
|
||||
|
||||
/// Domain separation string for authentication challenges.
|
||||
const AUTH_DOMAIN: &[u8] = b"fips-auth-v1";
|
||||
|
||||
@@ -50,6 +53,15 @@ pub enum IdentityError {
|
||||
|
||||
#[error("invalid npub: expected 32 bytes, got {0}")]
|
||||
InvalidNpubLength(usize),
|
||||
|
||||
#[error("invalid nsec: expected 'nsec' prefix, got '{0}'")]
|
||||
InvalidNsecPrefix(String),
|
||||
|
||||
#[error("invalid nsec: expected 32 bytes, got {0}")]
|
||||
InvalidNsecLength(usize),
|
||||
|
||||
#[error("invalid hex encoding: {0}")]
|
||||
InvalidHex(#[from] hex::FromHexError),
|
||||
}
|
||||
|
||||
/// 32-byte node identifier derived from SHA-256(npub).
|
||||
@@ -296,6 +308,12 @@ impl Identity {
|
||||
Ok(Self::from_secret_key(secret_key))
|
||||
}
|
||||
|
||||
/// Create an identity from an nsec string (bech32) or hex-encoded secret.
|
||||
pub fn from_secret_str(s: &str) -> Result<Self, IdentityError> {
|
||||
let secret_key = decode_secret(s)?;
|
||||
Ok(Self::from_secret_key(secret_key))
|
||||
}
|
||||
|
||||
/// Return the x-only public key.
|
||||
pub fn pubkey(&self) -> XOnlyPublicKey {
|
||||
self.keypair.x_only_public_key().0
|
||||
@@ -440,6 +458,42 @@ pub fn decode_npub(npub: &str) -> Result<XOnlyPublicKey, IdentityError> {
|
||||
Ok(pubkey)
|
||||
}
|
||||
|
||||
/// Encode a secret key as a bech32 nsec string (NIP-19).
|
||||
pub fn encode_nsec(secret_key: &SecretKey) -> String {
|
||||
bech32::encode::<Bech32>(NSEC_HRP, &secret_key.secret_bytes())
|
||||
.expect("nsec encoding cannot fail")
|
||||
}
|
||||
|
||||
/// Decode an nsec string to a secret key.
|
||||
pub fn decode_nsec(nsec: &str) -> Result<SecretKey, IdentityError> {
|
||||
let (hrp, data) = bech32::decode(nsec)?;
|
||||
|
||||
if hrp != NSEC_HRP {
|
||||
return Err(IdentityError::InvalidNsecPrefix(hrp.to_string()));
|
||||
}
|
||||
|
||||
if data.len() != 32 {
|
||||
return Err(IdentityError::InvalidNsecLength(data.len()));
|
||||
}
|
||||
|
||||
let secret_key = SecretKey::from_slice(&data)?;
|
||||
Ok(secret_key)
|
||||
}
|
||||
|
||||
/// Decode a secret key from either nsec (bech32) or hex format.
|
||||
pub fn decode_secret(s: &str) -> Result<SecretKey, IdentityError> {
|
||||
if s.starts_with("nsec1") {
|
||||
decode_nsec(s)
|
||||
} else {
|
||||
let bytes = hex::decode(s)?;
|
||||
if bytes.len() != 32 {
|
||||
return Err(IdentityError::InvalidNsecLength(bytes.len()));
|
||||
}
|
||||
let secret_key = SecretKey::from_slice(&bytes)?;
|
||||
Ok(secret_key)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -708,4 +762,91 @@ mod tests {
|
||||
assert!(display.starts_with("npub1"));
|
||||
assert_eq!(display, identity.npub());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nsec_roundtrip() {
|
||||
let secret_bytes: [u8; 32] = [
|
||||
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
|
||||
0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c,
|
||||
0x1d, 0x1e, 0x1f, 0x20,
|
||||
];
|
||||
|
||||
let secret_key = SecretKey::from_slice(&secret_bytes).unwrap();
|
||||
let nsec = encode_nsec(&secret_key);
|
||||
|
||||
assert!(nsec.starts_with("nsec1"));
|
||||
assert_eq!(nsec.len(), 63);
|
||||
|
||||
let decoded = decode_nsec(&nsec).unwrap();
|
||||
assert_eq!(decoded.secret_bytes(), secret_bytes);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decode_nsec_invalid_prefix() {
|
||||
// Use a valid npub (from a generated identity) to test prefix rejection
|
||||
let identity = Identity::generate();
|
||||
let npub = identity.npub();
|
||||
let result = decode_nsec(&npub);
|
||||
assert!(matches!(result, Err(IdentityError::InvalidNsecPrefix(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decode_secret_nsec() {
|
||||
let secret_bytes: [u8; 32] = [
|
||||
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
|
||||
0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c,
|
||||
0x1d, 0x1e, 0x1f, 0x20,
|
||||
];
|
||||
|
||||
let secret_key = SecretKey::from_slice(&secret_bytes).unwrap();
|
||||
let nsec = encode_nsec(&secret_key);
|
||||
|
||||
let decoded = decode_secret(&nsec).unwrap();
|
||||
assert_eq!(decoded.secret_bytes(), secret_bytes);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decode_secret_hex() {
|
||||
let hex_str = "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20";
|
||||
let decoded = decode_secret(hex_str).unwrap();
|
||||
|
||||
let expected: [u8; 32] = [
|
||||
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
|
||||
0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c,
|
||||
0x1d, 0x1e, 0x1f, 0x20,
|
||||
];
|
||||
assert_eq!(decoded.secret_bytes(), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_identity_from_secret_str_nsec() {
|
||||
let secret_bytes: [u8; 32] = [
|
||||
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
|
||||
0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c,
|
||||
0x1d, 0x1e, 0x1f, 0x20,
|
||||
];
|
||||
|
||||
let secret_key = SecretKey::from_slice(&secret_bytes).unwrap();
|
||||
let nsec = encode_nsec(&secret_key);
|
||||
|
||||
let identity = Identity::from_secret_str(&nsec).unwrap();
|
||||
let identity_from_bytes = Identity::from_secret_bytes(&secret_bytes).unwrap();
|
||||
|
||||
assert_eq!(identity.node_id(), identity_from_bytes.node_id());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_identity_from_secret_str_hex() {
|
||||
let hex_str = "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20";
|
||||
let secret_bytes: [u8; 32] = [
|
||||
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
|
||||
0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c,
|
||||
0x1d, 0x1e, 0x1f, 0x20,
|
||||
];
|
||||
|
||||
let identity = Identity::from_secret_str(hex_str).unwrap();
|
||||
let identity_from_bytes = Identity::from_secret_bytes(&secret_bytes).unwrap();
|
||||
|
||||
assert_eq!(identity.node_id(), identity_from_bytes.node_id());
|
||||
}
|
||||
}
|
||||
|
||||
+4
-2
@@ -3,9 +3,11 @@
|
||||
//! A distributed, decentralized network routing protocol for mesh nodes
|
||||
//! connecting over arbitrary transports.
|
||||
|
||||
pub mod config;
|
||||
pub mod identity;
|
||||
|
||||
pub use config::{Config, ConfigError, IdentityConfig};
|
||||
pub use identity::{
|
||||
decode_npub, encode_npub, AuthChallenge, AuthResponse, FipsAddress, Identity, IdentityError,
|
||||
NodeId, PeerIdentity,
|
||||
decode_npub, decode_nsec, decode_secret, encode_npub, encode_nsec, AuthChallenge, AuthResponse,
|
||||
FipsAddress, Identity, IdentityError, NodeId, PeerIdentity,
|
||||
};
|
||||
|
||||
+49
-92
@@ -1,104 +1,61 @@
|
||||
use fips::{AuthChallenge, Identity, PeerIdentity};
|
||||
use fips::Config;
|
||||
|
||||
fn main() {
|
||||
println!("FIPS Identity Module Demo");
|
||||
println!("=========================\n");
|
||||
println!("FIPS Node Startup");
|
||||
println!("=================\n");
|
||||
|
||||
// Generate a new identity
|
||||
println!("1. Generating a new identity...");
|
||||
let alice = Identity::generate();
|
||||
println!(" npub: {}", alice.npub());
|
||||
println!(" node_id: {}", alice.node_id());
|
||||
println!(" address: {}", alice.address());
|
||||
|
||||
// Create a peer identity from an npub
|
||||
println!("\n2. Creating PeerIdentity from npub...");
|
||||
let alice_peer = PeerIdentity::from_npub(&alice.npub()).unwrap();
|
||||
println!(" Parsed: {}", alice_peer);
|
||||
println!(" Match: {}", alice_peer.node_id() == alice.node_id());
|
||||
|
||||
// Sign and verify data
|
||||
println!("\n3. Signing and verifying data...");
|
||||
let message = b"Hello, FIPS network!";
|
||||
let signature = alice.sign(message);
|
||||
println!(" Message: {:?}", String::from_utf8_lossy(message));
|
||||
println!(" Signed by Alice");
|
||||
|
||||
let valid = alice_peer.verify(message, &signature);
|
||||
println!(" Verified by peer: {}", valid);
|
||||
|
||||
let tampered = alice_peer.verify(b"Tampered message", &signature);
|
||||
println!(" Tampered message: {}", tampered);
|
||||
|
||||
// Authentication challenge-response
|
||||
// This simulates the mutual authentication that occurs when two FIPS nodes
|
||||
// establish a connection. Unlike TLS which binds identity at the transport
|
||||
// layer, FIPS authentication works over any transport (including radio/serial).
|
||||
println!("\n4. Authentication challenge-response...");
|
||||
println!(" Scenario: Alice wants to verify that Bob controls his claimed npub");
|
||||
// Load configuration from standard search paths
|
||||
println!("1. Loading configuration...");
|
||||
println!(" Search paths (in priority order, lowest to highest):");
|
||||
for path in Config::search_paths() {
|
||||
let exists = path.exists();
|
||||
let status = if exists { "[found]" } else { "[not found]" };
|
||||
println!(" {} {}", status, path.display());
|
||||
}
|
||||
println!();
|
||||
|
||||
let bob = Identity::generate();
|
||||
println!(" Bob claims to be: {}", bob.npub());
|
||||
println!(" (Bob's node_id would be: {})", bob.node_id());
|
||||
println!();
|
||||
|
||||
// Step 1: Alice generates a random 32-byte challenge
|
||||
// This nonce ensures Bob can't pre-compute responses
|
||||
let challenge = AuthChallenge::generate();
|
||||
println!(" [Alice] Generated 32-byte random challenge");
|
||||
println!(" Challenge: {:02x}{:02x}{:02x}{:02x}...",
|
||||
challenge.as_bytes()[0], challenge.as_bytes()[1],
|
||||
challenge.as_bytes()[2], challenge.as_bytes()[3]);
|
||||
println!();
|
||||
|
||||
// Step 2: Bob signs the challenge with his private key
|
||||
// The signature covers: SHA256("fips-auth-v1" || challenge || timestamp)
|
||||
// - Domain prefix prevents cross-protocol signature reuse
|
||||
// - Timestamp enables replay attack detection
|
||||
let timestamp = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs();
|
||||
println!(" [Bob] Signing challenge with timestamp {}", timestamp);
|
||||
println!(" Digest = SHA256(\"fips-auth-v1\" || challenge || timestamp)");
|
||||
|
||||
let response = bob.sign_challenge(challenge.as_bytes(), timestamp);
|
||||
println!(" Signature created (64 bytes)");
|
||||
println!();
|
||||
|
||||
// Step 3: Alice verifies the response
|
||||
// If valid, she now knows Bob controls the private key for his claimed npub
|
||||
println!(" [Alice] Verifying Bob's response...");
|
||||
println!(" - Checking signature against claimed npub");
|
||||
println!(" - Checking timestamp is within acceptable window");
|
||||
|
||||
match challenge.verify(&response) {
|
||||
Ok(node_id) => {
|
||||
println!();
|
||||
println!(" [Alice] SUCCESS: Bob proved ownership of his npub");
|
||||
println!(" Verified node_id: {}", node_id);
|
||||
println!(" Bob is now an authenticated peer");
|
||||
}
|
||||
let (config, loaded_paths) = match Config::load() {
|
||||
Ok(result) => result,
|
||||
Err(e) => {
|
||||
println!();
|
||||
println!(" [Alice] FAILED: {}", e);
|
||||
println!(" Connection would be terminated");
|
||||
eprintln!(" Error loading config: {}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
if loaded_paths.is_empty() {
|
||||
println!(" No config files found, using defaults.");
|
||||
} else {
|
||||
println!(" Loaded {} config file(s):", loaded_paths.len());
|
||||
for path in &loaded_paths {
|
||||
println!(" - {}", path.display());
|
||||
}
|
||||
}
|
||||
|
||||
// Deterministic identity from secret
|
||||
println!("\n5. Deterministic identity from secret bytes...");
|
||||
let secret: [u8; 32] = [
|
||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
|
||||
0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d,
|
||||
0x1e, 0x1f,
|
||||
];
|
||||
let fixed = Identity::from_secret_bytes(&secret).unwrap();
|
||||
println!(" npub: {}", fixed.npub());
|
||||
// Create identity from configuration
|
||||
println!("\n2. Initializing identity...");
|
||||
let identity = match config.create_identity() {
|
||||
Ok(id) => id,
|
||||
Err(e) => {
|
||||
eprintln!(" Error creating identity: {}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
let fixed2 = Identity::from_secret_bytes(&secret).unwrap();
|
||||
println!(" Same secret produces same npub: {}", fixed.npub() == fixed2.npub());
|
||||
if config.has_identity() {
|
||||
println!(" Using configured identity.");
|
||||
} else {
|
||||
println!(" No identity configured, generated new keypair.");
|
||||
}
|
||||
|
||||
println!("\nDone.");
|
||||
println!();
|
||||
if let Some(nsec) = &config.identity.nsec {
|
||||
println!(" nsec: {}", nsec);
|
||||
} else {
|
||||
println!(" nsec: (generated)");
|
||||
}
|
||||
println!(" npub: {}", identity.npub());
|
||||
println!(" node_id: {}", identity.node_id());
|
||||
println!(" IPv6 address: {}", identity.address());
|
||||
|
||||
println!("\nReady.");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user