initial commit
Some checks failed
CI / lint (push) Successful in 1m37s
CI / test-python (push) Successful in 1m49s
CI / test-zig (push) Successful in 1m39s
CI / test-wasm (push) Successful in 1m54s
CI / test (push) Successful in 14m44s
CI / miri (push) Successful in 14m18s
CI / build (push) Successful in 1m9s
CI / fuzz-regression (push) Successful in 9m9s
CI / publish (push) Failing after 1m10s
CI / publish-python (push) Failing after 1m46s
CI / publish-wasm (push) Has been cancelled
Some checks failed
CI / lint (push) Successful in 1m37s
CI / test-python (push) Successful in 1m49s
CI / test-zig (push) Successful in 1m39s
CI / test-wasm (push) Successful in 1m54s
CI / test (push) Successful in 14m44s
CI / miri (push) Successful in 14m18s
CI / build (push) Successful in 1m9s
CI / fuzz-regression (push) Successful in 9m9s
CI / publish (push) Failing after 1m10s
CI / publish-python (push) Failing after 1m46s
CI / publish-wasm (push) Has been cancelled
Signed-off-by: Kamal Tufekcic <kamal@lo.sh>
This commit is contained in:
commit
1d99048c95
165830 changed files with 79062 additions and 0 deletions
195
soliton/src/primitives/ed25519.rs
Normal file
195
soliton/src/primitives/ed25519.rs
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
//! Ed25519 signatures (RFC 8032, FIPS 186-5).
|
||||
//!
|
||||
//! Native Ed25519 signing/verification via `ed25519-dalek`. Unlike XEdDSA,
|
||||
//! this uses a dedicated Ed25519 keypair — the X25519 key inside X-Wing is
|
||||
//! not involved in signing.
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use zeroize::Zeroize;
|
||||
|
||||
/// Ed25519 signature size: 64 bytes.
|
||||
pub const SIGNATURE_SIZE: usize = 64;
|
||||
|
||||
/// Ed25519 public key size: 32 bytes.
|
||||
pub const PUBLIC_KEY_SIZE: usize = 32;
|
||||
|
||||
/// Ed25519 secret key size: 32 bytes.
|
||||
pub const SECRET_KEY_SIZE: usize = 32;
|
||||
|
||||
/// Generate an Ed25519 keypair from the OS CSPRNG.
|
||||
///
|
||||
/// Generates 32 random bytes via `getrandom` and constructs the keypair
|
||||
/// deterministically from that seed. The `SigningKey` implements
|
||||
/// `ZeroizeOnDrop` — no manual cleanup needed.
|
||||
#[must_use = "dropping the keypair loses secret key material without zeroization"]
|
||||
pub fn keygen() -> (ed25519_dalek::VerifyingKey, ed25519_dalek::SigningKey) {
|
||||
// Generate 32 random bytes and use them as the Ed25519 secret key seed.
|
||||
// This matches ed25519-dalek's SigningKey::generate() but uses our
|
||||
// getrandom-based CSPRNG instead of requiring a rand_core::CryptoRngCore.
|
||||
let mut seed: [u8; 32] = super::random::random_array();
|
||||
let sk = ed25519_dalek::SigningKey::from_bytes(&seed);
|
||||
let vk = sk.verifying_key();
|
||||
// seed is [u8; 32] (Copy) — SigningKey::from_bytes copies it into the
|
||||
// SigningKey (which is ZeroizeOnDrop). Zeroize the original stack slot.
|
||||
seed.zeroize();
|
||||
(vk, sk)
|
||||
}
|
||||
|
||||
/// Sign a message with an Ed25519 secret key (RFC 8032).
|
||||
///
|
||||
/// Deterministic: the same (sk, message) always produces the same signature
|
||||
/// (no random nonce, unlike XEdDSA). The nonce is derived from SHA-512 of the
|
||||
/// secret key's prefix and the message, per RFC 8032 §5.1.6.
|
||||
#[must_use = "signature must not be discarded"]
|
||||
pub fn sign(sk: &ed25519_dalek::SigningKey, message: &[u8]) -> [u8; 64] {
|
||||
use ed25519_dalek::Signer;
|
||||
sk.sign(message).to_bytes()
|
||||
}
|
||||
|
||||
/// Verify an Ed25519 signature (RFC 8032, strict mode).
|
||||
///
|
||||
/// Uses `verify_strict` which rejects non-canonical signatures and small-order
|
||||
/// public keys, preventing malleability attacks.
|
||||
pub fn verify(vk: &ed25519_dalek::VerifyingKey, message: &[u8], sig: &[u8; 64]) -> Result<()> {
|
||||
let signature = ed25519_dalek::Signature::from_bytes(sig);
|
||||
vk.verify_strict(message, &signature)
|
||||
.map_err(|_| Error::VerificationFailed)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::error::Error;
|
||||
use proptest::prelude::*;
|
||||
|
||||
#[test]
|
||||
fn sign_verify_round_trip() {
|
||||
let (vk, sk) = keygen();
|
||||
let msg = b"hello ed25519";
|
||||
let sig = sign(&sk, msg);
|
||||
assert!(verify(&vk, msg, &sig).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sign_verify_different_messages() {
|
||||
let (vk, sk) = keygen();
|
||||
let sig = sign(&sk, b"message one");
|
||||
assert!(matches!(
|
||||
verify(&vk, b"message two", &sig),
|
||||
Err(Error::VerificationFailed)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sign_verify_different_keys() {
|
||||
let (_vk1, sk1) = keygen();
|
||||
let (vk2, _sk2) = keygen();
|
||||
let sig = sign(&sk1, b"test message");
|
||||
assert!(matches!(
|
||||
verify(&vk2, b"test message", &sig),
|
||||
Err(Error::VerificationFailed)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signature_is_64_bytes() {
|
||||
let (_vk, sk) = keygen();
|
||||
let sig = sign(&sk, b"size check");
|
||||
assert!(sig.iter().any(|&b| b != 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sign_is_deterministic() {
|
||||
let (_vk, sk) = keygen();
|
||||
let msg = b"same message both times";
|
||||
let sig1 = sign(&sk, msg);
|
||||
let sig2 = sign(&sk, msg);
|
||||
// Ed25519 is deterministic (RFC 8032) — unlike XEdDSA which uses random Z.
|
||||
assert_eq!(sig1, sig2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_empty_message() {
|
||||
let (vk, sk) = keygen();
|
||||
let msg: &[u8] = &[];
|
||||
let sig = sign(&sk, msg);
|
||||
assert!(verify(&vk, msg, &sig).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_large_message() {
|
||||
let (vk, sk) = keygen();
|
||||
let msg = vec![0xABu8; 65536]; // 64 KB
|
||||
let sig = sign(&sk, &msg);
|
||||
assert!(verify(&vk, &msg, &sig).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_rejects_non_canonical_s() {
|
||||
let (vk, sk) = keygen();
|
||||
let msg = b"non-canonical S test";
|
||||
let mut sig = sign(&sk, msg);
|
||||
assert!(verify(&vk, msg, &sig).is_ok());
|
||||
|
||||
// Replace S (bytes 32..64) with L, the Ed25519 curve order.
|
||||
let l_bytes: [u8; 32] = [
|
||||
0xed, 0xd3, 0xf5, 0x5c, 0x1a, 0x63, 0x12, 0x58, 0xd6, 0x9c, 0xf7, 0xa2, 0xde, 0xf9,
|
||||
0xde, 0x14, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x10,
|
||||
];
|
||||
sig[32..64].copy_from_slice(&l_bytes);
|
||||
assert!(matches!(
|
||||
verify(&vk, msg, &sig),
|
||||
Err(Error::VerificationFailed)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn regression_vector_rfc8032() {
|
||||
// RFC 8032 §7.1 Test Vector 1 (empty message).
|
||||
use hex_literal::hex;
|
||||
let sk_bytes = hex!("9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60");
|
||||
let pk_bytes = hex!("d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a");
|
||||
let expected_sig = hex!(
|
||||
"e5564300c360ac729086e2cc806e828a84877f1eb8e5d974d873e06522490155"
|
||||
"5fb8821590a33bacc61e39701cf9b46bd25bf5f0595bbe24655141438e7a100b"
|
||||
);
|
||||
|
||||
let sk = ed25519_dalek::SigningKey::from_bytes(&sk_bytes);
|
||||
let vk = sk.verifying_key();
|
||||
// Verify the derived public key matches the RFC 8032 test vector.
|
||||
assert_eq!(vk.as_bytes(), &pk_bytes);
|
||||
let sig = sign(&sk, b"");
|
||||
assert_eq!(sig, expected_sig);
|
||||
assert!(verify(&vk, b"", &sig).is_ok());
|
||||
}
|
||||
|
||||
proptest! {
|
||||
#![proptest_config(proptest::prelude::ProptestConfig::with_cases(1000))]
|
||||
#[test]
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
fn proptest_round_trip(
|
||||
sk_bytes in prop::array::uniform32(any::<u8>()),
|
||||
msg_len in 0..4096usize,
|
||||
flip_byte in 0..64usize,
|
||||
) {
|
||||
let sk = ed25519_dalek::SigningKey::from_bytes(&sk_bytes);
|
||||
let vk = sk.verifying_key();
|
||||
let msg: Vec<u8> = (0..msg_len).map(|i| (i % 256) as u8).collect();
|
||||
let sig = sign(&sk, &msg);
|
||||
prop_assert!(verify(&vk, &msg, &sig).is_ok());
|
||||
|
||||
// Forgery rejection: mutated signature must fail verification.
|
||||
let mut bad_sig = sig;
|
||||
bad_sig[flip_byte] ^= 0x01;
|
||||
prop_assert!(verify(&vk, &msg, &bad_sig).is_err());
|
||||
|
||||
// Forgery rejection: mutated message must fail verification.
|
||||
if !msg.is_empty() {
|
||||
let mut bad_msg = msg.clone();
|
||||
bad_msg[0] ^= 0x01;
|
||||
prop_assert!(verify(&vk, &bad_msg, &sig).is_err());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue