800 lines
26 KiB
Rust
800 lines
26 KiB
Rust
//! 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 session_params = SessionParams {
|
|
fallback: self.select_fallback(best_algo, remote_caps),
|
|
..Default::default()
|
|
};
|
|
|
|
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)));
|
|
}
|
|
}
|