Skip to content

Commit

Permalink
Added scaffolding to get work started on ML-DSA. (#289)
Browse files Browse the repository at this point in the history
  • Loading branch information
xvzcf authored May 23, 2024
1 parent 29148ac commit 3f14979
Show file tree
Hide file tree
Showing 22 changed files with 4,352 additions and 4 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ members = [
"polynomials-avx2",
"traits",
"polynomials-aarch64",
"libcrux-ml-dsa",
]

[workspace.package]
Expand Down
10 changes: 8 additions & 2 deletions hax-driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,14 @@ def shell(command, expect=0, cwd=None, env={}):
# Extract both `libcrux` and `libcrux-platform`
shell(
[
"cargo", "hax",
"-C", "-p", "libcrux", "-p", "libcrux-platform", ";",
"cargo",
"hax",
"-C",
"-p",
"libcrux",
"-p",
"libcrux-platform",
";",
"into",
"-i",
f"-** +libcrux::kem::kyber::** +!libcrux_platform::platform::* {exclude_sha3_implementations} -libcrux::**::types::index_impls::**",
Expand Down
18 changes: 18 additions & 0 deletions libcrux-ml-dsa/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[package]
name = "libcrux-ml-dsa"
version.workspace = true
authors.workspace = true
license.workspace = true
homepage.workspace = true
edition.workspace = true
repository.workspace = true
readme.workspace = true

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]

[dev-dependencies]
hex = { version = "0.4.3", features = ["serde"] }
serde_json = { version = "1.0" }
serde = { version = "1.0", features = ["derive"] }
3 changes: 3 additions & 0 deletions libcrux-ml-dsa/src/constants.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
pub(crate) const FIELD_MODULUS_MINUS_ONE_BIT_LENGTH: usize = 23;

pub(crate) const DROPPED_BITS_FROM_T: usize = 13;
4 changes: 4 additions & 0 deletions libcrux-ml-dsa/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
mod constants;
mod ml_dsa_generic;

pub mod ml_dsa_65;
27 changes: 27 additions & 0 deletions libcrux-ml-dsa/src/ml_dsa_65.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
use crate::constants::*;

// ML-DSA-65 parameters

const ROWS_IN_A: usize = 6;
const COLUMNS_IN_A: usize = 5;

const PUBLIC_KEY_SIZE: usize =
32 + (32 * ROWS_IN_A * (FIELD_MODULUS_MINUS_ONE_BIT_LENGTH - DROPPED_BITS_FROM_T));
const SECRET_KEY_SIZE: usize =
(32 + 32 + 64) + 32 * (((ROWS_IN_A + COLUMNS_IN_A) * 4) + (DROPPED_BITS_FROM_T * ROWS_IN_A));

pub struct MLDSA65KeyPair {
pub secret_key: [u8; SECRET_KEY_SIZE],
pub public_key: [u8; PUBLIC_KEY_SIZE],
}

/// Generate an ML-DSA-65 Key Pair
pub fn generate_key_pair(randomness: [u8; 32]) -> MLDSA65KeyPair {
let (secret_key, public_key) =
crate::ml_dsa_generic::generate_key_pair::<SECRET_KEY_SIZE, PUBLIC_KEY_SIZE>(randomness);

MLDSA65KeyPair {
secret_key,
public_key,
}
}
6 changes: 6 additions & 0 deletions libcrux-ml-dsa/src/ml_dsa_generic.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
pub(crate) fn generate_key_pair<const SECRET_KEY_SIZE: usize, const PUBLIC_KEY_SIZE: usize>(
randomness: [u8; 32],
) -> ([u8; SECRET_KEY_SIZE], [u8; PUBLIC_KEY_SIZE]) {
let _ = randomness;
todo!();
}
3 changes: 3 additions & 0 deletions libcrux-ml-dsa/tests/kats/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
In order to regenerate the JSON KAT files for all parameter sets, simply run `./generate_kats.py`.

N.B.: The ML-DSA implementation was taken from https://github.com/GiacomoPope/dilithium-py/pull/1 with some modifications.
103 changes: 103 additions & 0 deletions libcrux-ml-dsa/tests/kats/aes256_ctr_drbg.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import os
from utils import xor_bytes
from Crypto.Cipher import AES


class AES256_CTR_DRBG:
def __init__(self, seed=None, personalization=b""):
self.seed_length = 48
self.reseed_interval = 2**48
self.key = bytes([0]) * 32
self.V = bytes([0]) * 16
self.entropy_input = self.__check_entropy_input(seed)

seed_material = self.__instantiate(personalization=personalization)
self.ctr_drbg_update(seed_material)
self.reseed_ctr = 1

def __check_entropy_input(self, entropy_input):
"""
If no entropy given, us os.urandom, else
check that the input is of the right length.
"""
if entropy_input is None:
return os.urandom(self.seed_length)
elif len(entropy_input) != self.seed_length:
raise ValueError(
f"The entropy input must be of length: {self.seed_length}. Input has length {len(entropy_input)}"
)
return entropy_input

def __instantiate(self, personalization=b""):
"""
Combine the input seed and optional personalisation
string into the seed material for the DRBG
"""
if len(personalization) > self.seed_length:
raise ValueError(
f"The Personalization String must be at most length: {self.seed_length}. Input has length {len(personalization)}"
)
elif len(personalization) < self.seed_length:
personalization += bytes([0]) * (self.seed_length - len(personalization))
# debugging
assert len(personalization) == self.seed_length
return xor_bytes(self.entropy_input, personalization)

def __increment_counter(self):
int_V = int.from_bytes(self.V, "big")
new_V = (int_V + 1) % 2 ** (8 * 16)
self.V = new_V.to_bytes(16, byteorder="big")

def ctr_drbg_update(self, provided_data):
tmp = b""
cipher = AES.new(self.key, AES.MODE_ECB)
# Collect bytes from AES ECB
while len(tmp) != self.seed_length:
self.__increment_counter()
tmp += cipher.encrypt(self.V)

# Take the first 48 bytes
tmp = tmp[: self.seed_length]
tmp = xor_bytes(tmp, provided_data)

# Set the new values of key and V
self.key = tmp[:32]
self.V = tmp[32:]

def reseed(self, additional_information=b""):
"""
Reseed the DRBG for when reseed_ctr hits the
limit.
"""
seed_material = self.__instantiate(additional_information)
self.ctr_drbg_update(seed_material)
self.reseed_ctr = 1

def random_bytes(self, num_bytes, additional=None):
if self.reseed_ctr >= self.reseed_interval:
raise Warning("The DRBG has been exhausted! Reseed!")

# Set the optional additional information
if additional is None:
additional = bytes([0]) * self.seed_length
else:
if len(additional) > self.seed_length:
raise ValueError(
f"The additional input must be of length at most: {self.seed_length}. Input has length {len(seed)}"
)
elif len(additional) < self.seed_length:
additional += bytes([0]) * (self.seed_length - len(additional))
self.ctr_drbg_update(additional)

# Collect bytes!
tmp = b""
cipher = AES.new(self.key, AES.MODE_ECB)
while len(tmp) < num_bytes:
self.__increment_counter()
tmp += cipher.encrypt(self.V)

# Collect only the requested number of bits
output_bytes = tmp[:num_bytes]
self.ctr_drbg_update(additional)
self.reseed_ctr += 1
return output_bytes
Loading

0 comments on commit 3f14979

Please sign in to comment.