mirror of
https://github.com/dalek-cryptography/ed25519-dalek.git
synced 2026-01-08 18:57:54 -05:00
Validation criteria tests (#253)
This commit is contained in:
committed by
GitHub
parent
65aeda0867
commit
f036eaf482
@@ -11,7 +11,7 @@ documentation = "https://docs.rs/ed25519-dalek"
|
||||
keywords = ["cryptography", "ed25519", "curve25519", "signature", "ECC"]
|
||||
categories = ["cryptography", "no-std"]
|
||||
description = "Fast and efficient ed25519 EdDSA key generations, signing, and verification in pure Rust."
|
||||
exclude = [ ".gitignore", "TESTVECTORS", "res/*" ]
|
||||
exclude = [ ".gitignore", "TESTVECTORS", "VALIDATIONVECTORS", "res/*" ]
|
||||
rust-version = "1.60"
|
||||
|
||||
[badges]
|
||||
|
||||
11136
VALIDATIONVECTORS
Normal file
11136
VALIDATIONVECTORS
Normal file
File diff suppressed because it is too large
Load Diff
232
tests/validation_criteria.rs
Normal file
232
tests/validation_criteria.rs
Normal file
@@ -0,0 +1,232 @@
|
||||
use ed25519::signature::Verifier;
|
||||
use ed25519_dalek::{Signature, VerifyingKey};
|
||||
|
||||
use serde::{de::Error as SError, Deserialize, Deserializer};
|
||||
use std::{collections::BTreeSet as Set, fs::File};
|
||||
|
||||
/// The set of edge cases that [`VerifyingKey::verify()`] permits.
|
||||
const VERIFY_ALLOWED_EDGECASES: &[Flag] = &[
|
||||
Flag::LowOrderA,
|
||||
Flag::LowOrderR,
|
||||
Flag::NonCanonicalA,
|
||||
Flag::LowOrderComponentA,
|
||||
Flag::LowOrderComponentR,
|
||||
// `ReencodedK` is not actually permitted by `verify()`, but it looks that way in the tests
|
||||
// because it sometimes occurs with a low-order A. 1/8 of the time, the resulting signature
|
||||
// will be identical the one made with a normal k. find_validation_criteria shows that indeed
|
||||
// this occurs 10/58 of the time
|
||||
Flag::ReencodedK,
|
||||
];
|
||||
|
||||
/// The set of edge cases that [`VerifyingKey::verify_strict()`] permits
|
||||
const VERIFY_STRICT_ALLOWED_EDGECASES: &[Flag] =
|
||||
&[Flag::LowOrderComponentA, Flag::LowOrderComponentR];
|
||||
|
||||
/// Each variant describes a specfiic edge case that can occur in an Ed25519 signature. Refer to
|
||||
/// the test vector [README][] for more info.
|
||||
///
|
||||
/// [README]: https://github.com/C2SP/CCTV/blob/5ea85644bd035c555900a2f707f7e4c31ea65ced/ed25519vectors/README.md
|
||||
#[derive(Deserialize, Debug, Copy, Clone, PartialOrd, Ord, Eq, PartialEq)]
|
||||
enum Flag {
|
||||
#[serde(rename = "low_order")]
|
||||
LowOrder,
|
||||
#[serde(rename = "low_order_A")]
|
||||
LowOrderA,
|
||||
#[serde(rename = "low_order_R")]
|
||||
LowOrderR,
|
||||
#[serde(rename = "non_canonical_A")]
|
||||
NonCanonicalA,
|
||||
#[serde(rename = "non_canonical_R")]
|
||||
NonCanonicalR,
|
||||
#[serde(rename = "low_order_component_A")]
|
||||
LowOrderComponentA,
|
||||
#[serde(rename = "low_order_component_R")]
|
||||
LowOrderComponentR,
|
||||
#[serde(rename = "low_order_residue")]
|
||||
LowOrderResidue,
|
||||
#[serde(rename = "reencoded_k")]
|
||||
ReencodedK,
|
||||
}
|
||||
|
||||
/// This is an intermediate representation between JSON and TestVector
|
||||
#[derive(Deserialize)]
|
||||
struct IntermediateTestVector {
|
||||
number: usize,
|
||||
#[serde(deserialize_with = "bytes_from_hex", rename = "key")]
|
||||
pubkey: Vec<u8>,
|
||||
#[serde(deserialize_with = "bytes_from_hex")]
|
||||
sig: Vec<u8>,
|
||||
msg: String,
|
||||
flags: Option<Set<Flag>>,
|
||||
}
|
||||
|
||||
/// The test vector struct from [CCTV][]. `sig` may or may not be a valid signature of `msg` with
|
||||
/// respect to `pubkey`, depending on the verification function's validation criteria. `flags`
|
||||
/// describes all the edge cases which this test vector falls into.
|
||||
///
|
||||
/// [CCTV]: https://github.com/C2SP/CCTV/tree/5ea85644bd035c555900a2f707f7e4c31ea65ced/ed25519vectors
|
||||
struct TestVector {
|
||||
number: usize,
|
||||
pubkey: VerifyingKey,
|
||||
sig: Signature,
|
||||
msg: Vec<u8>,
|
||||
flags: Set<Flag>,
|
||||
}
|
||||
|
||||
impl From<IntermediateTestVector> for TestVector {
|
||||
fn from(tv: IntermediateTestVector) -> Self {
|
||||
let number = tv.number;
|
||||
let pubkey = {
|
||||
let mut buf = [0u8; 32];
|
||||
buf.copy_from_slice(&tv.pubkey);
|
||||
VerifyingKey::from_bytes(&buf).unwrap()
|
||||
};
|
||||
let sig = {
|
||||
let mut buf = [0u8; 64];
|
||||
buf.copy_from_slice(&tv.sig);
|
||||
Signature::from_bytes(&buf).unwrap()
|
||||
};
|
||||
let msg = tv.msg.as_bytes().to_vec();
|
||||
|
||||
// Unwrap the Option<Set<Flag>>
|
||||
let flags = tv.flags.unwrap_or_else(Default::default);
|
||||
|
||||
Self {
|
||||
number,
|
||||
pubkey,
|
||||
sig,
|
||||
msg,
|
||||
flags,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tells serde how to deserialize bytes from hex
|
||||
fn bytes_from_hex<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let mut hex_str = String::deserialize(deserializer)?;
|
||||
// Prepend a 0 if it's not even length
|
||||
if hex_str.len() % 2 == 1 {
|
||||
hex_str.insert(0, '0');
|
||||
}
|
||||
hex::decode(hex_str).map_err(|e| SError::custom(format!("{:?}", e)))
|
||||
}
|
||||
|
||||
fn get_test_vectors() -> impl Iterator<Item = TestVector> {
|
||||
let f = File::open("VALIDATIONVECTORS").expect(
|
||||
"This test is only available when the code has been cloned from the git repository, since
|
||||
the VALIDATIONVECTORS file is large and is therefore not included within the distributed \
|
||||
crate.",
|
||||
);
|
||||
|
||||
serde_json::from_reader::<_, Vec<IntermediateTestVector>>(f)
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(TestVector::from)
|
||||
}
|
||||
|
||||
/// Tests that the verify() and verify_strict() functions succeed only on test cases whose flags
|
||||
/// (i.e., edge cases it falls into) are a subset of VERIFY_ALLOWED_EDGECASES and
|
||||
/// VERIFY_STRICT_ALLOWED_EDGECASES, respectively
|
||||
#[test]
|
||||
fn check_validation_criteria() {
|
||||
let verify_allowed_edgecases = Set::from_iter(VERIFY_ALLOWED_EDGECASES.to_vec().into_iter());
|
||||
let verify_strict_allowed_edgecases =
|
||||
Set::from_iter(VERIFY_STRICT_ALLOWED_EDGECASES.to_vec().into_iter());
|
||||
|
||||
for TestVector {
|
||||
number,
|
||||
pubkey,
|
||||
msg,
|
||||
sig,
|
||||
flags,
|
||||
} in get_test_vectors()
|
||||
{
|
||||
// If all the verify-permitted flags here are ones we permit, then verify() should succeed.
|
||||
// Otherwise, it should not.
|
||||
let success = pubkey.verify(&msg, &sig).is_ok();
|
||||
if flags.is_subset(&verify_allowed_edgecases) {
|
||||
assert!(success, "verify() expected success in testcase #{number}",);
|
||||
} else {
|
||||
assert!(!success, "verify() expected failure in testcase #{number}",);
|
||||
}
|
||||
|
||||
// If all the verify_strict-permitted flags here are ones we permit, then verify_strict()
|
||||
// should succeed. Otherwise, it should not.
|
||||
let success = pubkey.verify_strict(&msg, &sig).is_ok();
|
||||
if flags.is_subset(&verify_strict_allowed_edgecases) {
|
||||
assert!(
|
||||
success,
|
||||
"verify_strict() expected success in testcase #{number}",
|
||||
);
|
||||
} else {
|
||||
assert!(
|
||||
!success,
|
||||
"verify_strict() expected failure in testcase #{number}",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Prints the flags that are consistently permitted by verify() and verify_strict()
|
||||
#[test]
|
||||
fn find_validation_criteria() {
|
||||
let mut verify_allowed_edgecases = Set::new();
|
||||
let mut verify_strict_allowed_edgecases = Set::new();
|
||||
|
||||
// Counts the number of times a signature with a re-encoded k and a low-order A verified. This
|
||||
// happens with 1/8 probability, assuming the usual verification equation(s).
|
||||
let mut num_lucky_reencoded_k = 0;
|
||||
let mut num_reencoded_k = 0;
|
||||
|
||||
for TestVector {
|
||||
number: _,
|
||||
pubkey,
|
||||
msg,
|
||||
sig,
|
||||
flags,
|
||||
} in get_test_vectors()
|
||||
{
|
||||
// If verify() was a success, add all the associated flags to verify-permitted set
|
||||
let success = pubkey.verify(&msg, &sig).is_ok();
|
||||
|
||||
// If this is ReencodedK && LowOrderA, log some statistics
|
||||
if flags.contains(&Flag::ReencodedK) && flags.contains(&Flag::LowOrderA) {
|
||||
num_reencoded_k += 1;
|
||||
num_lucky_reencoded_k += success as u8;
|
||||
}
|
||||
|
||||
if success {
|
||||
for flag in &flags {
|
||||
// Don't count re-encoded k when A is low-order. This is because the
|
||||
// re-encoded k might be a multiple of 8 by accident
|
||||
if *flag == Flag::ReencodedK && flags.contains(&Flag::LowOrderA) {
|
||||
continue;
|
||||
} else {
|
||||
verify_allowed_edgecases.insert(*flag);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If verify_strict() was a success, add all the associated flags to
|
||||
// verify_strict-permitted set
|
||||
let success = pubkey.verify_strict(&msg, &sig).is_ok();
|
||||
if success {
|
||||
for flag in &flags {
|
||||
verify_strict_allowed_edgecases.insert(*flag);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("VERIFY_ALLOWED_EDGECASES: {:?}", verify_allowed_edgecases);
|
||||
println!(
|
||||
"VERIFY_STRICT_ALLOWED_EDGECASES: {:?}",
|
||||
verify_strict_allowed_edgecases
|
||||
);
|
||||
println!(
|
||||
"re-encoded k && low-order A yielded a valid signature {}/{} of the time",
|
||||
num_lucky_reencoded_k, num_reencoded_k
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user