Comments, docs and new names for some functions

This commit is contained in:
AlekseiVambol
2025-02-10 05:22:15 +02:00
parent 65fe469c8d
commit 88722c78e4
7 changed files with 127 additions and 22 deletions

View File

@@ -1,3 +1,28 @@
# mdsecheck
The crate provides tools for generating random square [MDS matrices](https://en.wikipedia.org/wiki/MDS_matrix) over prime finite fields and applying the MDSECheck method to check such matrices for unconditional security as the components of affine permutation boxes of [partial substitution-permutation networks (P-SPNs)](https://eprint.iacr.org/2020/500.pdf), which are widespread designs of the modern symmetric ciphers and hash functions. The used data types of field elements and polynomials are provided by the crates [ark-ff](https://docs.rs/ark-ff) and [ark-poly](https://docs.rs/ark-poly). The auxiliary tools in the crate modules are accessible as well.
Work in Progress
The round unconditional P-SPN security level of the square MDS matrix M is defined as l rounds, where l is a positive integer, if and only if M simultaneously satisfies the following conditions:
1. The [minimal polynomials](https://en.wikipedia.org/wiki/Minimal_polynomial_(linear_algebra)) of M, M<sup>2</sup>, ..., M<sup>l</sup> have maximum degree and are irreducible.
2. The minimal polynomial of M<sup>l + 1</sup> is not of maximum degree or not irreducible.
Theorem 8 in the paper ["Proving Resistance Against Infinitely Long Subspace Trails: How to Choose the Linear Layer"](https://eprint.iacr.org/2020/500.pdf) by L. Grassi, C. Rechberger and M. Schofnegger ensures that if the round unconditional P-SPN security level of the matrix is l, then "there is no infinitely long [subspace trail](https://eprint.iacr.org/2020/500.pdf) with/without active S-boxes of period less than or equal to l" regardless of the structure of the P-SPN affine permutation box using this matrix, but does not provide the same guarantees for larger periods. This independence from the P-SPN affine permutation boxes is the reason for using the term "unconditional security". Once an MDS matrix with the round unconditional P-SPN security level l has been chosen, it can protect any P-SPN with at most l rounds from the ["attacks based on infinitely long truncated differentials with probability 1"](https://eprint.iacr.org/2020/500.pdf).
To check whether the round unconditional P-SPN security level of the specified matrix is no less than the specified bound, the crate provides the implementation of the MDSECheck method, whose name is derived from the words "MDS", "security", "elaborated" and "check". An incomplete description of this novel method can be found in [this report](https://notes.status.im/CVMoa6EcTmS2D4VPBCsH2w), while a reference to a more detailed article dedicated to MDSECheck is planned to be included in the next version of the crate.
# Example
```rust
use ark_bn254::Fr;
use mdsecheck::{random_cauchy, security_level};
use rand_chacha::{rand_core::SeedableRng, ChaCha8Rng};
// Generating pseudorandom 5 x 5 MDS matrices over the BN254 scalar field until
// a matrix with the round unconditional P-SPN security level 25 is obtained
let mut r = ChaCha8Rng::seed_from_u64(123456);
loop {
// The field is large enough to generate 5 x 5 Cauchy matrices
let m = random_cauchy::<Fr>(5, &mut r).unwrap();
if security_level(&m, 25) == Some(25) {
println!("{:?}", m);
break;
}
}
```

View File

@@ -1,3 +1,5 @@
#![doc = include_str!("../README.md")]
use ark_ff::PrimeField;
use ark_poly::{polynomial::univariate::DensePolynomial, DenseUVPolynomial};
use indexmap::IndexSet;
@@ -7,8 +9,14 @@ pub mod mat;
pub mod num;
pub mod poly;
/// Creates a random Cauchy square MDS matrix, which has the order specified by the first
/// argument and the entries, which are determined by both the order and the source of
/// randomness specified by the second argument. If the first argument is 0 or the field
/// does not have enough elements for the specified matrix order, then None is returned.
pub fn random_cauchy<F: PrimeField>(n: u32, r: &mut (impl Rng + ?Sized)) -> Option<Vec<Vec<F>>> {
if (n == 0) || (F::MODULUS < F::BigInt::from(n as u64 * 2)) {
// The first argument is 0 or the field does not have
// enough elements for the specified matrix order
return None;
}
let n = n as usize;
@@ -20,6 +28,8 @@ pub fn random_cauchy<F: PrimeField>(n: u32, r: &mut (impl Rng + ?Sized)) -> Opti
for y in d.iter().skip(n) {
let mut v = Vec::with_capacity(n);
for x in d.iter().take(n) {
// Since all elements in IndexSet are distinct, for
// each pair of them the difference is invertible
v.push((*x - y).inverse().unwrap());
}
m.push(v);
@@ -27,11 +37,23 @@ pub fn random_cauchy<F: PrimeField>(n: u32, r: &mut (impl Rng + ?Sized)) -> Opti
Some(m)
}
pub fn secure_rounds<F: PrimeField>(a: &[impl AsRef<[F]>], l: u32) -> Option<u32> {
/// Computes the largest positive number, which exceeds neither the round unconditional
/// P-SPN security level of the specified MDS matrix, nor the second argument, by means
/// of the MDSECheck method. The matrix is not checked for being MDS, so it should be
/// generated properly, e.g. using the tools the crate provides. If the matrix is not
/// unconditionally P-SPN secure, then None is returned.
pub fn security_level<F: PrimeField>(a: &[impl AsRef<[F]>], l: u32) -> Option<u32> {
let n = a.len();
if (n < 2) || (l == 0) {
// The first argument is not a matrix, which can
// be used in P-SPN, or the second argument is 0
return None;
}
// Using the Krylovs method fragment, which successfully
// computes the minimal polynomial of the matrix, provided
// that the polynomial is of maximum degree and irreducible,
// fails if the polynomial is not of maximum degree and may
// fail in other cases
let mut m = Vec::<Vec<F>>::with_capacity(n);
m.push(vec![F::ONE; n]);
for i in 0..n - 1 {
@@ -43,10 +65,16 @@ pub fn secure_rounds<F: PrimeField>(a: &[impl AsRef<[F]>], l: u32) -> Option<u32
(m[y][x], m[x][y]) = (m[x][y], m[y][x]);
}
}
let mut s = mat::unique_solution(&m, &b)?;
// If the function returns at this point, then the Krylovs method fragment
// has failed. Consequently, the matrix is not unconditionally P-SPN secure
let mut s = mat::system_solution(&m, &b)?;
s.iter_mut().for_each(|e| *e = -*e);
s.push(F::ONE);
let c = DensePolynomial::<F>::from_coefficients_vec(s);
// Checking the irreducibility of the minimal polynomial, which has been found using
// the Krylovs method fragment, by means of Algorithm 2.2.9 in the book "Prime Numbers -
// A Computational Perspective (2nd edn.)" by R. Crandall and C. Pomerance. Some values
// computed in this step will be used in the next one
let f = num::prime_divisors(n as u32)
.into_iter()
.map(|e| n / e as usize)
@@ -57,18 +85,28 @@ pub fn secure_rounds<F: PrimeField>(a: &[impl AsRef<[F]>], l: u32) -> Option<u32
for d in 1..=n / 2 {
r = poly::power_modulo(&r, F::characteristic(), &c)?;
if !poly::coprimality(&(&r - &x), &c) {
// The minimal polynomial is not irreducible
return None;
}
if f.contains(&d) {
// Saving a value useful for the next step
y.push(r.clone());
}
}
// Checking that the minimal polynomials for the higher powers of the matrix also are
// of maximum degree and irreducible. This is done by checking that the higher powers
// of a root of the characteristic polynomial of the matrix do not belong to nontrivial
// subfields of the splitting field of the characteristic polynomial. Since the minimal
// polynomial of the matrix is of maximum degree, it equals the characteristic polynomial
let (mut g, mut h) = (x.clone(), y.clone());
for i in 2..=l {
g = poly::reduced_modulo(&(&g * &x), &c)?;
for (v, u) in h.iter_mut().zip(y.iter()) {
*v = poly::reduced_modulo(&(&*v * u), &c)?;
if *v == g {
// For the current power of the matrix the minimal polynomial is not
// of maximum degree or not irreducible, so the round unconditional
// P-SPN security level of the matrix equals the previous exponent
return Some(i - 1);
}
}

View File

@@ -1,5 +1,9 @@
//! Provides auxiliary tools for working with matrices.
use ark_ff::Field;
/// Computes the matrix product of the arguments. If the arguments are
/// not matrices for which the product is defined, then None is returned.
pub fn product_matrix<F: Field>(
a: &[impl AsRef<[F]>],
b: &[impl AsRef<[F]>],
@@ -26,6 +30,8 @@ pub fn product_matrix<F: Field>(
Some(m)
}
/// Computes the product of the specified matrix and column vector. If the arguments
/// are not a matrix-vector pair for which the product is defined, then None is returned.
pub fn product_vector<F: Field>(a: &[impl AsRef<[F]>], b: &[F]) -> Option<Vec<F>> {
let k = b.len();
if (k == 0) || a.is_empty() {
@@ -43,7 +49,11 @@ pub fn product_vector<F: Field>(a: &[impl AsRef<[F]>], b: &[F]) -> Option<Vec<F>
Some(v)
}
pub fn unique_solution<F: Field>(a: &[impl AsRef<[F]>], b: &[F]) -> Option<Vec<F>> {
/// Computes such a column vector that the product of the specified nonsingular
/// matrix and it is equal to the specified column vector by means of the Gaussian
/// elimination method. If the first argument is not a nonsingular matrix of the
/// height of the specified vector, then None is returned.
pub fn system_solution<F: Field>(a: &[impl AsRef<[F]>], b: &[F]) -> Option<Vec<F>> {
let n = a.len();
if n == 0 {
// The matrix is empty
@@ -62,6 +72,7 @@ pub fn unique_solution<F: Field>(a: &[impl AsRef<[F]>], b: &[F]) -> Option<Vec<F
}
m.push([r, &[*v][..]].concat());
}
// Obtaining the row echelon form of the augmented matrix
for r in 0..n {
if m[r][r] == F::ZERO {
if let Some(p) = (r + 1..n).find(|y| m[*y][r] != F::ZERO) {
@@ -84,6 +95,8 @@ pub fn unique_solution<F: Field>(a: &[impl AsRef<[F]>], b: &[F]) -> Option<Vec<F
}
}
}
// Transforming the rightmost column of the row
// echelon matrix into the sought column vector
for r in (1..n).rev() {
for y in 0..r {
m[y][n] = m[y][n] - m[y][r] * m[r][n];

View File

@@ -1,3 +1,7 @@
//! Provides auxiliary tools for working with numbers.
/// Computes the ascending list of prime numbers that divide
/// the argument by means of the wheel factorization method.
pub fn prime_divisors(mut n: u32) -> Vec<u32> {
if n < 2 {
return vec![];
@@ -26,6 +30,10 @@ pub fn prime_divisors(mut n: u32) -> Vec<u32> {
extract(d, &mut n, &mut p);
b = sqrt(n);
}
// A trial divisor is either 5 or 6k + 1 or 6k + 5 for
// some positive integer k. Therefore, the difference
// between the (j + 1)-th and the j-th trial divisor
// is 2, if j is odd, and 4 otherwise
(d, s) = (d + s, s ^ 6);
}
if n > 1 {
@@ -34,6 +42,11 @@ pub fn prime_divisors(mut n: u32) -> Vec<u32> {
p
}
/// Creates an iterator over the bits of the argument's big-endian
/// binary representation with leading zeroes removed. The argument
/// is an unsigned integer represented by its 64-bit chunks stored
/// in the little-endian order. If the argument is 0, an empty
/// iterator is returned.
pub fn reversed_bits(n: &[u64]) -> impl Iterator<Item = bool> + '_ {
n.iter()
.rev()

View File

@@ -1,9 +1,12 @@
//! Provides auxiliary tools for working with polynomials.
use ark_ff::{PrimeField, Zero};
use ark_poly::{
polynomial::univariate::{DenseOrSparsePolynomial, DensePolynomial},
DenseUVPolynomial, Polynomial,
};
/// Checks whether the arguments are coprime polynomials by means of the Euclidean method.
pub fn coprimality<F: PrimeField>(a: &DensePolynomial<F>, b: &DensePolynomial<F>) -> bool {
let mut a = match reduced_modulo(a, b) {
Some(r) => r,
@@ -19,10 +22,16 @@ pub fn coprimality<F: PrimeField>(a: &DensePolynomial<F>, b: &DensePolynomial<F>
a.degree() == 0
}
/// Creates a polynomial from the specified big-endian slice of coefficients.
pub fn new<F: PrimeField>(c: &[impl Into<F> + Clone]) -> DensePolynomial<F> {
DensePolynomial::from_coefficients_vec(c.iter().rev().map(|e| e.clone().into()).collect())
}
/// Computes the first argument raised to the power of the second argument
/// modulo the third one by means of the left-to-right binary exponentiation
/// method. The second argument is an unsigned integer represented by its
/// 64-bit chunks stored in the little-endian order. If the modulus is 0,
/// then None is returned.
pub fn power_modulo<F: PrimeField>(
p: &DensePolynomial<F>,
e: &[u64],
@@ -39,12 +48,14 @@ pub fn power_modulo<F: PrimeField>(
Some(r)
}
/// Computes the first argument modulo the second.
/// If the modulus is 0, then None is returned.
pub fn reduced_modulo<F: PrimeField>(
p: &DensePolynomial<F>,
m: &DensePolynomial<F>,
) -> Option<DensePolynomial<F>> {
// This check is to avoid a "divide_with_q_and_r" panic, since
// for some reason "divide_with_q_and_r" never returns None
// This check is performed to avoid panics of "divide_with_q_and_r",
// because for some reason "divide_with_q_and_r" never returns None
if m.is_zero() {
return None;
}

View File

@@ -1,5 +1,5 @@
use ark_bn254::Fr;
use mdsecheck::{mat, random_cauchy, secure_rounds};
use mdsecheck::{mat, random_cauchy, security_level};
use rand_chacha::{rand_core::SeedableRng, ChaCha8Rng};
#[test]
@@ -8,17 +8,22 @@ fn test_random_cauchy() {
let b = [Fr::from(1), Fr::from(2), Fr::from(3), Fr::from(4)];
for (_, n) in (0..100).zip((1..=b.len()).cycle()) {
let m = random_cauchy::<Fr>(n as u32, &mut r).unwrap();
mat::unique_solution(&m, &b[..n]).unwrap();
// Checking the generated matrix for nonsingularity
mat::system_solution(&m, &b[..n]).unwrap();
}
}
#[test]
fn test_secure_rounds() {
fn test_security_level() {
let mut r = ChaCha8Rng::seed_from_u64(456);
for (i, n) in (0..12).zip((2..=7).cycle()) {
let m = random_cauchy::<Fr>(n, &mut r).unwrap();
let s = secure_rounds(&m, 25);
let s = security_level(&m, 25);
match i {
// The expected output has been computed by means of a SageMath script, which uses
// the built-in tools for working with matrices and polynomials over finite fields.
// In particular, the methods for computing the characteristic polynomial of a matrix
// and checking the irreducibility of a polynomial have been used
0 | 7 | 9 => assert_eq!(s, Some(25)),
_ => assert_eq!(s, None),
}

View File

@@ -1,5 +1,5 @@
use ark_bn254::Fr;
use mdsecheck::mat::{product_matrix, product_vector, unique_solution};
use mdsecheck::mat::{product_matrix, product_vector, system_solution};
#[test]
fn test_product_matrix() {
@@ -156,31 +156,31 @@ fn test_product_vector() {
}
#[test]
fn test_unique_solution() {
fn test_system_solution() {
assert_eq!(
unique_solution(&[] as &[&[Fr]], &[Fr::from(1), Fr::from(2), Fr::from(3)]),
system_solution(&[] as &[&[Fr]], &[Fr::from(1), Fr::from(2), Fr::from(3)]),
None
);
assert_eq!(
unique_solution(&[vec![]], &[Fr::from(3), Fr::from(2), Fr::from(1)]),
system_solution(&[vec![]], &[Fr::from(3), Fr::from(2), Fr::from(1)]),
None
);
assert_eq!(
unique_solution(
system_solution(
&[[Fr::from(2), Fr::from(1)], [Fr::from(1), Fr::from(0)]],
&[]
),
None
);
assert_eq!(
unique_solution(
system_solution(
&[vec![Fr::from(3), Fr::from(4)], vec![Fr::from(5)]],
&[Fr::from(6), Fr::from(7)]
),
None
);
assert_eq!(
unique_solution(
system_solution(
&[
[Fr::from(1), Fr::from(2), Fr::from(3)],
[Fr::from(4), Fr::from(5), Fr::from(6)]
@@ -190,7 +190,7 @@ fn test_unique_solution() {
None
);
assert_eq!(
unique_solution(
system_solution(
&[
[Fr::from(1), Fr::from(2)],
[Fr::from(2), Fr::from(3)],
@@ -201,7 +201,7 @@ fn test_unique_solution() {
None
);
assert_eq!(
unique_solution(
system_solution(
&[
[Fr::from(1), Fr::from(2), Fr::from(3)],
[Fr::from(4), Fr::from(5), Fr::from(6)],
@@ -212,7 +212,7 @@ fn test_unique_solution() {
None
);
assert_eq!(
unique_solution(
system_solution(
&[
[Fr::from(1), Fr::from(2), Fr::from(3)],
[Fr::from(4), Fr::from(5), Fr::from(6)],
@@ -223,7 +223,7 @@ fn test_unique_solution() {
None
);
assert_eq!(
unique_solution(
system_solution(
&[
[Fr::from(1), Fr::from(2), Fr::from(4)],
[Fr::from(1), Fr::from(3), Fr::from(9)],