Reputation: 45
I am making a rust library for Python using Pyo3 and maturin that implements different cryptographic protocols. I thought it would be nice to add one that could resist quantum attacks. Thats why I decided to implement the SWOOSH cryptographic protocol, which is based on lattice cryptography, specifically the Module Learning With Errors (M-LWE) problem.
In order to do this I tried to understand the paper which explains it and also the rust and jasmine implementation that they developed and I ended up with the following downsized implementation:
use pyo3::prelude::*;
use rand::{Rng, SeedableRng, thread_rng};
use rand_chacha::ChaCha20Rng;
const N: usize = 16; // Simplified from 8192
const Q: u64 = 12289; // A smaller prime modulus
const D: usize = 8; // Simplified from 256
type Poly = [i64; D];
type PolyVec = [Poly; N];
fn mod_q(x: i64) -> i64 {
let r = x % Q as i64;
if r < 0 {
r + Q as i64
} else {
r
}
}
fn poly_add(a: &Poly, b: &Poly) -> Poly {
let mut res = [0i64; D];
for i in 0..D {
res[i] = mod_q(a[i] + b[i]);
}
res
}
fn poly_mul(a: &Poly, b: &Poly) -> Poly {
let mut res = [0i64; D];
for i in 0..D {
for j in 0..D {
let k = (i + j) % D;
res[k] = mod_q(res[k] + a[i] * b[j]);
}
}
res
}
fn sample_poly(rng: &mut ChaCha20Rng) -> Poly {
let mut poly = [0i64; D];
for i in 0..D {
let r = rng.gen_range(0..3);
poly[i] = if r == 0 { -1 } else if r == 1 { 0 } else { 1 };
}
poly
}
fn generate_matrix_a(rng: &mut ChaCha20Rng) -> [PolyVec; N] {
let mut matrix = [[sample_poly(rng); N]; N];
for i in 0..N {
for j in 0..N {
matrix[i][j] = sample_poly(rng);
}
}
matrix
}
fn generate_keys() -> (PolyVec, PolyVec) {
let seed: [u8; 32] = thread_rng().gen();
let mut rng = ChaCha20Rng::from_seed(seed);
let s = [sample_poly(&mut rng); N];
let e = [sample_poly(&mut rng); N];
let a = generate_matrix_a(&mut rng);
let mut pk = e;
for i in 0..N {
let mut temp = [0i64; D];
for j in 0..N {
let prod = poly_mul(&a[i][j], &s[j]);
temp = poly_add(&temp, &prod);
}
pk[i] = poly_add(&pk[i], &temp);
}
(s, pk)
}
fn derive_shared_key(s: &PolyVec, pk: &PolyVec) -> Vec<u8> {
let mut k = [0i64; D];
for i in 0..N {
let prod = poly_mul(&s[i], &pk[i]);
k = poly_add(&k, &prod);
}
reconcile(&k)
}
fn reconcile(k: &Poly) -> Vec<u8> {
let mut key_bits = Vec::with_capacity(D);
for i in 0..D {
key_bits.push(if k[i] > Q as i64 / 2 { 1 } else { 0 });
}
key_bits
}
#[pyfunction]
pub fn swoosh_generate_keys() -> PyResult<(Vec<i64>, Vec<i64>)> {
let (sk, pk) = generate_keys();
Ok((sk.into_iter().flatten().collect(), pk.into_iter().flatten().collect()))
}
#[pyfunction]
pub fn swoosh_derive_shared_key(sk: Vec<i64>, pk: Vec<i64>) -> PyResult<Vec<u8>> {
if sk.len() != N * D || pk.len() != N * D {
return Err(PyErr::new::<pyo3::exceptions::PyValueError, _>("Invalid key length"));
}
let sk_polyvec: PolyVec = sk.chunks(D).map(|chunk| chunk.try_into().unwrap()).collect::<Vec<Poly>>().try_into().unwrap();
let pk_polyvec: PolyVec = pk.chunks(D).map(|chunk| chunk.try_into().unwrap()).collect::<Vec<Poly>>().try_into().unwrap();
Ok(derive_shared_key(&sk_polyvec, &pk_polyvec))
}
The problem is that the keys never match between each other, so it keeps on failing basically and I cannot understand why it is happening despite my debugging, and because it is a complex implementation I don´t know if the values I get before are correct. So how can I fix it and is my approach even remotely correct?
Here is the testing code:
def test_swoosh():
print("\nTesting SWOOSH key exchange...")
try:
print("Generating first key pair...")
private_key1, public_key1 = shadow_crypt.swoosh_generate_keys()
print(f"SWOOSH Private Key 1: {private_key1}")
print(f"SWOOSH Public Key 1: {public_key1}")
print("Generating second key pair...")
private_key2, public_key2 = shadow_crypt.swoosh_generate_keys()
print(f"SWOOSH Private Key 2: {private_key2}")
print(f"SWOOSH Public Key 2: {public_key2}")
print("Deriving shared keys...")
shared_key1 = shadow_crypt.swoosh_derive_shared_key(private_key1, public_key2)
shared_key2 = shadow_crypt.swoosh_derive_shared_key(private_key2, public_key1)
print(f"SWOOSH Shared Key 1: {shared_key1}")
print(f"SWOOSH Shared Key 2: {shared_key2}")
if shared_key1 == shared_key2:
print("SWOOSH shared keys match!")
else:
print("WARNING: SWOOSH shared keys do not match!")
print(f"SWOOSH Private key length: {len(private_key1)} integers")
print(f"SWOOSH Public key length: {len(public_key1)} integers")
print(f"SWOOSH Shared key length: {len(shared_key1)} integers")
return shared_key1 == shared_key2
except Exception as e:
print(f"An error occurred: {e}")
import traceback
traceback.print_exc()
return False
And here is the output:
Testing SWOOSH key exchange...
Generating first key pair...
SWOOSH Private Key 1: [1, 0, -1, 0, -1, 1, -1, 1, 1, 0, -1, 0, -1, 1, -1, 1, 1, 0, -1, 0, -1, 1, -1, 1, 1, 0, -1, 0, -1, 1, -1, 1, 1, 0, -1, 0, -1, 1, -1, 1, 1, 0, -1, 0, -1, 1, -1, 1, 1, 0, -1, 0, -1, 1, -1, 1, 1, 0, -1, 0, -1, 1, -1, 1, 1, 0, -1, 0, -1, 1, -1, 1, 1, 0, -1, 0, -1, 1, -1, 1, 1, 0, -1, 0, -1, 1, -1, 1, 1, 0, -1, 0, -1, 1, -1, 1, 1, 0, -1, 0, -1, 1, -1, 1, 1, 0, -1, 0, -1, 1, -1, 1, 1, 0, -1, 0, -1, 1, -1, 1, 1, 0, -1, 0, -1, 1, -1, 1]
SWOOSH Public Key 1: [2, 3, 12281, 12287, 12288, 10, 12278, 8, 8, 3, 11, 12271, 12286, 5, 12285, 12288, 1, 12, 12283, 12288, 2, 12286, 12282, 3, 12279, 5, 12281, 20, 12278, 12288, 1, 5, 12285, 12287, 11, 10, 12281, 2, 12284, 12286, 12282, 5, 0, 12287, 13, 12275, 12287, 8, 15, 12282, 12287, 12279, 12, 12285, 5, 12281, 12282, 2, 10, 1, 3, 12279, 0, 2, 0, 12283, 12280, 12288, 12, 6, 12284, 4, 12287, 8, 12286, 12287, 2, 10, 12274, 3, 12285, 3, 12286, 8, 12286, 8, 0, 12281, 12285, 17, 12267, 3, 12275, 6, 4, 11, 12287, 12281, 12278, 12284, 4, 12288, 13, 11, 9, 12283, 6, 12286, 1, 12283, 2, 12287, 0, 12285, 12279, 8, 0, 12287, 4, 5, 12278, 7, 10, 3, 12275, 3, 12280, 12]
Generating second key pair...
SWOOSH Private Key 2: [-1, 1, 0, -1, 0, 0, 0, 1, -1, 1, 0, -1, 0, 0, 0, 1, -1, 1, 0, -1, 0, 0, 0, 1, -1, 1, 0, -1, 0, 0, 0, 1, -1, 1, 0, -1, 0, 0, 0, 1, -1, 1, 0, -1, 0, 0, 0, 1, -1, 1, 0, -1, 0, 0, 0, 1, -1, 1, 0, -1, 0, 0, 0, 1, -1, 1, 0, -1, 0, 0, 0, 1, -1, 1, 0, -1, 0, 0, 0, 1, -1, 1, 0, -1, 0, 0, 0, 1, -1, 1, 0, -1, 0, 0, 0, 1, -1, 1, 0, -1, 0, 0, 0, 1, -1, 1, 0, -1, 0, 0, 0, 1, -1, 1, 0, -1, 0, 0, 0, 1, -1, 1, 0, -1, 0, 0, 0, 1]
SWOOSH Public Key 2: [12266, 11, 7, 12275, 12, 12285, 12286, 14, 12279, 0, 12288, 9, 12285, 5, 12284, 6, 3, 8, 12281, 0, 12286, 3, 12288, 12287, 1, 2, 4, 12281, 12284, 2, 12284, 9, 12284, 7, 12283, 5, 3, 12280, 1, 4, 6, 12287, 12282, 7, 3, 12286, 1, 12284, 1, 7, 12281, 2, 6, 12284, 0, 12286, 12285, 15, 12285, 12281, 15, 12277, 6, 12281, 12280, 6, 2, 1, 12287, 7, 12281, 3, 12287, 12286, 7, 12279, 2, 12282, 12287, 15, 12279, 14, 12272, 8, 3, 12284, 10, 12286, 12279, 9, 6, 12279, 10, 12276, 4, 4, 12274, 16, 12285, 12283, 12287, 2, 1, 8, 12287, 13, 12271, 1, 2, 12282, 14, 12286, 12284, 7, 12288, 12287, 2, 12278, 18, 12281, 12285, 12288, 3, 12285, 12285, 6, 4, 0]
Deriving shared keys...
SWOOSH Shared Key 1: ([1, 0, 1, 1, 0, 1, 0, 1], [12253, 151, 12179, 12244, 116, 12074, 270, 12158])
SWOOSH Shared Key 2: ([0, 1, 0, 1, 1, 1, 0, 1], [89, 12236, 25, 12257, 12280, 12280, 79, 12199])
WARNING: SWOOSH shared keys do not match!
SWOOSH Private key length: 128 integers
SWOOSH Public key length: 128 integers
SWOOSH Shared key length: 2 integers
Upvotes: 0
Views: 107
Reputation: 31
Thanks for your interest in our paper — I'm one of the authors.
I've taken a look at your code, and while I haven't gone through every detail, I can offer a few observations that might help.
First off, the parameters you've selected for N and D are much too small, which means there is no security in your current implementation (ignoring any side-channels, etc). I'm assuming that this is for demonstration or educational purposes rather than practical use.
More critically, the parameters you've chosen lead to a significant correctness error. As mentioned in Lemma 4 of our paper, the probability that the keys generated by the protocol do not match is given by:
\frac{4 * \beta^2 * D^2 * N}{q}
For the parameters you've selected:
\frac{4 * 1^2 * 8^2 * 16}{12289} \approx 0.33
This means there's roughly a 33% chance that your keys won't match, even if everything else is implemented correctly.
Another issue I noticed is the way you're sampling coefficients. Currently, you're using a uniform distribution where each coefficient takes the value -1, 0, or 1 with equal probability. However, what you should be using is a centred binomial distribution where p(-1) = p(1) = 0.25 and p(0) = 0.5.
Upvotes: 3