Skip to content

Commit b6f7e8b

Browse files
committed
feat(crypto): add Ed25519 point and X25519 key validation functions
Adds two new validation functions to the library: 1. crypto_core_ed25519_is_valid_point() in crypto_core.rs: - Implements strict Ed25519 point validation with explicit checks: * Rejects non-canonical encodings (high bit in last byte set) * Rejects all-zero representation * Rejects the identity element ([1,0,...,0]) - Uses curve25519-dalek for additional curve equation checking - Includes comprehensive documentation and test coverage - Note: Stricter than libsodium's validation for security reasons 2. KeyPair::is_valid_public_key() in keypair.rs: - Provides X25519 public key validation for crypto_box usage - Implements appropriate checks for X25519 context: * Rejects all-zero representation * Verifies high bit of last byte is clear - Clearly documents that this does not check if a point lies on the curve - Includes test coverage for valid/invalid key scenarios These new functions allow users to validate cryptographic points/keys before using them in operations, helping prevent potential security issues with invalid or malicious inputs.
1 parent 1b51d16 commit b6f7e8b

4 files changed

Lines changed: 418 additions & 2 deletions

File tree

src/classic/crypto_core.rs

Lines changed: 246 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
use curve25519_dalek::edwards::CompressedEdwardsY;
2+
13
use crate::constants::{
2-
CRYPTO_CORE_HCHACHA20_INPUTBYTES, CRYPTO_CORE_HCHACHA20_KEYBYTES,
4+
CRYPTO_CORE_ED25519_BYTES, CRYPTO_CORE_HCHACHA20_INPUTBYTES, CRYPTO_CORE_HCHACHA20_KEYBYTES,
35
CRYPTO_CORE_HCHACHA20_OUTPUTBYTES, CRYPTO_CORE_HSALSA20_INPUTBYTES,
46
CRYPTO_CORE_HSALSA20_KEYBYTES, CRYPTO_CORE_HSALSA20_OUTPUTBYTES, CRYPTO_SCALARMULT_BYTES,
57
CRYPTO_SCALARMULT_SCALARBYTES,
@@ -22,6 +24,8 @@ pub type HSalsa20Input = [u8; CRYPTO_CORE_HSALSA20_INPUTBYTES];
2224
pub type HSalsa20Key = [u8; CRYPTO_CORE_HSALSA20_KEYBYTES];
2325
/// Stack-allocated HSalsa20 output.
2426
pub type HSalsa20Output = [u8; CRYPTO_CORE_HSALSA20_OUTPUTBYTES];
27+
/// Stack-allocated Ed25519 point.
28+
pub type Ed25519Point = [u8; CRYPTO_CORE_ED25519_BYTES];
2529

2630
/// Computes the public key for a previously generated secret key.
2731
///
@@ -123,6 +127,100 @@ pub fn crypto_core_hchacha20(
123127
output[28..32].copy_from_slice(&x15.to_le_bytes());
124128
}
125129

130+
/// Checks if a given point is on the Ed25519 curve.
131+
///
132+
/// This function determines if a given point is a valid point on the Ed25519
133+
/// curve that can be safely used for cryptographic operations.
134+
///
135+
/// # Security Note
136+
///
137+
/// This implementation uses `curve25519-dalek` for validation and is stricter
138+
/// than libsodium's `crypto_core_ed25519_is_valid_point`. Specifically, it may
139+
/// reject certain points, such as small-order points (e.g., the point
140+
/// represented by `[1, 0, ..., 0]`), which libsodium might accept. While
141+
/// libsodium's behavior provides compatibility, using points rejected by this
142+
/// function can lead to security vulnerabilities in certain protocols. Relying
143+
/// on this stricter check is generally recommended for new applications.
144+
///
145+
/// By default, this function enforces canonical encoding by requiring the high
146+
/// bit of the last byte to be 0. If you're working with Ed25519 keys generated
147+
/// by [`crypto_sign_keypair`](`crate::classic::crypto_sign::crypto_sign_keypair`)
148+
/// that might have the high bit set, you should use
149+
/// [`crypto_core_ed25519_is_valid_point_relaxed`] instead.
150+
///
151+
/// # Example
152+
///
153+
/// ```
154+
/// use dryoc::classic::crypto_core::{
155+
/// Ed25519Point, crypto_core_ed25519_is_valid_point,
156+
/// crypto_core_ed25519_is_valid_point_relaxed,
157+
/// };
158+
/// use dryoc::classic::crypto_sign::crypto_sign_keypair;
159+
///
160+
/// // Get a valid Ed25519 public key (valid point)
161+
/// let (pk, _) = crypto_sign_keypair();
162+
///
163+
/// // For keys from crypto_sign_keypair(), use the relaxed validation
164+
/// // as they may have the high bit set
165+
/// assert!(crypto_core_ed25519_is_valid_point_relaxed(&pk));
166+
///
167+
/// // Strict validation for a manually constructed point
168+
/// let mut invalid_point = [0u8; 32];
169+
/// invalid_point[31] = 0x80; // Set high bit, making it invalid
170+
/// assert!(!crypto_core_ed25519_is_valid_point(&invalid_point));
171+
/// ```
172+
///
173+
/// Not fully compatible with libsodium's `crypto_core_ed25519_is_valid_point`
174+
/// due to stricter checks.
175+
pub fn crypto_core_ed25519_is_valid_point(p: &Ed25519Point) -> bool {
176+
crypto_core_ed25519_is_valid_point_internal(p, false)
177+
}
178+
179+
/// Version of [`crypto_core_ed25519_is_valid_point`] that optionally ignores
180+
/// the high bit check.
181+
///
182+
/// This is particularly useful when validating Ed25519 public keys generated by
183+
/// [`crypto_sign_keypair`](`crate::classic::crypto_sign::crypto_sign_keypair`),
184+
/// which may have the high bit set.
185+
pub fn crypto_core_ed25519_is_valid_point_relaxed(p: &Ed25519Point) -> bool {
186+
crypto_core_ed25519_is_valid_point_internal(p, true)
187+
}
188+
189+
/// Internal implementation for point validation that can optionally ignore the
190+
/// high bit check.
191+
fn crypto_core_ed25519_is_valid_point_internal(p: &Ed25519Point, ignore_high_bit: bool) -> bool {
192+
// Check 1: Canonical encoding. The high bit of the last byte must be 0, unless
193+
// ignore_high_bit is true.
194+
if !ignore_high_bit && p[CRYPTO_CORE_ED25519_BYTES - 1] & 0x80 != 0 {
195+
return false;
196+
}
197+
198+
// Check 2: Reject the all-zero point, which is invalid.
199+
const ZERO_POINT: Ed25519Point = [0u8; CRYPTO_CORE_ED25519_BYTES];
200+
if p == &ZERO_POINT {
201+
return false;
202+
}
203+
204+
// Check 3: Reject the identity element ([1, 0, ..., 0]) which is a small-order
205+
// point.
206+
const SMALL_ORDER_POINT_IDENTITY: Ed25519Point = [
207+
1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
208+
0, 0,
209+
];
210+
if p == &SMALL_ORDER_POINT_IDENTITY {
211+
return false;
212+
}
213+
214+
// Check 4: Use curve25519-dalek decompression for point-on-curve check.
215+
// This will also reject points not in the prime-order subgroup if the feature
216+
// `serde` is not enabled for curve25519-dalek, but we explicitly checked
217+
// identity.
218+
match CompressedEdwardsY::from_slice(p) {
219+
Ok(compressed) => compressed.decompress().is_some(),
220+
Err(_) => false, // Should not happen if length is correct, but handle defensively.
221+
}
222+
}
223+
126224
#[inline]
127225
fn salsa20_rotl32(x: u32, y: u32, rot: u32) -> u32 {
128226
x.wrapping_add(y).rotate_left(rot)
@@ -216,6 +314,8 @@ pub fn crypto_core_hsalsa20(
216314
mod tests {
217315
use super::*;
218316
use crate::classic::crypto_box::*;
317+
use crate::classic::crypto_sign::crypto_sign_keypair;
318+
use crate::keypair::{KeyPair, PublicKey, SecretKey};
219319

220320
#[test]
221321
fn test_crypto_scalarmult_base() {
@@ -333,4 +433,149 @@ mod tests {
333433
);
334434
}
335435
}
436+
437+
#[test]
438+
fn test_crypto_core_ed25519_is_valid_point() {
439+
// Test with a known valid public key (from one of the crypto_sign test vectors)
440+
// This point is on the curve and correctly encoded.
441+
let valid_pk = [
442+
215, 90, 152, 1, 130, 177, 10, 183, 213, 75, 254, 211, 201, 100, 7, 58, 14, 225, 114,
443+
243, 218, 166, 35, 37, 175, 2, 26, 104, 247, 7, 81, 26,
444+
];
445+
assert!(
446+
crypto_core_ed25519_is_valid_point(&valid_pk),
447+
"Known valid Ed25519 public key should be considered valid"
448+
);
449+
450+
// Test a point with the high bit set (invalid compressed format)
451+
// Standard Ed25519 compression requires the high bit of the last byte to be 0.
452+
let mut invalid_point_high_bit = [0u8; CRYPTO_CORE_ED25519_BYTES];
453+
invalid_point_high_bit[31] = 0x80; // Set high bit, making it invalid
454+
assert!(
455+
!crypto_core_ed25519_is_valid_point(&invalid_point_high_bit),
456+
"Point with high bit set in last byte should be invalid"
457+
);
458+
459+
// Test the identity element (0, 1), which is a valid point.
460+
// Its compressed form is [1, 0, ..., 0].
461+
// While mathematically valid, this is a small-order point that can cause
462+
// security issues in certain cryptographic protocols, such as enabling
463+
// invalid curve attacks. Stricter implementations (like curve25519-dalek)
464+
// reject small-order points for this reason, whereas Libsodium accepts them.
465+
let small_order_point_identity = [
466+
1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
467+
0, 0, 0,
468+
];
469+
assert!(
470+
!crypto_core_ed25519_is_valid_point(&small_order_point_identity),
471+
"Small-order point (identity element) should be rejected by stricter validation"
472+
);
473+
474+
// Test a point that is not on the curve (but is canonically encoded)
475+
// Example: A point generated randomly is unlikely to be on the curve.
476+
// We expect this to be rejected by the decompression check.
477+
let mut point_not_on_curve = [0u8; CRYPTO_CORE_ED25519_BYTES];
478+
// Fill with some non-zero value that's unlikely to form a valid point
479+
// but is canonically encoded (last byte < 128)
480+
point_not_on_curve[0] = 2; // Example modification
481+
assert!(
482+
!crypto_core_ed25519_is_valid_point(&point_not_on_curve),
483+
"Point not on the curve should be invalid"
484+
);
485+
486+
// Test the zero point [0, ..., 0], which is invalid encoding.
487+
let zero_point = [0u8; CRYPTO_CORE_ED25519_BYTES];
488+
assert!(
489+
!crypto_core_ed25519_is_valid_point(&zero_point),
490+
"Zero point represents invalid encoding"
491+
);
492+
}
493+
494+
#[test]
495+
fn test_keypair_on_curve() {
496+
// Run multiple attempts to ensure we catch any potential issues
497+
let iterations = 25;
498+
let mut strict_failures = 0;
499+
let mut relaxed_failures = 0;
500+
501+
println!(
502+
"\n=== Testing Ed25519 key validation across {} iterations ===",
503+
iterations
504+
);
505+
506+
for i in 0..iterations {
507+
// Generate an Ed25519 keypair
508+
let (ed25519_pk, _) = crypto_sign_keypair();
509+
510+
// Check with strict validation (may fail due to high bit)
511+
let strict_valid = crypto_core_ed25519_is_valid_point(&ed25519_pk);
512+
513+
// Check with relaxed validation (should always pass for generated keys)
514+
let relaxed_valid = crypto_core_ed25519_is_valid_point_relaxed(&ed25519_pk);
515+
516+
if !strict_valid {
517+
strict_failures += 1;
518+
// Only check the reason when strict validation fails
519+
let high_bit_set = ed25519_pk[CRYPTO_CORE_ED25519_BYTES - 1] & 0x80 != 0;
520+
println!("Iteration {}: Ed25519 key strict validation failed:", i);
521+
println!(" High bit set: {}", high_bit_set);
522+
}
523+
524+
if !relaxed_valid {
525+
relaxed_failures += 1;
526+
// This shouldn't happen for properly generated keys
527+
println!(
528+
"ERROR: Iteration {}: Ed25519 key failed relaxed validation",
529+
i
530+
);
531+
532+
// Check all conditions to see why it failed
533+
const ZERO_POINT: Ed25519Point = [0u8; CRYPTO_CORE_ED25519_BYTES];
534+
let is_zero = ed25519_pk == ZERO_POINT;
535+
536+
const SMALL_ORDER_POINT_IDENTITY: Ed25519Point = [
537+
1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
538+
0, 0, 0, 0, 0, 0,
539+
];
540+
let is_identity = ed25519_pk == SMALL_ORDER_POINT_IDENTITY;
541+
542+
let on_curve = match CompressedEdwardsY::from_slice(&ed25519_pk) {
543+
Ok(compressed) => compressed.decompress().is_some(),
544+
Err(_) => false,
545+
};
546+
547+
println!(" Zero point: {}", is_zero);
548+
println!(" Identity element: {}", is_identity);
549+
println!(" On curve: {}", on_curve);
550+
println!(" Key: {:?}", ed25519_pk);
551+
}
552+
553+
// We should always be able to verify keys with relaxed validation
554+
assert!(
555+
relaxed_valid,
556+
"Generated Ed25519 key failed relaxed validation"
557+
);
558+
}
559+
560+
println!(
561+
"\nSummary: {} of {} Ed25519 keys failed strict validation",
562+
strict_failures, iterations
563+
);
564+
println!(
565+
"Summary: {} of {} Ed25519 keys failed relaxed validation",
566+
relaxed_failures, iterations
567+
);
568+
569+
// X25519 keys should be valid with standard validation (they're generated
570+
// clamped)
571+
println!("\n=== Testing X25519 key validation ===");
572+
let (x25519_pk, _) = crypto_box_keypair();
573+
574+
assert!(
575+
KeyPair::<PublicKey, SecretKey>::is_valid_public_key(&x25519_pk),
576+
"X25519 public key should be valid according to X25519 rules"
577+
);
578+
579+
println!("X25519 key validation: Success");
580+
}
336581
}

src/classic/crypto_sign_ed25519.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ pub(crate) fn crypto_sign_ed25519_keypair_inplace(
7373
use crate::rng::copy_randombytes;
7474
let mut seed = [0u8; CRYPTO_SIGN_ED25519_SEEDBYTES];
7575
copy_randombytes(&mut seed);
76-
crypto_sign_ed25519_seed_keypair_inplace(public_key, secret_key, &seed)
76+
crypto_sign_ed25519_seed_keypair_inplace(public_key, secret_key, &seed);
7777
}
7878

7979
/// Generates a random Ed25519 keypair which can be used for signing

src/constants.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ pub const CRYPTO_SIGN_ED25519_SECRETKEYBYTES: usize = 32 + 32;
134134
pub const CRYPTO_SIGN_ED25519_BYTES: usize = 64;
135135
pub const CRYPTO_SIGN_ED25519_SEEDBYTES: usize = 32;
136136
pub const CRYPTO_SIGN_ED25519_MESSAGEBYTES_MAX: usize = SODIUM_SIZE_MAX - CRYPTO_SIGN_ED25519_BYTES;
137+
pub const CRYPTO_CORE_ED25519_BYTES: usize = 32;
137138

138139
pub const CRYPTO_SIGN_BYTES: usize = CRYPTO_SIGN_ED25519_BYTES;
139140
pub const CRYPTO_SIGN_SEEDBYTES: usize = CRYPTO_SIGN_ED25519_SEEDBYTES;

0 commit comments

Comments
 (0)