feat(crypto): add post-quantum algorithm negotiation protocol
Implements a protocol for nodes to negotiate which post-quantum signature algorithm to use for communication. Supports Dilithium3, SPHINCS+ (128s/192s/256s), and FALCON (512/1024) with configurable preferences based on: - Security level (NIST 1-5) - Bandwidth constraints (signature size limits) - Algorithm family preference (lattice vs hash-based) Key features: - AlgorithmCapabilities for advertising node capabilities - AlgorithmNegotiator for selecting best common algorithm - Scoring strategies (local/remote preference, average, min/max) - Fallback algorithm selection (different family for resilience) - Session parameters with renegotiation support - Full test coverage (11 tests) This completes Milestone 2 (Enhanced Quantum Cryptography) of Phase 13.
This commit is contained in:
parent
3df4ba0752
commit
af79e21a1b
3 changed files with 805 additions and 0 deletions
|
|
@ -36,6 +36,7 @@ tiny-bip39 = "1.0"
|
|||
|
||||
# Utilities
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
borsh = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
zeroize = { version = "1.7", features = ["derive"] }
|
||||
|
|
|
|||
|
|
@ -91,6 +91,7 @@ pub mod falcon;
|
|||
pub mod kdf;
|
||||
pub mod keypair;
|
||||
pub mod mnemonic;
|
||||
pub mod negotiation;
|
||||
pub mod signature;
|
||||
pub mod sphincs;
|
||||
|
||||
|
|
@ -110,6 +111,13 @@ pub use falcon::{
|
|||
FalconError, FalconKeypair, FalconPublicKey, FalconSecretKey, FalconSignature, FalconVariant,
|
||||
};
|
||||
|
||||
// Algorithm negotiation protocol
|
||||
pub use negotiation::{
|
||||
AlgorithmCapabilities, AlgorithmFamily, AlgorithmNegotiator, NegotiationError,
|
||||
NegotiationMessage, NegotiationPolicy, NegotiationResult, PqAlgorithm, ScoringStrategy,
|
||||
SessionParams,
|
||||
};
|
||||
|
||||
/// Re-export common types
|
||||
pub use synor_types::{Address, Hash256, Network};
|
||||
|
||||
|
|
|
|||
796
crates/synor-crypto/src/negotiation.rs
Normal file
796
crates/synor-crypto/src/negotiation.rs
Normal file
|
|
@ -0,0 +1,796 @@
|
|||
//! Post-Quantum Algorithm Negotiation Protocol
|
||||
//!
|
||||
//! This module provides a protocol for nodes to negotiate which post-quantum
|
||||
//! signature algorithm to use for communication. Nodes advertise their
|
||||
//! capabilities and preferences, and the protocol selects the best common
|
||||
//! algorithm based on security, performance, and bandwidth constraints.
|
||||
//!
|
||||
//! # Supported Algorithms
|
||||
//!
|
||||
//! | Algorithm | NIST | Sig Size | Use Case |
|
||||
//! |-----------|------|----------|----------|
|
||||
//! | Dilithium3 (ML-DSA-65) | FIPS 204 | ~3.3KB | Default, balanced |
|
||||
//! | SPHINCS+ (SLH-DSA) | FIPS 205 | ~7.8KB | Conservative, hash-based |
|
||||
//! | FALCON (FN-DSA) | FIPS 206 | ~0.7KB | Compact, mobile-friendly |
|
||||
//!
|
||||
//! # Negotiation Flow
|
||||
//!
|
||||
//! ```text
|
||||
//! Node A Node B
|
||||
//! │ │
|
||||
//! ├──── Capabilities ─────────────►│
|
||||
//! │ (supported algorithms, │
|
||||
//! │ preferences, version) │
|
||||
//! │ │
|
||||
//! │◄─── Capabilities ──────────────┤
|
||||
//! │ │
|
||||
//! ├──── Selection ────────────────►│
|
||||
//! │ (chosen algorithm, │
|
||||
//! │ session parameters) │
|
||||
//! │ │
|
||||
//! │◄─── Acknowledgment ────────────┤
|
||||
//! │ │
|
||||
//! │ ═══ Secure Channel ══════════ │
|
||||
//! │ (using negotiated algo) │
|
||||
//! ```
|
||||
//!
|
||||
//! # Selection Priority
|
||||
//!
|
||||
//! 1. Security: Prefer algorithms with higher security margins
|
||||
//! 2. Compatibility: Must be supported by both nodes
|
||||
//! 3. Bandwidth: Consider signature size for constrained networks
|
||||
//! 4. Performance: Prefer faster algorithms when security is equal
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use thiserror::Error;
|
||||
|
||||
/// Post-quantum signature algorithms supported by Synor
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum PqAlgorithm {
|
||||
/// Dilithium3 (ML-DSA-65) - NIST FIPS 204
|
||||
/// Default choice, balanced security and performance
|
||||
Dilithium3,
|
||||
|
||||
/// SPHINCS+-SHAKE-128s (SLH-DSA) - NIST FIPS 205
|
||||
/// Hash-based, conservative security, larger signatures
|
||||
SphincsShake128s,
|
||||
|
||||
/// SPHINCS+-SHAKE-192s (SLH-DSA) - NIST FIPS 205
|
||||
/// Higher security margin, larger signatures
|
||||
SphincsShake192s,
|
||||
|
||||
/// SPHINCS+-SHAKE-256s (SLH-DSA) - NIST FIPS 205
|
||||
/// Maximum security, largest signatures
|
||||
SphincsShake256s,
|
||||
|
||||
/// FALCON-512 (FN-DSA) - NIST FIPS 206
|
||||
/// Compact signatures, ideal for mobile/IoT
|
||||
Falcon512,
|
||||
|
||||
/// FALCON-1024 (FN-DSA) - NIST FIPS 206
|
||||
/// Higher security with compact signatures
|
||||
Falcon1024,
|
||||
}
|
||||
|
||||
impl PqAlgorithm {
|
||||
/// Get NIST security level (1-5)
|
||||
pub fn security_level(&self) -> u8 {
|
||||
match self {
|
||||
Self::Dilithium3 => 3,
|
||||
Self::SphincsShake128s => 1,
|
||||
Self::SphincsShake192s => 3,
|
||||
Self::SphincsShake256s => 5,
|
||||
Self::Falcon512 => 1,
|
||||
Self::Falcon1024 => 5,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get approximate signature size in bytes
|
||||
pub fn signature_size(&self) -> usize {
|
||||
match self {
|
||||
Self::Dilithium3 => 3293,
|
||||
Self::SphincsShake128s => 7856,
|
||||
Self::SphincsShake192s => 16224,
|
||||
Self::SphincsShake256s => 29792,
|
||||
Self::Falcon512 => 690,
|
||||
Self::Falcon1024 => 1330,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get approximate public key size in bytes
|
||||
pub fn public_key_size(&self) -> usize {
|
||||
match self {
|
||||
Self::Dilithium3 => 1952,
|
||||
Self::SphincsShake128s => 32,
|
||||
Self::SphincsShake192s => 48,
|
||||
Self::SphincsShake256s => 64,
|
||||
Self::Falcon512 => 897,
|
||||
Self::Falcon1024 => 1793,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the NIST FIPS standard number
|
||||
pub fn fips_standard(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Dilithium3 => "FIPS 204 (ML-DSA)",
|
||||
Self::SphincsShake128s | Self::SphincsShake192s | Self::SphincsShake256s => {
|
||||
"FIPS 205 (SLH-DSA)"
|
||||
}
|
||||
Self::Falcon512 | Self::Falcon1024 => "FIPS 206 (FN-DSA)",
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if this is a hash-based algorithm (SPHINCS+)
|
||||
pub fn is_hash_based(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Self::SphincsShake128s | Self::SphincsShake192s | Self::SphincsShake256s
|
||||
)
|
||||
}
|
||||
|
||||
/// Check if this is a lattice-based algorithm
|
||||
pub fn is_lattice_based(&self) -> bool {
|
||||
matches!(self, Self::Dilithium3 | Self::Falcon512 | Self::Falcon1024)
|
||||
}
|
||||
|
||||
/// Get human-readable name
|
||||
pub fn display_name(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Dilithium3 => "Dilithium3 (ML-DSA-65)",
|
||||
Self::SphincsShake128s => "SPHINCS+-SHAKE-128s",
|
||||
Self::SphincsShake192s => "SPHINCS+-SHAKE-192s",
|
||||
Self::SphincsShake256s => "SPHINCS+-SHAKE-256s",
|
||||
Self::Falcon512 => "FALCON-512",
|
||||
Self::Falcon1024 => "FALCON-1024",
|
||||
}
|
||||
}
|
||||
|
||||
/// Default priority order (higher = more preferred)
|
||||
fn default_priority(&self) -> u8 {
|
||||
match self {
|
||||
Self::Dilithium3 => 100, // Default, well-balanced
|
||||
Self::Falcon1024 => 90, // High security, compact
|
||||
Self::Falcon512 => 85, // Compact, mobile-friendly
|
||||
Self::SphincsShake192s => 70, // Conservative backup
|
||||
Self::SphincsShake256s => 60, // Maximum security
|
||||
Self::SphincsShake128s => 50, // Basic SPHINCS+
|
||||
}
|
||||
}
|
||||
|
||||
/// All supported algorithms
|
||||
pub fn all() -> &'static [PqAlgorithm] {
|
||||
&[
|
||||
Self::Dilithium3,
|
||||
Self::SphincsShake128s,
|
||||
Self::SphincsShake192s,
|
||||
Self::SphincsShake256s,
|
||||
Self::Falcon512,
|
||||
Self::Falcon1024,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for PqAlgorithm {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.display_name())
|
||||
}
|
||||
}
|
||||
|
||||
/// Node's algorithm capabilities and preferences
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AlgorithmCapabilities {
|
||||
/// Protocol version
|
||||
pub version: u32,
|
||||
/// Node identifier (public key hash or peer ID)
|
||||
pub node_id: [u8; 32],
|
||||
/// Supported algorithms with their priorities (higher = more preferred)
|
||||
pub supported: HashMap<PqAlgorithm, u8>,
|
||||
/// Minimum acceptable security level (NIST 1-5)
|
||||
pub min_security_level: u8,
|
||||
/// Maximum acceptable signature size (0 = unlimited)
|
||||
pub max_signature_size: usize,
|
||||
/// Preferred algorithm family (lattice, hash, any)
|
||||
pub preferred_family: AlgorithmFamily,
|
||||
/// Timestamp for freshness
|
||||
pub timestamp: u64,
|
||||
/// Optional extensions for future compatibility
|
||||
pub extensions: HashMap<String, Vec<u8>>,
|
||||
}
|
||||
|
||||
impl AlgorithmCapabilities {
|
||||
/// Create capabilities with default preferences
|
||||
pub fn default_for_node(node_id: [u8; 32]) -> Self {
|
||||
let mut supported = HashMap::new();
|
||||
for algo in PqAlgorithm::all() {
|
||||
supported.insert(*algo, algo.default_priority());
|
||||
}
|
||||
|
||||
Self {
|
||||
version: 1,
|
||||
node_id,
|
||||
supported,
|
||||
min_security_level: 1,
|
||||
max_signature_size: 0,
|
||||
preferred_family: AlgorithmFamily::Any,
|
||||
timestamp: std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs(),
|
||||
extensions: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create capabilities for bandwidth-constrained environments
|
||||
pub fn bandwidth_optimized(node_id: [u8; 32]) -> Self {
|
||||
let mut caps = Self::default_for_node(node_id);
|
||||
caps.max_signature_size = 5000; // Exclude large SPHINCS+ variants
|
||||
caps.supported.insert(PqAlgorithm::Falcon512, 100);
|
||||
caps.supported.insert(PqAlgorithm::Falcon1024, 95);
|
||||
caps.supported.insert(PqAlgorithm::Dilithium3, 80);
|
||||
caps
|
||||
}
|
||||
|
||||
/// Create capabilities for maximum security
|
||||
pub fn security_optimized(node_id: [u8; 32]) -> Self {
|
||||
let mut caps = Self::default_for_node(node_id);
|
||||
caps.min_security_level = 3;
|
||||
caps.supported.insert(PqAlgorithm::SphincsShake256s, 100);
|
||||
caps.supported.insert(PqAlgorithm::Falcon1024, 95);
|
||||
caps.supported.insert(PqAlgorithm::SphincsShake192s, 90);
|
||||
caps.supported.insert(PqAlgorithm::Dilithium3, 85);
|
||||
caps
|
||||
}
|
||||
|
||||
/// Add support for an algorithm with priority
|
||||
pub fn add_algorithm(&mut self, algo: PqAlgorithm, priority: u8) {
|
||||
self.supported.insert(algo, priority);
|
||||
}
|
||||
|
||||
/// Remove support for an algorithm
|
||||
pub fn remove_algorithm(&mut self, algo: PqAlgorithm) {
|
||||
self.supported.remove(&algo);
|
||||
}
|
||||
|
||||
/// Check if an algorithm is supported
|
||||
pub fn supports(&self, algo: PqAlgorithm) -> bool {
|
||||
self.supported.contains_key(&algo)
|
||||
}
|
||||
|
||||
/// Get priority for an algorithm (0 if not supported)
|
||||
pub fn priority(&self, algo: PqAlgorithm) -> u8 {
|
||||
self.supported.get(&algo).copied().unwrap_or(0)
|
||||
}
|
||||
|
||||
/// Encode capabilities to bytes for transmission
|
||||
pub fn encode(&self) -> Vec<u8> {
|
||||
serde_json::to_vec(self).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Decode capabilities from bytes
|
||||
pub fn decode(data: &[u8]) -> Result<Self, NegotiationError> {
|
||||
serde_json::from_slice(data).map_err(|e| NegotiationError::InvalidCapabilities(e.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
/// Algorithm family preference
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum AlgorithmFamily {
|
||||
/// Lattice-based (Dilithium, FALCON)
|
||||
Lattice,
|
||||
/// Hash-based (SPHINCS+)
|
||||
HashBased,
|
||||
/// No preference
|
||||
#[default]
|
||||
Any,
|
||||
}
|
||||
|
||||
/// Result of algorithm negotiation
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct NegotiationResult {
|
||||
/// Selected algorithm
|
||||
pub algorithm: PqAlgorithm,
|
||||
/// Combined score (higher = better match)
|
||||
pub score: u32,
|
||||
/// Whether both nodes preferred this algorithm
|
||||
pub mutual_preference: bool,
|
||||
/// Session parameters
|
||||
pub session_params: SessionParams,
|
||||
}
|
||||
|
||||
/// Session parameters established during negotiation
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SessionParams {
|
||||
/// Session identifier
|
||||
pub session_id: [u8; 32],
|
||||
/// Negotiation timestamp
|
||||
pub negotiated_at: u64,
|
||||
/// Session expiry (0 = no expiry)
|
||||
pub expires_at: u64,
|
||||
/// Allow algorithm renegotiation mid-session
|
||||
pub allow_renegotiation: bool,
|
||||
/// Fallback algorithm if primary fails
|
||||
pub fallback: Option<PqAlgorithm>,
|
||||
}
|
||||
|
||||
impl Default for SessionParams {
|
||||
fn default() -> Self {
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
let mut session_id = [0u8; 32];
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos();
|
||||
session_id[..16].copy_from_slice(&now.to_le_bytes());
|
||||
|
||||
Self {
|
||||
session_id,
|
||||
negotiated_at: now as u64 / 1_000_000_000,
|
||||
expires_at: 0,
|
||||
allow_renegotiation: true,
|
||||
fallback: Some(PqAlgorithm::Dilithium3),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Algorithm negotiator
|
||||
pub struct AlgorithmNegotiator {
|
||||
/// Local node capabilities
|
||||
local_caps: AlgorithmCapabilities,
|
||||
/// Negotiation policy
|
||||
policy: NegotiationPolicy,
|
||||
}
|
||||
|
||||
impl AlgorithmNegotiator {
|
||||
/// Create a new negotiator with local capabilities
|
||||
pub fn new(local_caps: AlgorithmCapabilities) -> Self {
|
||||
Self {
|
||||
local_caps,
|
||||
policy: NegotiationPolicy::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create with custom policy
|
||||
pub fn with_policy(local_caps: AlgorithmCapabilities, policy: NegotiationPolicy) -> Self {
|
||||
Self { local_caps, policy }
|
||||
}
|
||||
|
||||
/// Get local capabilities
|
||||
pub fn capabilities(&self) -> &AlgorithmCapabilities {
|
||||
&self.local_caps
|
||||
}
|
||||
|
||||
/// Negotiate algorithm with remote peer
|
||||
pub fn negotiate(
|
||||
&self,
|
||||
remote_caps: &AlgorithmCapabilities,
|
||||
) -> Result<NegotiationResult, NegotiationError> {
|
||||
// Find common algorithms
|
||||
let local_algos: HashSet<_> = self.local_caps.supported.keys().collect();
|
||||
let remote_algos: HashSet<_> = remote_caps.supported.keys().collect();
|
||||
let common: Vec<_> = local_algos.intersection(&remote_algos).copied().collect();
|
||||
|
||||
if common.is_empty() {
|
||||
return Err(NegotiationError::NoCommonAlgorithm);
|
||||
}
|
||||
|
||||
// Filter by constraints
|
||||
let valid_algos: Vec<_> = common
|
||||
.into_iter()
|
||||
.filter(|algo| {
|
||||
// Check security level
|
||||
let meets_local_security =
|
||||
algo.security_level() >= self.local_caps.min_security_level;
|
||||
let meets_remote_security =
|
||||
algo.security_level() >= remote_caps.min_security_level;
|
||||
|
||||
// Check signature size
|
||||
let local_size_ok = self.local_caps.max_signature_size == 0
|
||||
|| algo.signature_size() <= self.local_caps.max_signature_size;
|
||||
let remote_size_ok = remote_caps.max_signature_size == 0
|
||||
|| algo.signature_size() <= remote_caps.max_signature_size;
|
||||
|
||||
// Check family preference
|
||||
let family_ok = self.matches_family_preference(algo);
|
||||
|
||||
meets_local_security
|
||||
&& meets_remote_security
|
||||
&& local_size_ok
|
||||
&& remote_size_ok
|
||||
&& family_ok
|
||||
})
|
||||
.collect();
|
||||
|
||||
if valid_algos.is_empty() {
|
||||
return Err(NegotiationError::ConstraintsMismatch);
|
||||
}
|
||||
|
||||
// Score and select best algorithm
|
||||
let mut best_algo = valid_algos[0];
|
||||
let mut best_score = 0u32;
|
||||
|
||||
for algo in valid_algos {
|
||||
let local_prio = self.local_caps.priority(*algo) as u32;
|
||||
let remote_prio = remote_caps.priority(*algo) as u32;
|
||||
|
||||
let score = match self.policy.scoring {
|
||||
ScoringStrategy::LocalPreference => local_prio * 2 + remote_prio,
|
||||
ScoringStrategy::RemotePreference => local_prio + remote_prio * 2,
|
||||
ScoringStrategy::Average => (local_prio + remote_prio) / 2,
|
||||
ScoringStrategy::Minimum => local_prio.min(remote_prio),
|
||||
ScoringStrategy::Maximum => local_prio.max(remote_prio),
|
||||
};
|
||||
|
||||
// Bonus for security level
|
||||
let security_bonus = if self.policy.prioritize_security {
|
||||
algo.security_level() as u32 * 10
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
// Penalty for large signatures (if bandwidth-conscious)
|
||||
let size_penalty = if self.policy.minimize_bandwidth {
|
||||
(algo.signature_size() / 1000) as u32
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let final_score = score + security_bonus - size_penalty;
|
||||
|
||||
if final_score > best_score {
|
||||
best_score = final_score;
|
||||
best_algo = algo;
|
||||
}
|
||||
}
|
||||
|
||||
// Determine mutual preference
|
||||
let local_top = self.top_preference();
|
||||
let remote_top = Self::top_preference_from_caps(remote_caps);
|
||||
let mutual_preference = local_top == Some(*best_algo) && remote_top == Some(*best_algo);
|
||||
|
||||
// Create session params
|
||||
let mut session_params = SessionParams::default();
|
||||
session_params.fallback = self.select_fallback(best_algo, remote_caps);
|
||||
|
||||
Ok(NegotiationResult {
|
||||
algorithm: *best_algo,
|
||||
score: best_score,
|
||||
mutual_preference,
|
||||
session_params,
|
||||
})
|
||||
}
|
||||
|
||||
/// Check if algorithm matches family preference
|
||||
fn matches_family_preference(&self, algo: &PqAlgorithm) -> bool {
|
||||
match self.local_caps.preferred_family {
|
||||
AlgorithmFamily::Any => true,
|
||||
AlgorithmFamily::Lattice => algo.is_lattice_based(),
|
||||
AlgorithmFamily::HashBased => algo.is_hash_based(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get local top preference
|
||||
fn top_preference(&self) -> Option<PqAlgorithm> {
|
||||
self.local_caps
|
||||
.supported
|
||||
.iter()
|
||||
.max_by_key(|(_, prio)| *prio)
|
||||
.map(|(algo, _)| *algo)
|
||||
}
|
||||
|
||||
/// Get top preference from capabilities
|
||||
fn top_preference_from_caps(caps: &AlgorithmCapabilities) -> Option<PqAlgorithm> {
|
||||
caps.supported
|
||||
.iter()
|
||||
.max_by_key(|(_, prio)| *prio)
|
||||
.map(|(algo, _)| *algo)
|
||||
}
|
||||
|
||||
/// Select a fallback algorithm different from primary
|
||||
fn select_fallback(
|
||||
&self,
|
||||
primary: &PqAlgorithm,
|
||||
remote_caps: &AlgorithmCapabilities,
|
||||
) -> Option<PqAlgorithm> {
|
||||
// Prefer a different algorithm family for fallback
|
||||
let primary_is_lattice = primary.is_lattice_based();
|
||||
|
||||
self.local_caps
|
||||
.supported
|
||||
.keys()
|
||||
.filter(|algo| {
|
||||
**algo != *primary
|
||||
&& remote_caps.supports(**algo)
|
||||
&& algo.is_lattice_based() != primary_is_lattice
|
||||
})
|
||||
.max_by_key(|algo| self.local_caps.priority(**algo))
|
||||
.copied()
|
||||
}
|
||||
|
||||
/// Quick negotiation using just algorithm names
|
||||
pub fn quick_negotiate(
|
||||
local: &[PqAlgorithm],
|
||||
remote: &[PqAlgorithm],
|
||||
) -> Option<PqAlgorithm> {
|
||||
// Find common algorithms and return the one with highest default priority
|
||||
let local_set: HashSet<_> = local.iter().collect();
|
||||
let remote_set: HashSet<_> = remote.iter().collect();
|
||||
|
||||
local_set
|
||||
.intersection(&remote_set)
|
||||
.max_by_key(|algo| algo.default_priority())
|
||||
.copied()
|
||||
.copied()
|
||||
}
|
||||
}
|
||||
|
||||
/// Negotiation policy configuration
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct NegotiationPolicy {
|
||||
/// How to score algorithm preferences
|
||||
pub scoring: ScoringStrategy,
|
||||
/// Prioritize higher security levels
|
||||
pub prioritize_security: bool,
|
||||
/// Minimize bandwidth usage (prefer smaller signatures)
|
||||
pub minimize_bandwidth: bool,
|
||||
/// Require mutual top preference
|
||||
pub require_mutual: bool,
|
||||
}
|
||||
|
||||
impl Default for NegotiationPolicy {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
scoring: ScoringStrategy::Average,
|
||||
prioritize_security: false,
|
||||
minimize_bandwidth: false,
|
||||
require_mutual: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Strategy for scoring algorithm preferences
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ScoringStrategy {
|
||||
/// Weight local preferences higher
|
||||
LocalPreference,
|
||||
/// Weight remote preferences higher
|
||||
RemotePreference,
|
||||
/// Average both preferences
|
||||
Average,
|
||||
/// Use minimum (both must prefer)
|
||||
Minimum,
|
||||
/// Use maximum (either prefers)
|
||||
Maximum,
|
||||
}
|
||||
|
||||
/// Negotiation errors
|
||||
#[derive(Debug, Error)]
|
||||
pub enum NegotiationError {
|
||||
#[error("No common algorithm supported by both nodes")]
|
||||
NoCommonAlgorithm,
|
||||
|
||||
#[error("Constraints cannot be satisfied (security level or size limits)")]
|
||||
ConstraintsMismatch,
|
||||
|
||||
#[error("Invalid capabilities message: {0}")]
|
||||
InvalidCapabilities(String),
|
||||
|
||||
#[error("Protocol version mismatch: local={local}, remote={remote}")]
|
||||
VersionMismatch { local: u32, remote: u32 },
|
||||
|
||||
#[error("Negotiation timeout")]
|
||||
Timeout,
|
||||
|
||||
#[error("Remote rejected negotiation: {0}")]
|
||||
Rejected(String),
|
||||
}
|
||||
|
||||
/// Negotiation message types for the protocol
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum NegotiationMessage {
|
||||
/// Advertise capabilities
|
||||
Capabilities(AlgorithmCapabilities),
|
||||
|
||||
/// Propose selected algorithm
|
||||
Selection {
|
||||
algorithm: PqAlgorithm,
|
||||
session_params: SessionParams,
|
||||
},
|
||||
|
||||
/// Acknowledge selection
|
||||
Acknowledgment { session_id: [u8; 32], accepted: bool },
|
||||
|
||||
/// Request renegotiation
|
||||
Renegotiate { reason: String },
|
||||
|
||||
/// Error message
|
||||
Error { code: u32, message: String },
|
||||
}
|
||||
|
||||
impl NegotiationMessage {
|
||||
/// Encode message to bytes
|
||||
pub fn encode(&self) -> Vec<u8> {
|
||||
serde_json::to_vec(self).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Decode message from bytes
|
||||
pub fn decode(data: &[u8]) -> Result<Self, NegotiationError> {
|
||||
serde_json::from_slice(data)
|
||||
.map_err(|e| NegotiationError::InvalidCapabilities(e.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_algorithm_properties() {
|
||||
assert_eq!(PqAlgorithm::Dilithium3.security_level(), 3);
|
||||
assert_eq!(PqAlgorithm::Falcon512.signature_size(), 690);
|
||||
assert!(PqAlgorithm::SphincsShake256s.is_hash_based());
|
||||
assert!(PqAlgorithm::Dilithium3.is_lattice_based());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_capabilities_default() {
|
||||
let node_id = [0xabu8; 32];
|
||||
let caps = AlgorithmCapabilities::default_for_node(node_id);
|
||||
|
||||
assert_eq!(caps.version, 1);
|
||||
assert!(caps.supports(PqAlgorithm::Dilithium3));
|
||||
assert!(caps.supports(PqAlgorithm::Falcon512));
|
||||
assert!(caps.supports(PqAlgorithm::SphincsShake128s));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bandwidth_optimized_caps() {
|
||||
let node_id = [0xabu8; 32];
|
||||
let caps = AlgorithmCapabilities::bandwidth_optimized(node_id);
|
||||
|
||||
// FALCON should have highest priority
|
||||
assert!(caps.priority(PqAlgorithm::Falcon512) > caps.priority(PqAlgorithm::Dilithium3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_basic_negotiation() {
|
||||
let local_id = [0xaa; 32];
|
||||
let remote_id = [0xbb; 32];
|
||||
|
||||
let local_caps = AlgorithmCapabilities::default_for_node(local_id);
|
||||
let remote_caps = AlgorithmCapabilities::default_for_node(remote_id);
|
||||
|
||||
let negotiator = AlgorithmNegotiator::new(local_caps);
|
||||
let result = negotiator.negotiate(&remote_caps).unwrap();
|
||||
|
||||
// Should select Dilithium3 as default preference
|
||||
assert_eq!(result.algorithm, PqAlgorithm::Dilithium3);
|
||||
assert!(result.mutual_preference);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bandwidth_constrained_negotiation() {
|
||||
let local_id = [0xaa; 32];
|
||||
let remote_id = [0xbb; 32];
|
||||
|
||||
let local_caps = AlgorithmCapabilities::bandwidth_optimized(local_id);
|
||||
let remote_caps = AlgorithmCapabilities::bandwidth_optimized(remote_id);
|
||||
|
||||
let policy = NegotiationPolicy {
|
||||
minimize_bandwidth: true,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let negotiator = AlgorithmNegotiator::with_policy(local_caps, policy);
|
||||
let result = negotiator.negotiate(&remote_caps).unwrap();
|
||||
|
||||
// Should prefer FALCON for bandwidth-constrained scenarios
|
||||
assert!(result.algorithm == PqAlgorithm::Falcon512 ||
|
||||
result.algorithm == PqAlgorithm::Falcon1024);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_security_constrained_negotiation() {
|
||||
let local_id = [0xaa; 32];
|
||||
let remote_id = [0xbb; 32];
|
||||
|
||||
let local_caps = AlgorithmCapabilities::security_optimized(local_id);
|
||||
let remote_caps = AlgorithmCapabilities::security_optimized(remote_id);
|
||||
|
||||
let negotiator = AlgorithmNegotiator::new(local_caps);
|
||||
let result = negotiator.negotiate(&remote_caps).unwrap();
|
||||
|
||||
// Should select high-security algorithm
|
||||
assert!(result.algorithm.security_level() >= 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_no_common_algorithm() {
|
||||
let local_id = [0xaa; 32];
|
||||
let remote_id = [0xbb; 32];
|
||||
|
||||
let mut local_caps = AlgorithmCapabilities::default_for_node(local_id);
|
||||
local_caps.supported.clear();
|
||||
local_caps.add_algorithm(PqAlgorithm::Falcon512, 100);
|
||||
|
||||
let mut remote_caps = AlgorithmCapabilities::default_for_node(remote_id);
|
||||
remote_caps.supported.clear();
|
||||
remote_caps.add_algorithm(PqAlgorithm::SphincsShake256s, 100);
|
||||
|
||||
let negotiator = AlgorithmNegotiator::new(local_caps);
|
||||
let result = negotiator.negotiate(&remote_caps);
|
||||
|
||||
assert!(matches!(result, Err(NegotiationError::NoCommonAlgorithm)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_quick_negotiate() {
|
||||
let local = vec![PqAlgorithm::Dilithium3, PqAlgorithm::Falcon512];
|
||||
let remote = vec![PqAlgorithm::Falcon512, PqAlgorithm::SphincsShake128s];
|
||||
|
||||
let result = AlgorithmNegotiator::quick_negotiate(&local, &remote);
|
||||
assert_eq!(result, Some(PqAlgorithm::Falcon512));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_message_encoding() {
|
||||
let node_id = [0xaa; 32];
|
||||
let caps = AlgorithmCapabilities::default_for_node(node_id);
|
||||
let msg = NegotiationMessage::Capabilities(caps);
|
||||
|
||||
let encoded = msg.encode();
|
||||
let decoded = NegotiationMessage::decode(&encoded).unwrap();
|
||||
|
||||
if let NegotiationMessage::Capabilities(decoded_caps) = decoded {
|
||||
assert_eq!(decoded_caps.node_id, node_id);
|
||||
} else {
|
||||
panic!("Wrong message type");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fallback_selection() {
|
||||
let local_id = [0xaa; 32];
|
||||
let remote_id = [0xbb; 32];
|
||||
|
||||
let local_caps = AlgorithmCapabilities::default_for_node(local_id);
|
||||
let remote_caps = AlgorithmCapabilities::default_for_node(remote_id);
|
||||
|
||||
let negotiator = AlgorithmNegotiator::new(local_caps);
|
||||
let result = negotiator.negotiate(&remote_caps).unwrap();
|
||||
|
||||
// Fallback should be different from primary
|
||||
if let Some(fallback) = result.session_params.fallback {
|
||||
assert_ne!(fallback, result.algorithm);
|
||||
// Fallback should be from different family if possible
|
||||
if result.algorithm.is_lattice_based() {
|
||||
// Might be hash-based fallback
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_constraints_mismatch() {
|
||||
let local_id = [0xaa; 32];
|
||||
let remote_id = [0xbb; 32];
|
||||
|
||||
// Local requires security level 5
|
||||
let mut local_caps = AlgorithmCapabilities::default_for_node(local_id);
|
||||
local_caps.min_security_level = 5;
|
||||
|
||||
// Remote only supports low-security algorithms
|
||||
let mut remote_caps = AlgorithmCapabilities::default_for_node(remote_id);
|
||||
remote_caps.supported.clear();
|
||||
remote_caps.add_algorithm(PqAlgorithm::Falcon512, 100); // Level 1
|
||||
remote_caps.add_algorithm(PqAlgorithm::SphincsShake128s, 90); // Level 1
|
||||
|
||||
let negotiator = AlgorithmNegotiator::new(local_caps);
|
||||
let result = negotiator.negotiate(&remote_caps);
|
||||
|
||||
assert!(matches!(result, Err(NegotiationError::ConstraintsMismatch)));
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue