Validation criteria tests (#253)

This commit is contained in:
Michael Rosenberg
2023-01-05 22:58:54 -05:00
committed by GitHub
parent 65aeda0867
commit f036eaf482
3 changed files with 11369 additions and 1 deletions

View File

@@ -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

File diff suppressed because it is too large Load Diff

View 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
);
}