Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added scaffolding to get work started on ML-DSA. #289

Merged
merged 4 commits into from
May 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading