import subprocess
import tempfile
import platform
import argparse
import shutil
import pathlib
import re
import sys
import threading
import pyparsing as pp
import os
import yaml
import time
from concurrent.futures import ThreadPoolExecutor
from functools import partial
from rich.console import Console
from rich.progress import (
Progress,
BarColumn,
TextColumn,
TaskProgressColumn,
TimeElapsedColumn,
)
console = Console()
_progress = None
_main_task = None
_current_task = ""
modulus = 8380417
root_of_unity = 1753
montgomery_factor = pow(2, 32, modulus)
_RE_DEFINED = re.compile(r"defined\(([^)]+)\)")
_RE_MARKDOWN_CITE = re.compile(r"\[\^(?P<id>\w+)\]")
_RE_C_CITE = re.compile(r"@\[(?P<id>\w+)")
_RE_BYTECODE_START = re.compile(
r"=== bytecode start: (?:aarch64|x86_64)/mldsa/([^/\s]+?)\.o"
)
_RE_FUNC_SYMBOL = re.compile(r"MLD_ASM_FN_SYMBOL\((.*)\)")
_RE_MACRO_CHECK = re.compile(r"[^_]((?:MLD_|MLDSA_)\w+)(.*)$", re.M)
_RE_MLKEM_MACRO_CHECK = re.compile(r"[^_]((?:MLK_|MLKEM_)\w+)(.*)$", re.M)
_RE_DEFINE = re.compile(r"^\s*#define\s+(\w+)")
_RE_ARGS_COMMENT = re.compile(r"(.*?)(\s*//.*)?$")
_RE_MACRO_DEF = re.compile(r"^\s*\.macro\s+(\w+)")
_RE_MACRO_DEF_ARGS = re.compile(r"^(\s*\.macro\s+\w+)(\s+.*)$")
_RE_LEADING_SPACE = re.compile(r"^(\s*)")
_file_cache = {}
_file_cache_lock = threading.Lock()
_errors = []
_errors_lock = threading.Lock()
_progress_lock = threading.Lock()
def read_file(filename, original=False):
with _file_cache_lock:
if filename in _file_cache:
key = "content" if original is False else "original"
return _file_cache[filename][key]
with open(filename, "r") as f:
content = f.read()
_file_cache[filename] = {
"content": content,
"original": content,
"force_format": False,
}
return content
def update_file(filename, content, force_format=False):
with _file_cache_lock:
if filename not in _file_cache:
try:
with open(filename, "r") as f:
original = f.read()
_file_cache[filename] = {"original": original}
except FileNotFoundError:
_file_cache[filename] = {"original": None}
e = _file_cache[filename]
e["content"] = content
e["force_format"] = e.get("force_format", False) or force_format
def finalize_format_batch(batch):
if not batch:
return
temp_files = []
try:
for filename in batch:
content = read_file(filename)
if content is None:
continue
with tempfile.NamedTemporaryFile(mode="w", suffix=".c", delete=False) as f:
f.write(content)
temp_files.append((f.name, filename))
clang_format_file = os.path.join(
os.path.dirname(__file__), "..", ".clang-format"
)
p = subprocess.run(
["clang-format", "-i", f"-style=file:{clang_format_file}"]
+ [t[0] for t in temp_files],
capture_output=True,
text=True,
)
if p.returncode != 0:
print(p.stderr)
print(
f"Failed to auto-format autogenerated code (clang-format return code {p.returncode}). Are you running in a nix shell? See CONTRIBUTING.md."
)
exit(1)
for temp_path, filename in temp_files:
with open(temp_path, "r") as f:
update_file(filename, f.read())
finally:
for temp_path, _ in temp_files:
os.unlink(temp_path)
def finalize_file(item, dry_run):
filename, data = item
content_old = data["original"]
content_new = data["content"]
if content_old == content_new:
return
if content_new is None:
if dry_run is False:
file_updated(filename, removed=True)
os.remove(filename)
else:
error(filename, None)
return
if dry_run is False:
file_updated(filename)
with open(filename, "w") as f:
f.write(content_new)
else:
filename_new = f"{filename}.new"
with open(filename_new, "w") as f:
f.write(content_new)
error(filename, filename_new)
def format_files(dry_run):
to_format = [
filename
for filename, data in _file_cache.items()
if data["force_format"] or filename.endswith((".c", ".h", ".i"))
]
batch_size = 20
batches = [
to_format[i : i + batch_size] for i in range(0, len(to_format), batch_size)
]
run_parallel(batches, finalize_format_batch)
def finalize(dry_run):
run_parallel(_file_cache.items(), partial(finalize_file, dry_run=dry_run))
_step_start_time = time.time()
def high_level_task(msg):
global _current_task
_current_task = msg
if _progress:
_progress.update(_main_task, description=f"[cyan]{msg}[/]")
def high_level_status(msg, skipped=False):
global _step_start_time
elapsed = time.time() - _step_start_time
if skipped:
symbol = "[dim]–[/dim]"
else:
symbol = "[green]✓[/green]"
if _progress:
_progress.print(f"{symbol} {msg} ({elapsed:.1f}s)", highlight=False)
_progress.advance(_main_task)
else:
console.print(f"{symbol} {msg} ({elapsed:.1f}s)", highlight=False)
_step_start_time = time.time()
def run_parallel(files, func):
if not files:
return []
files = list(files)
total = len(files)
state = {"completed": 0, "last_file": ""}
def update_progress():
if _progress and total > 0:
suffix = (
f" {os.path.basename(state['last_file'])}" if state["last_file"] else ""
)
_progress.update(
_main_task,
description=f"[cyan]{_current_task}[/] [dim][{state['completed']}/{total}]{suffix}[/]",
)
def wrapped(f):
result = func(f)
with _progress_lock:
state["completed"] += 1
state["last_file"] = str(f[0]) if isinstance(f, tuple) else str(f)
update_progress()
return result
with ThreadPoolExecutor() as executor:
return list(executor.map(wrapped, files))
def error(filename, filename_new):
with _errors_lock:
_errors.append((filename, filename_new))
def print_check_errors():
for filename, filename_new in _errors:
console.print(f"[red]error[/] {filename}")
if filename_new is not None:
console.print(
f"Autogenerated file {filename} needs updating. Have you called scripts/autogen? Wrote new version to {filename_new}."
)
if os.path.exists(filename):
subprocess.run(["diff", filename, filename_new])
else:
console.print(
f"Autogenerated file {filename} needs removing. Have you called scripts/autogen?"
)
return len(_errors) == 0
def file_updated(filename, removed=False):
if removed is False:
console.print(f"[bold]updated {filename}[/]")
else:
console.print(f"[bold]removed {filename}[/]")
def gen_autogen_warning():
yield ""
yield "/*"
yield " * WARNING: This file is auto-generated from scripts/autogen"
yield " * in the mldsa-native repository."
yield " * Do not modify it directly."
yield " */"
def gen_header():
yield "/*"
yield " * Copyright (c) The mldsa-native project authors"
yield " * SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT"
yield " */"
yield from gen_autogen_warning()
yield ""
def gen_hol_light_header():
yield "(*"
yield " * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved."
yield " * SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT-0"
yield " *)"
yield ""
yield "(*"
yield " * WARNING: This file is auto-generated from scripts/autogen"
yield " * in the mldsa-native repository."
yield " * Do not modify it directly."
yield " *)"
yield ""
def gen_yaml_header():
yield "# Copyright (c) The mldsa-native project authors"
yield "# SPDX-License-Identifier: Apache-2.0 OR ISC OR MIT"
yield ""
def gen_yaml_autogen_warning():
yield "# WARNING: This file is auto-generated from scripts/autogen"
yield "# in the mldsa-native repository."
yield "# Do not modify it directly."
def format_content(content):
clang_format_file = os.path.join(os.path.dirname(__file__), "..", ".clang-format")
p = subprocess.run(
["clang-format", f"-style=file:{clang_format_file}"],
capture_output=True,
input=content,
text=True,
)
if p.returncode != 0:
print(p.stderr)
print(
f"Failed to auto-format autogenerated code (clang-format return code {p.returncode}). Are you running in a nix shell? See CONTRIBUTING.md."
)
exit(1)
return p.stdout
class CondParser:
def __init__(self):
c_identifier = pp.common.identifier()
c_integer_suffix = pp.one_of("U L LU UL LL ULL LLU", caseless=True)
c_dec_integer = pp.Combine(
pp.Optional(pp.one_of("+ -"))
+ pp.Word(pp.nums)
+ pp.Optional(c_integer_suffix)
)
c_hex_integer = pp.Combine(
pp.Literal("0x") + pp.Word(pp.hexnums) + pp.Optional(c_integer_suffix)
)
self.parser = pp.infix_notation(
c_identifier | c_hex_integer | c_dec_integer,
[
(pp.one_of("!"), 1, pp.opAssoc.RIGHT),
(pp.one_of("!= == <= >= > <"), 2, pp.opAssoc.LEFT),
(pp.one_of("&&"), 2, pp.opAssoc.LEFT),
(pp.one_of("||"), 2, pp.opAssoc.LEFT),
],
)
@staticmethod
def connective(res):
if not isinstance(res, list):
return None
elif len(res) == 2:
return res[0]
else:
return res[1]
@staticmethod
def map_top(f, res):
if not isinstance(res, list):
return res
else:
return list(map(f, res))
@staticmethod
def args(res):
return res[::2]
@staticmethod
def simplify_double_negation(res):
if CondParser.connective(res) == "!" and CondParser.connective(res[1]) == "!":
res = res[1][1]
res = CondParser.map_top(CondParser.simplify_double_negation, res)
return res
@staticmethod
def simplify_not_eq(res):
if CondParser.connective(res) == "!" and CondParser.connective(res[1]) == "==":
res = res[1]
res[1] = "!="
if CondParser.connective(res) == "!" and CondParser.connective(res[1]) == "!=":
res = res[1]
res[1] = "=="
res = CondParser.map_top(CondParser.simplify_not_eq, res)
return res
@staticmethod
def simplify_neq_chain(res):
if (
CondParser.connective(res) == "&&"
and CondParser.connective(res[-1]) == "=="
):
lhs = res[-1][0]
rhs = res[-1][2]
args = []
for a in CondParser.args(res[:-1]):
if CondParser.connective(a) == "!=" and a[0] == lhs:
args.append(a[2])
else:
args = None
break
if args is None:
return res
if rhs.isdigit() and all(
map(lambda a: a.isdigit() and int(a) != int(rhs), args)
):
return res[-1]
res = CondParser.map_top(CondParser.simplify_neq_chain, res)
return res
@staticmethod
def print_exp(exp, inner=False):
conn = CondParser.connective(exp)
if conn is None:
return exp
elif conn == "!":
res = f"!{CondParser.print_exp(exp[1], inner=True)}"
else:
padded_conn = f" {conn} "
res = padded_conn.join(
map(lambda e: CondParser.print_exp(e, inner=True), CondParser.args(exp))
)
if inner is True and conn in ["&&", "||"]:
res = f"({res})"
return res
def simplify_assoc(exp):
conn = CondParser.connective(exp)
if conn in ["&&", "||"]:
args = CondParser.args(exp)
new_args = []
for a in args:
if CondParser.connective(a) == conn:
new_args += CondParser.args(a)
else:
new_args.append(a)
exp = [x for y in map(lambda x: [x, conn], new_args) for x in y][:-1]
exp = CondParser.map_top(CondParser.simplify_assoc, exp)
return exp
def simplify_all(exp):
exp = CondParser.simplify_double_negation(exp)
exp = CondParser.simplify_not_eq(exp)
exp = CondParser.simplify_neq_chain(exp)
exp = CondParser.simplify_assoc(exp)
return exp
def parse_condition(self, exp, simplify=True):
try:
exp = self.parser.parse_string(exp, parse_all=True).as_list()[0]
except pp.ParseException:
print(f"WARNING: Ignoring condition '{exp}' I cannot parse")
return exp
if simplify is True:
exp = CondParser.simplify_all(exp)
return exp
def normalize_condition(self, exp):
return CondParser.print_exp(self.parse_condition(exp))
def adjust_preprocessor_comments_for_filename(
content, source_file, parser, show_status=False
):
content = content.split("\n")
new_content = []
if_stack = []
def merge_escaped_lines(line, i):
while line.endswith("\\"):
line = line.removesuffix("\\").rstrip() + content[i + 1].lstrip()
i = i + 1
return (line, i)
def merge_commented_lines(line, i):
if "/*" not in line or "*/" in line:
return (line, i)
i += 1
while "*/" not in content[i]:
line += content[i]
i += 1
line += content[i]
return (line, i)
def should_print(cur_line_no, conds, line_no, force_print):
line_threshold = 5
if force_print is True:
return True
if cur_line_no - line_no >= line_threshold:
return True
return False
def format_condition(cond):
cond = _RE_DEFINED.sub(r"\1", cond)
return parser.normalize_condition(cond)
def format_conditions(conds, branch):
prev_conds = list(map(lambda s: f"!({s})", conds[:-1]))
final_cond = conds[-1]
if branch is False:
final_cond = f"!({final_cond})"
full_cond = "&&".join(prev_conds + [final_cond])
return format_condition(full_cond)
def wrap_long_directive(directive, condition, max_len=80):
single_line = directive + " " + condition
if len(single_line) <= max_len:
return single_line
words = condition.split()
lines = []
current = f"{directive} "
indent = (len(directive) + 4) * " "
indent_final = (len(directive) + 2) * " "
for word in words:
if len(current) + len(word) + 1 <= max_len:
current += word + " "
else:
lines.append(current.rstrip() + " \\")
if word == "*/":
current = indent_final + word
else:
current = indent + word + " "
lines.append(current.rstrip())
return "\n".join(lines)
def adhoc_format(directive, content):
if not source_file.endswith(".S"):
return directive + " /* " + content + " */"
return wrap_long_directive(directive, "/* " + content + " */")
i = 0
while i < len(content):
line = content[i].strip()
if line.startswith("#ifdef "):
line = "#if defined(" + line.removeprefix("#ifdef").strip() + ")"
if line.startswith("#ifndef "):
line = "#if !defined(" + line.removeprefix("#ifndef").strip() + ")"
if line.startswith("#if"):
line, _ = merge_escaped_lines(line, i)
cond = line.removeprefix("#if")
if_stack.append(([cond], i, True, False))
new_content.append(content[i])
elif line.startswith("#elif"):
conds, _, _, force_print = if_stack.pop()
line, _ = merge_escaped_lines(line, i)
conds.append(line.removeprefix("#elif"))
if_stack.append((conds, i, True, force_print))
new_content.append(content[i])
elif line.startswith("#else"):
line, i = merge_escaped_lines(line, i)
_, i = merge_commented_lines(line, i)
conds, j, branch, force_print = if_stack.pop()
assert branch is True
print_else = should_print(i, cond, j, force_print)
if_stack.append((conds, i, False, print_else))
if print_else is True:
cond = format_conditions(conds, True)
new_content.append(adhoc_format("#else", cond))
else:
new_content.append("#else")
elif line.startswith("#endif"):
line, i = merge_escaped_lines(line, i)
_, i = merge_commented_lines(line, i)
conds, j, branch, force_print = if_stack.pop()
print_endif = should_print(i, conds, j, force_print)
if print_endif is False:
new_content.append("#endif")
else:
cond = format_conditions(conds, branch)
new_content.append(adhoc_format("#endif", cond))
else:
i_old = i
_, i = merge_commented_lines(line, i_old)
new_content += content[i_old : i + 1]
i += 1
return "\n".join(new_content)
def gen_preprocessor_comments_for(parser, source_file):
content = read_file(source_file)
new_content = adjust_preprocessor_comments_for_filename(
content, source_file, parser, show_status=True
)
update_file(source_file, new_content)
def gen_preprocessor_comments():
files = get_c_source_files() + get_asm_source_files() + get_header_files()
parser = CondParser()
run_parallel(files, partial(gen_preprocessor_comments_for, parser))
def bitreverse(i, n):
r = 0
for _ in range(n):
r = 2 * r + (i & 1)
i >>= 1
return r
def signed_reduce(a):
c = a % modulus
if c >= modulus / 2:
c -= modulus
return c
def gen_c_zetas():
zeta = [0] for i in range(1, 256):
zeta.append(signed_reduce(pow(root_of_unity, i, modulus) * montgomery_factor))
yield from (zeta[bitreverse(i, 8)] for i in range(256))
def gen_c_zeta_file():
def gen():
yield from gen_header()
yield ""
yield "/*"
yield " * Table of zeta values used in the reference NTT and inverse NTT."
yield " * See autogen for details."
yield " */"
yield "static const int32_t mld_zetas[MLDSA_N] = {"
yield from map(lambda t: str(t) + ",", gen_c_zetas())
yield "};"
yield ""
update_file("mldsa/src/zetas.inc", "\n".join(gen()), force_format=True)
def prepare_root_for_barrett(root):
root = signed_reduce(root)
def round_to_even(t):
rt = round(t)
if rt % 2 == 0:
return rt
if rt <= t:
return rt + 1
return rt - 1
root_twisted = round_to_even((root * 2**32) / modulus) // 2
return root, root_twisted
def gen_aarch64_root_of_unity_for_block(layer, block, inv=False, scale=False):
log = bitreverse(pow(2, layer) + block, 8)
if inv is True:
log = -log
root = pow(root_of_unity, log, modulus)
if scale is True:
root = root * pow(2, 32 - 8, modulus)
root, root_twisted = prepare_root_for_barrett(root)
return root, root_twisted
def gen_aarch64_fwd_ntt_zetas_layer123456():
yield from gen_aarch64_root_of_unity_for_block(0, 0)
yield from gen_aarch64_root_of_unity_for_block(1, 0)
yield from gen_aarch64_root_of_unity_for_block(1, 1)
yield from gen_aarch64_root_of_unity_for_block(2, 0)
yield from gen_aarch64_root_of_unity_for_block(2, 1)
yield from gen_aarch64_root_of_unity_for_block(2, 2)
yield from gen_aarch64_root_of_unity_for_block(2, 3)
yield from (0, 0)
for block in range(8): yield from gen_aarch64_root_of_unity_for_block(3, block)
yield from gen_aarch64_root_of_unity_for_block(4, 2 * block + 0)
yield from gen_aarch64_root_of_unity_for_block(4, 2 * block + 1)
yield from gen_aarch64_root_of_unity_for_block(5, 4 * block + 0)
yield from gen_aarch64_root_of_unity_for_block(5, 4 * block + 1)
yield from gen_aarch64_root_of_unity_for_block(5, 4 * block + 2)
yield from gen_aarch64_root_of_unity_for_block(5, 4 * block + 3)
yield from (0, 0)
def gen_aarch64_fwd_ntt_zetas_layer78():
for block in range(8):
for i in range(2):
yield gen_aarch64_root_of_unity_for_block(6, 8 * block + 0)[i]
yield gen_aarch64_root_of_unity_for_block(6, 8 * block + 1)[i]
yield gen_aarch64_root_of_unity_for_block(6, 8 * block + 2)[i]
yield gen_aarch64_root_of_unity_for_block(6, 8 * block + 3)[i]
for i in range(2):
yield gen_aarch64_root_of_unity_for_block(7, 16 * block + 0)[i]
yield gen_aarch64_root_of_unity_for_block(7, 16 * block + 2)[i]
yield gen_aarch64_root_of_unity_for_block(7, 16 * block + 4)[i]
yield gen_aarch64_root_of_unity_for_block(7, 16 * block + 6)[i]
for i in range(2):
yield gen_aarch64_root_of_unity_for_block(7, 16 * block + 1)[i]
yield gen_aarch64_root_of_unity_for_block(7, 16 * block + 3)[i]
yield gen_aarch64_root_of_unity_for_block(7, 16 * block + 5)[i]
yield gen_aarch64_root_of_unity_for_block(7, 16 * block + 7)[i]
for i in range(2):
yield gen_aarch64_root_of_unity_for_block(6, 8 * block + 0 + 4)[i]
yield gen_aarch64_root_of_unity_for_block(6, 8 * block + 1 + 4)[i]
yield gen_aarch64_root_of_unity_for_block(6, 8 * block + 2 + 4)[i]
yield gen_aarch64_root_of_unity_for_block(6, 8 * block + 3 + 4)[i]
for i in range(2):
yield gen_aarch64_root_of_unity_for_block(7, 16 * block + 0 + 8)[i]
yield gen_aarch64_root_of_unity_for_block(7, 16 * block + 2 + 8)[i]
yield gen_aarch64_root_of_unity_for_block(7, 16 * block + 4 + 8)[i]
yield gen_aarch64_root_of_unity_for_block(7, 16 * block + 6 + 8)[i]
for i in range(2):
yield gen_aarch64_root_of_unity_for_block(7, 16 * block + 1 + 8)[i]
yield gen_aarch64_root_of_unity_for_block(7, 16 * block + 3 + 8)[i]
yield gen_aarch64_root_of_unity_for_block(7, 16 * block + 5 + 8)[i]
yield gen_aarch64_root_of_unity_for_block(7, 16 * block + 7 + 8)[i]
def gen_aarch64_intt_zetas_layer78():
for block in range(16):
for i in range(2):
yield gen_aarch64_root_of_unity_for_block(6, block * 4 + 0, inv=True)[i]
yield gen_aarch64_root_of_unity_for_block(6, block * 4 + 1, inv=True)[i]
yield gen_aarch64_root_of_unity_for_block(6, block * 4 + 2, inv=True)[i]
yield gen_aarch64_root_of_unity_for_block(6, block * 4 + 3, inv=True)[i]
for i in range(2):
yield gen_aarch64_root_of_unity_for_block(7, block * 8 + 0, inv=True)[i]
yield gen_aarch64_root_of_unity_for_block(7, block * 8 + 2, inv=True)[i]
yield gen_aarch64_root_of_unity_for_block(7, block * 8 + 4, inv=True)[i]
yield gen_aarch64_root_of_unity_for_block(7, block * 8 + 6, inv=True)[i]
for i in range(2):
yield gen_aarch64_root_of_unity_for_block(7, block * 8 + 1, inv=True)[i]
yield gen_aarch64_root_of_unity_for_block(7, block * 8 + 3, inv=True)[i]
yield gen_aarch64_root_of_unity_for_block(7, block * 8 + 5, inv=True)[i]
yield gen_aarch64_root_of_unity_for_block(7, block * 8 + 7, inv=True)[i]
def gen_aarch64_intt_zetas_layer123456():
for i in range(16):
yield from gen_aarch64_root_of_unity_for_block(4, i, inv=True)
yield from (0, 0) yield from gen_aarch64_root_of_unity_for_block(5, i * 2, inv=True)
yield from gen_aarch64_root_of_unity_for_block(5, i * 2 + 1, inv=True)
yield from gen_aarch64_root_of_unity_for_block(0, 0, inv=True, scale=True)
yield from gen_aarch64_root_of_unity_for_block(1, 0, inv=True)
yield from gen_aarch64_root_of_unity_for_block(1, 1, inv=True)
yield from gen_aarch64_root_of_unity_for_block(2, 0, inv=True)
yield from gen_aarch64_root_of_unity_for_block(2, 1, inv=True)
yield from gen_aarch64_root_of_unity_for_block(2, 2, inv=True)
yield from gen_aarch64_root_of_unity_for_block(2, 3, inv=True)
yield from gen_aarch64_root_of_unity_for_block(3, 0, inv=True)
yield from gen_aarch64_root_of_unity_for_block(3, 1, inv=True)
yield from gen_aarch64_root_of_unity_for_block(3, 2, inv=True)
yield from gen_aarch64_root_of_unity_for_block(3, 3, inv=True)
yield from gen_aarch64_root_of_unity_for_block(3, 4, inv=True)
yield from gen_aarch64_root_of_unity_for_block(3, 5, inv=True)
yield from gen_aarch64_root_of_unity_for_block(3, 6, inv=True)
yield from gen_aarch64_root_of_unity_for_block(3, 7, inv=True)
yield from (0, 0)
def print_hol_light_array(g, as_int=True, entries_per_line=8, pad=0):
def format_hol_light_int(n):
prefix = ""
if n < 0:
prefix = "-- "
n = -n
c = "&" if as_int is True else ""
return f"{prefix}{c}{n:>{pad}};"
items = list(map(format_hol_light_int, g))
items[-1] = items[-1][:-1]
for i in range(0, len(items), entries_per_line):
yield " " + " ".join(items[i : i + entries_per_line])
def emit_c_array(type_str, name, data, formatter=None, suffix=""):
data = list(data)
if data and isinstance(data[0], (list, tuple)):
count = sum(len(row) for row in data)
else:
count = len(data)
yield f"MLD_ALIGN MLD_INTERNAL_DATA_DEFINITION {type_str}"
yield f" {name}[{count}]{suffix} = {{"
if formatter is not None:
yield from formatter(data)
else:
yield from (f"{elem}," for elem in data)
yield "};"
def emit_c_array_decl(type_str, name, count, suffix=""):
return f"MLD_INTERNAL_DATA_DECLARATION {type_str} {name}[{count}]{suffix};"
def _fmt_indexed_rows(data):
for i, row in enumerate(data):
yield ",".join(map(str, row)) + f" /* {i} */,"
def gen_aarch64_zeta_file():
def gen():
yield from gen_header()
yield '#include "../../../common.h"'
yield ""
yield "#if defined(MLD_ARITH_BACKEND_AARCH64) && \\"
yield " !defined(MLD_CONFIG_MULTILEVEL_NO_SHARED)"
yield ""
yield '#include "arith_native_aarch64.h"'
yield ""
yield "/*"
yield " * Table of zeta values used in the AArch64 forward NTT"
yield " * See autogen for details."
yield " */"
yield from emit_c_array(
"const int32_t",
"mld_aarch64_ntt_zetas_layer123456",
gen_aarch64_fwd_ntt_zetas_layer123456(),
)
yield ""
yield from emit_c_array(
"const int32_t",
"mld_aarch64_ntt_zetas_layer78",
gen_aarch64_fwd_ntt_zetas_layer78(),
)
yield ""
yield from emit_c_array(
"const int32_t",
"mld_aarch64_intt_zetas_layer78",
gen_aarch64_intt_zetas_layer78(),
)
yield ""
yield from emit_c_array(
"const int32_t",
"mld_aarch64_intt_zetas_layer123456",
gen_aarch64_intt_zetas_layer123456(),
)
yield ""
yield "#else"
yield ""
yield "MLD_EMPTY_CU(aarch64_zetas)"
yield ""
yield "#endif"
yield ""
update_file("dev/aarch64_opt/src/aarch64_zetas.c", "\n".join(gen()))
update_file("dev/aarch64_clean/src/aarch64_zetas.c", "\n".join(gen()))
def gen_aarch64_rej_uniform_eta_table_rows():
def get_set_bits_idxs(i):
bits = list(map(int, format(i, "08b")))
bits.reverse()
return [bit_idx for bit_idx in range(8) if bits[bit_idx] == 1]
for i in range(256):
idxs = get_set_bits_idxs(i)
idxs = [j for i in idxs for j in [2 * i, 2 * i + 1]]
idxs = idxs + [255] * (16 - len(idxs))
yield idxs
def gen_aarch64_rej_uniform_eta_table():
def gen():
yield from gen_header()
yield '#include "../../../common.h"'
yield ""
yield "#if defined(MLD_ARITH_BACKEND_AARCH64) && \\"
yield " !defined(MLD_CONFIG_NO_KEYPAIR_API) && \\"
yield " !defined(MLD_CONFIG_MULTILEVEL_NO_SHARED)"
yield ""
yield '#include "arith_native_aarch64.h"'
yield ""
yield "/*"
yield " * Lookup table used by 16-bit rejection sampling (rej_eta)."
yield " * Adapted from ML-KEM for ML-DSA eta rejection sampling."
yield " * See autogen for details."
yield " */"
yield from emit_c_array(
"const uint8_t",
"mld_rej_uniform_eta_table",
gen_aarch64_rej_uniform_eta_table_rows(),
formatter=_fmt_indexed_rows,
)
yield ""
yield ("#else /* MLD_ARITH_BACKEND_AARCH64 && !MLD_CONFIG_NO_KEYPAIR_API && \\")
yield " !MLD_CONFIG_MULTILEVEL_NO_SHARED */"
yield ""
yield "MLD_EMPTY_CU(aarch64_rej_uniform_eta_table)"
yield ""
yield (
"#endif /* !(MLD_ARITH_BACKEND_AARCH64 && !MLD_CONFIG_NO_KEYPAIR_API && \\"
)
yield " !MLD_CONFIG_MULTILEVEL_NO_SHARED) */"
yield ""
update_file("dev/aarch64_opt/src/rej_uniform_eta_table.c", "\n".join(gen()))
update_file("dev/aarch64_clean/src/rej_uniform_eta_table.c", "\n".join(gen()))
def gen_aarch64_hol_light_rej_uniform_eta_table():
yield from gen_hol_light_header()
yield "(*"
yield " * Constant table values used in the AArch64 eta rejection sampling."
yield " * Each entry is 16 bytes. There are 256 entries (one per 8-bit mask),"
yield " * for a total of 4096 bytes. Entries use 2-byte (16-bit) coefficient"
yield " * indices since ML-DSA eta samples are stored as 16-bit halfwords"
yield " * on the stack before being converted to 32-bit in the final copy."
yield " * See autogen for details."
yield " *)"
yield ""
yield "let mldsa_rej_uniform_eta_table = (REWRITE_RULE[MAP] o define)"
yield " `mldsa_rej_uniform_eta_table:byte list = MAP word ["
data = [v for row in gen_aarch64_rej_uniform_eta_table_rows() for v in row]
yield from print_hol_light_array(data, as_int=False, entries_per_line=16, pad=3)
yield " ]`;;"
yield ""
update_file(
"proofs/hol_light/aarch64/proofs/mldsa_rej_uniform_eta_table.ml",
"\n".join(gen_aarch64_hol_light_rej_uniform_eta_table()),
)
def gen_aarch64_rej_uniform_table_rows():
def get_set_bits_idxs(i):
bits = list(map(int, format(i, "08b")))
bits.reverse()
return [bit_idx for bit_idx in range(8) if bits[bit_idx] == 1]
for i in range(16):
idxs = get_set_bits_idxs(i)
idxs = [j for i in idxs for j in [4 * i + k for k in range(4)]]
idxs = idxs + [255] * (16 - len(idxs))
yield idxs
def gen_aarch64_rej_uniform_table():
def gen():
yield from gen_header()
yield '#include "../../../common.h"'
yield ""
yield "#if defined(MLD_ARITH_BACKEND_AARCH64) && \\"
yield " !defined(MLD_CONFIG_MULTILEVEL_NO_SHARED)"
yield ""
yield '#include "arith_native_aarch64.h"'
yield ""
yield "/*"
yield " * Lookup table used by rejection sampling of the public matrix."
yield " * See autogen for details."
yield " */"
yield from emit_c_array(
"const uint8_t",
"mld_rej_uniform_table",
gen_aarch64_rej_uniform_table_rows(),
formatter=_fmt_indexed_rows,
)
yield ""
yield "#else"
yield ""
yield "MLD_EMPTY_CU(aarch64_rej_uniform_table)"
yield ""
yield "#endif"
yield ""
update_file("dev/aarch64_opt/src/rej_uniform_table.c", "\n".join(gen()))
update_file("dev/aarch64_clean/src/rej_uniform_table.c", "\n".join(gen()))
def gen_aarch64_polyz_unpack_indices(bit_width):
v2_shift = 48 - 2 * bit_width
for group in range(4):
base_coeff = group * 4
reg_offset = 0 if group < 2 else 16
for coeff in range(4):
i = base_coeff + coeff
byte_start = (i * bit_width) // 8 - reg_offset
for j in range(3):
b = byte_start + j
if group >= 2 and b >= 16:
b += v2_shift
yield b
yield 255
def gen_aarch64_polyz_unpack_table():
param_set_guards = {
17: "#if defined(MLD_CONFIG_MULTILEVEL_WITH_SHARED) || MLD_CONFIG_PARAMETER_SET == 44",
19: (
"#if defined(MLD_CONFIG_MULTILEVEL_WITH_SHARED) || \\\n"
" (MLD_CONFIG_PARAMETER_SET == 65 || MLD_CONFIG_PARAMETER_SET == 87)"
),
}
def format_row(vals):
return ", ".join(f"{v:>3}" for v in vals) + ","
def gen():
yield from gen_header()
yield '#include "../../../common.h"'
yield ""
yield "#if defined(MLD_ARITH_BACKEND_AARCH64) && \\"
yield " !defined(MLD_CONFIG_MULTILEVEL_NO_SHARED)"
yield ""
yield '#include "arith_native_aarch64.h"'
yield ""
yield "#if !defined(MLD_CONFIG_NO_SIGN_API) || !defined(MLD_CONFIG_NO_VERIFY_API)"
yield "/* Table of indices used for tbl instructions in polyz_unpack_{17,19}."
yield " * See autogen for details. */"
yield ""
for gamma1_bits in [17, 19]:
bit_width = gamma1_bits + 1
indices = list(gen_aarch64_polyz_unpack_indices(bit_width))
yield param_set_guards[gamma1_bits]
def _fmt_polyz(data, _fr=format_row):
for row_start in range(0, len(data), 16):
yield " " + _fr(data[row_start : row_start + 16])
yield from emit_c_array(
"const uint8_t",
f"mld_polyz_unpack_{gamma1_bits}_indices",
indices,
formatter=_fmt_polyz,
)
yield "#endif"
yield ""
yield "#endif /* !MLD_CONFIG_NO_SIGN_API || !MLD_CONFIG_NO_VERIFY_API */"
yield ""
yield "#else /* MLD_ARITH_BACKEND_AARCH64 && !MLD_CONFIG_MULTILEVEL_NO_SHARED */"
yield ""
yield "MLD_EMPTY_CU(aarch64_polyz_unpack_table)"
yield ""
yield "#endif /* !(MLD_ARITH_BACKEND_AARCH64 && !MLD_CONFIG_MULTILEVEL_NO_SHARED) */"
yield ""
update_file("dev/aarch64_opt/src/polyz_unpack_table.c", "\n".join(gen()))
update_file("dev/aarch64_clean/src/polyz_unpack_table.c", "\n".join(gen()))
def gen_hol_light_rej_uniform_table():
def gen():
yield from gen_hol_light_header()
yield "(*"
yield " * Constant table values used in the AArch64 rejection sampling."
yield " * Each entry is 16 bytes. There are 16 entries (one per 4-bit mask),"
yield " * for a total of 256 bytes. Entries use 4-byte (32-bit) coefficient"
yield " * indices since ML-DSA coefficients are 32-bit."
yield " * See autogen for details."
yield " *)"
yield ""
rows = list(gen_aarch64_rej_uniform_table_rows())
flat = [v for row in rows for v in row]
yield "let mldsa_rej_uniform_table = (REWRITE_RULE[MAP] o define)"
yield " `mldsa_rej_uniform_table:byte list = MAP word ["
yield from print_hol_light_array(flat, as_int=False, pad=3, entries_per_line=16)
yield " ]`;;"
yield ""
update_file(
"proofs/hol_light/aarch64/proofs/mldsa_rej_uniform_table.ml",
"\n".join(gen()),
)
def gen_hol_light_polyz_unpack_table():
def gen():
yield from gen_hol_light_header()
yield "(*"
yield " * TBL index tables for polyz_unpack_{17,19}"
yield " * See autogen for details."
yield " *)"
yield ""
for gamma1_bits in [17, 19]:
bit_width = gamma1_bits + 1
name = f"mldsa_polyz_unpack_{gamma1_bits}_indices"
indices = list(gen_aarch64_polyz_unpack_indices(bit_width))
yield f"let {name} = (REWRITE_RULE[MAP] o define)"
yield f" `{name}:byte list = MAP word ["
yield from print_hol_light_array(indices, as_int=False, pad=3)
yield "]`;;"
yield ""
update_file(
"proofs/hol_light/aarch64/proofs/mldsa_polyz_unpack_consts.ml",
"\n".join(gen()),
)
def gen_avx2_rej_uniform_table_rows():
def get_set_bits_idxs(i):
bits = list(map(int, format(i, "08b")))
bits.reverse()
return [bit_idx for bit_idx in range(8) if bits[bit_idx] == 1]
for i in range(256):
idxs = get_set_bits_idxs(i)
idxs = [i for i in idxs]
idxs = idxs + [0] * (8 - len(idxs))
yield "{" + ",".join(map(str, idxs)) + "}"
def gen_avx2_rej_uniform_table():
def gen():
yield from gen_header()
yield '#include "../../../common.h"'
yield ""
yield "#if defined(MLD_ARITH_BACKEND_X86_64_DEFAULT) && \\"
yield " !defined(MLD_CONFIG_MULTILEVEL_NO_SHARED)"
yield ""
yield '#include "arith_native_x86_64.h"'
yield ""
yield "/*"
yield " * Lookup table used by rejection sampling."
yield " * See autogen for details."
yield " */"
yield from emit_c_array(
"const uint8_t",
"mld_rej_uniform_table",
gen_avx2_rej_uniform_table_rows(),
suffix="[8]",
)
yield ""
yield "#else"
yield ""
yield "MLD_EMPTY_CU(avx2_rej_uniform_table)"
yield ""
yield "#endif"
yield ""
update_file("dev/x86_64/src/rej_uniform_table.c", "\n".join(gen()))
def gen_keccak_round_constants():
rc = [0] * 24
lfsr = 1
for round_idx in range(24):
rc_val = 0
for j in range(7):
if lfsr & 1:
rc_val ^= 1 << ((1 << j) - 1)
lfsr = (lfsr << 1) ^ (0x71 if lfsr & 0x80 else 0)
lfsr &= 0xFF
rc[round_idx] = rc_val
yield from rc
def gen_keccak_rho_shuffle(offset):
for lane in range(4):
base = lane * 8
val = 0
for byte_pos in range(8):
src = (byte_pos + offset) % 8 + base
val |= src << (byte_pos * 8)
yield val
def gen_keccak_rho8():
yield from gen_keccak_rho_shuffle(-1)
def gen_keccak_rho56():
yield from gen_keccak_rho_shuffle(+1)
def print_hol_light_word64_list(g, entries_per_line=4):
values = [f"word 0x{v:016X}" for v in g]
for i in range(0, len(values), entries_per_line):
row = values[i : i + entries_per_line]
is_last = i + entries_per_line >= len(values)
line = "; ".join(row)
if not is_last:
line += ";"
yield " " + line
def print_c_uint64_array(name, values, entries_per_line=1):
vals = list(values)
yield "MLD_ALIGN MLD_INTERNAL_DATA_DEFINITION const uint64_t"
yield f" {name}[{len(vals)}] = {{"
for i in range(0, len(vals), entries_per_line):
row = vals[i : i + entries_per_line]
yield " " + ", ".join(f"0x{v:016x}" for v in row) + ","
yield "};"
def gen_aarch64_keccak_constants_c_file():
def gen_c():
yield from gen_header()
yield '#include "../../../../common.h"'
yield ""
yield "#if (defined(MLD_FIPS202_AARCH64_NEED_X1_SCALAR) || \\"
yield " defined(MLD_FIPS202_AARCH64_NEED_X1_V84A) || \\"
yield " defined(MLD_FIPS202_AARCH64_NEED_X2_V84A) || \\"
yield " defined(MLD_FIPS202_AARCH64_NEED_X4_V8A_SCALAR_HYBRID) || \\"
yield " defined(MLD_FIPS202_AARCH64_NEED_X4_V8A_V84A_SCALAR_HYBRID)) && \\"
yield " !defined(MLD_CONFIG_MULTILEVEL_NO_SHARED)"
yield ""
yield '#include "fips202_native_aarch64.h"'
yield ""
yield from print_c_uint64_array(
"mld_keccakf1600_round_constants",
gen_keccak_round_constants(),
entries_per_line=3,
)
yield ""
yield "#else /* (MLD_FIPS202_AARCH64_NEED_X1_SCALAR || \\"
yield " MLD_FIPS202_AARCH64_NEED_X1_V84A || MLD_FIPS202_AARCH64_NEED_X2_V84A \\"
yield " || MLD_FIPS202_AARCH64_NEED_X4_V8A_SCALAR_HYBRID || \\"
yield " MLD_FIPS202_AARCH64_NEED_X4_V8A_V84A_SCALAR_HYBRID) && \\"
yield " !MLD_CONFIG_MULTILEVEL_NO_SHARED */"
yield ""
yield "MLD_EMPTY_CU(fips202_aarch64_round_constants)"
yield ""
yield "#endif /* !((MLD_FIPS202_AARCH64_NEED_X1_SCALAR || \\"
yield " MLD_FIPS202_AARCH64_NEED_X1_V84A || MLD_FIPS202_AARCH64_NEED_X2_V84A \\"
yield " || MLD_FIPS202_AARCH64_NEED_X4_V8A_SCALAR_HYBRID || \\"
yield " MLD_FIPS202_AARCH64_NEED_X4_V8A_V84A_SCALAR_HYBRID) && \\"
yield " !MLD_CONFIG_MULTILEVEL_NO_SHARED) */"
yield ""
update_file(
"dev/fips202/aarch64/src/keccakf1600_round_constants.c",
"\n".join(gen_c()),
force_format=True,
)
def gen_avx2_keccak_constants_c_file():
def gen_c():
yield from gen_header()
yield '#include "../../../../common.h"'
yield "#if defined(MLD_FIPS202_X86_64_NEED_X4_AVX2) && \\"
yield " !defined(MLD_CONFIG_MULTILEVEL_NO_SHARED)"
yield ""
yield "#include <stdint.h>"
yield ""
yield '#include "fips202_native_x86_64.h"'
yield ""
yield from print_c_uint64_array(
"mld_keccakf1600_round_constants",
gen_keccak_round_constants(),
entries_per_line=3,
)
yield ""
yield from print_c_uint64_array("mld_keccak_rho8", gen_keccak_rho8())
yield ""
yield from print_c_uint64_array("mld_keccak_rho56", gen_keccak_rho56())
yield ""
yield "#else /* MLD_FIPS202_X86_64_NEED_X4_AVX2 && !MLD_CONFIG_MULTILEVEL_NO_SHARED */"
yield ""
yield "MLD_EMPTY_CU(fips202_x86_64_constants)"
yield ""
yield "#endif /* !(MLD_FIPS202_X86_64_NEED_X4_AVX2 && !MLD_CONFIG_MULTILEVEL_NO_SHARED) */"
yield ""
update_file(
"dev/fips202/x86_64/src/keccakf1600_constants.c",
"\n".join(gen_c()),
force_format=True,
)
def gen_hol_light_keccak_constants_file():
def gen():
yield from gen_hol_light_header()
yield "(* Keccak-f[1600] round constants RC[i] for i = 0..23. *)"
yield ""
yield "let round_constants = define `round_constants:int64 list = ["
yield from print_hol_light_word64_list(
gen_keccak_round_constants(), entries_per_line=1
)
yield "]`;;"
yield ""
update_file(
"proofs/hol_light/common/keccak_constants.ml",
"\n".join(gen()),
)
def gen_avx2_keccak_hol_light_constants_file():
def gen():
yield from gen_hol_light_header()
yield "(* Keccak constants for x86_64 AVX2 implementations. *)"
yield ""
yield "let rho8_constant = define `rho8_constant:int64 list = ["
yield from print_hol_light_word64_list(gen_keccak_rho8(), entries_per_line=1)
yield "]`;;"
yield ""
yield "let rho56_constant = define `rho56_constant:int64 list = ["
yield from print_hol_light_word64_list(gen_keccak_rho56(), entries_per_line=1)
yield "]`;;"
yield ""
update_file(
"proofs/hol_light/x86_64/proofs/keccak_f1600_x4_avx2_constants.ml",
"\n".join(gen()),
)
def signed_reduce_u32(a):
c = a % 2**32
if c >= 2**31:
c -= 2**32
return c
def prepare_root_for_montmul(root, mult):
root = signed_reduce(root * montgomery_factor)
if mult:
root = signed_reduce_u32(root * pow(modulus, -1, 2**32))
return root
def gen_avx2_root_of_unity_for_block(layer, block, mult=False):
log = bitreverse(pow(2, layer) + block, 8)
root = pow(root_of_unity, log, modulus)
return prepare_root_for_montmul(root, mult)
def gen_avx2_fwd_ntt_zetas(mult=False):
def gen_twiddles(layer, block, repeat, mult):
root = gen_avx2_root_of_unity_for_block(layer, block, mult)
return [root] * repeat
def gen_twiddles_many(layer, block_base, block_offsets, repeat, mult):
roots = list(
map(
lambda x: gen_twiddles(layer, block_base + x, repeat, mult),
block_offsets,
)
)
yield from (r for ln in roots for r in ln)
f = signed_reduce(-pow(root_of_unity, -128, modulus) * 2**56)
if mult:
f = signed_reduce_u32(f * pow(modulus, -1, 2**32))
yield f
yield from gen_twiddles_many(0, 0, range(1), 1, mult)
yield from gen_twiddles_many(1, 0, range(2), 1, mult)
yield from gen_twiddles_many(2, 0, range(4), 1, mult)
yield from gen_twiddles_many(3, 0, range(8), 4, mult)
yield from gen_twiddles_many(4, 0, range(16), 2, mult)
yield from gen_twiddles_many(5, 0, range(32), 1, mult)
for i in range(32):
yield from gen_twiddles_many(6, i * 2, range(1), 1, mult)
for i in range(32):
yield from gen_twiddles_many(6, i * 2 + 1, range(1), 1, mult)
for k in range(4):
for i in range(32):
yield from gen_twiddles_many(7, i * 4 + k, range(1), 1, mult)
def gen_avx2_zetas_qdata():
def cmod(a, mod):
c = a % mod
if c >= mod / 2:
c -= mod
return c
q_dup = [modulus] * 8
qinv_dup = [pow(modulus, -1, 2**32)] * 8
div_qinv_dup = [cmod(pow(modulus, -1, 2**32) * pow(2, 64 - 8, modulus), 2**32)] * 8
div_dup = [pow(2, 64 - 8, modulus)] * 8
zetas_qinv = list(gen_avx2_fwd_ntt_zetas(mult=True))
zetas = list(gen_avx2_fwd_ntt_zetas(mult=False))
q_idx = 0
qinv_idx = q_idx + len(q_dup)
div_qinv_idx = qinv_idx + len(qinv_dup)
div_idx = div_qinv_idx + len(div_qinv_dup)
zetas_qinv_idx = div_idx + len(div_dup)
zetas_idx = zetas_qinv_idx + len(zetas_qinv)
constants = q_dup + qinv_dup + div_qinv_dup + div_dup + zetas_qinv + zetas
offsets = {
"q": q_idx,
"qinv": qinv_idx,
"div_qinv": div_qinv_idx,
"div": div_idx,
"zetas_qinv": zetas_qinv_idx,
"zetas": zetas_idx,
}
return constants, offsets
def gen_aarch64_hol_light_zeta_file():
def gen():
yield from gen_hol_light_header()
yield "(*"
yield " * Table of zeta values used in the AArch64 NTTs"
yield " * See autogen for details."
yield " *)"
yield ""
yield "let ntt_zetas_layer012345 = define `ntt_zetas_layer012345:int list = ["
yield from print_hol_light_array(gen_aarch64_fwd_ntt_zetas_layer123456())
yield "]`;;"
yield ""
yield "let ntt_zetas_layer67 = define `ntt_zetas_layer67:int list = ["
yield from print_hol_light_array(gen_aarch64_fwd_ntt_zetas_layer78())
yield "]`;;"
yield ""
yield "let intt_zetas_layer78 = define `intt_zetas_layer78:int list = ["
yield from print_hol_light_array(gen_aarch64_intt_zetas_layer78())
yield "]`;;"
yield ""
yield "let intt_zetas_layer123456 = define `intt_zetas_layer123456:int list = ["
yield from print_hol_light_array(gen_aarch64_intt_zetas_layer123456())
yield "]`;;"
yield ""
update_file("proofs/hol_light/aarch64/proofs/mldsa_zetas.ml", "\n".join(gen()))
def gen_avx2_hol_light_zeta_file():
def gen():
yield from gen_hol_light_header()
yield "(*"
yield " * Table of zeta values used in the AVX2 NTTs"
yield " * See autogen for details."
yield " *)"
yield ""
yield "let mldsa_complete_qdata = define `mldsa_complete_qdata:int list = ["
constants, _ = gen_avx2_zetas_qdata()
yield from print_hol_light_array(constants)
yield "]`;;"
yield ""
update_file("proofs/hol_light/x86_64/proofs/mldsa_zetas.ml", "\n".join(gen()))
def gen_avx2_zeta_file():
constants, offsets = gen_avx2_zetas_qdata()
def gen_c():
yield from gen_header()
yield '#include "../../../common.h"'
yield ""
yield "#if defined(MLD_ARITH_BACKEND_X86_64_DEFAULT) && \\"
yield " !defined(MLD_CONFIG_MULTILEVEL_NO_SHARED)"
yield ""
yield '#include "consts.h"'
yield ""
yield "/*"
yield " * Table of zeta values used in the AVX2 forward and inverse NTT"
yield " * See autogen for details."
yield " */"
yield from emit_c_array("const int32_t", "mld_qdata", constants)
yield ""
yield "#else"
yield ""
yield "MLD_EMPTY_CU(avx2_consts)"
yield ""
yield "#endif"
yield ""
def gen_h():
yield from gen_header()
yield "#ifndef MLD_NATIVE_X86_64_SRC_CONSTS_H"
yield "#define MLD_NATIVE_X86_64_SRC_CONSTS_H"
yield '#include "../../../common.h"'
yield f"#define MLD_AVX2_BACKEND_DATA_OFFSET_8XQ {offsets['q']}"
yield f"#define MLD_AVX2_BACKEND_DATA_OFFSET_8XQINV {offsets['qinv']}"
yield f"#define MLD_AVX2_BACKEND_DATA_OFFSET_8XDIV_QINV {offsets['div_qinv']}"
yield f"#define MLD_AVX2_BACKEND_DATA_OFFSET_8XDIV {offsets['div']}"
yield f"#define MLD_AVX2_BACKEND_DATA_OFFSET_ZETAS_QINV {offsets['zetas_qinv']}"
yield f"#define MLD_AVX2_BACKEND_DATA_OFFSET_ZETAS {offsets['zetas']}"
yield ""
yield "#ifndef __ASSEMBLER__"
yield "#define mld_qdata MLD_NAMESPACE(qdata)"
yield emit_c_array_decl("const int32_t", "mld_qdata", len(constants))
yield "#endif"
yield ""
yield "#endif"
yield ""
update_file("dev/x86_64/src/consts.c", "\n".join(gen_c()))
update_file("dev/x86_64/src/consts.h", "\n".join(gen_h()))
def get_c_source_files(main_only=False, core_only=False, strip_mldsa=False):
if main_only is True:
return get_files("mldsa/src/**/*.c", strip_mldsa=strip_mldsa)
elif core_only is True:
return get_files("mldsa/src/**/*.c", strip_mldsa=strip_mldsa) + get_files(
"dev/**/*.c"
)
else:
return get_files("**/*.c", strip_mldsa=strip_mldsa)
def get_asm_source_files(main_only=False, core_only=False, strip_mldsa=False):
if main_only is True:
return get_files("mldsa/src/**/*.S", strip_mldsa=strip_mldsa)
elif core_only is True:
return get_files("mldsa/src/**/*.S", strip_mldsa=strip_mldsa) + get_files(
"dev/**/*.S", strip_mldsa=strip_mldsa
)
else:
return get_files("**/*.S", strip_mldsa=strip_mldsa)
def get_header_files(main_only=False, core_only=False):
if main_only is True:
return get_files("mldsa/*.h") + get_files("mldsa/src/**/*.h")
elif core_only is True:
return (
get_files("mldsa/*.h")
+ get_files("mldsa/src/**/*.h")
+ get_files("dev/**/*.h")
+ get_files("integration/**/*.h")
)
else:
return get_files("**/*.h")
def get_markdown_files(main_only=False):
return get_files("**/*.md")
def get_files(pattern, strip_mldsa=False):
def normalize(f):
return f.removeprefix("mldsa/") if strip_mldsa else f
fs_files = {f for f in get_all_files() if pathlib.Path(f).is_file()}
cache_files = set(_file_cache.keys())
pattern = pattern.replace(".", r"\.")
pattern = pattern.replace("**/", "<<<DOUBLESTAR>>>")
pattern = pattern.replace("*", "[^/]*")
pattern = pattern.replace("<<<DOUBLESTAR>>>", "(?:.*/)?")
regexp = re.compile(f"^{pattern}$")
res = list(
map(
normalize,
filter(
lambda f: regexp.match(f) and (read_file(f) is not None),
fs_files | cache_files,
),
)
)
return res
def get_all_files():
r = subprocess.run(["git", "ls-files"], capture_output=True, text=True)
assert r.returncode == 0
files = r.stdout.split("\n")
files = filter(lambda s: s != "" and pathlib.Path(s).is_symlink() is False, files)
return files
def get_defines_from_file(c):
for line in read_file(c).split("\n"):
m = _RE_DEFINE.match(line)
if m:
yield (c, m.group(1))
def get_defines(all=False):
if all is False:
files = get_header_files(main_only=True)
else:
files = get_header_files() + get_c_source_files() + get_asm_source_files()
for results in run_parallel(files, get_defines_from_file):
yield from results
def get_checked_defines():
allow_list = [("__contract__", "cbmc.h"), ("__loop__", "cbmc.h")]
def is_allowed(d, c):
for d0, c0 in allow_list:
if c.endswith(c0) is True and d0 == d:
return True
return False
for c, d in get_defines():
if d.startswith("_") and is_allowed(d, c) is False:
raise Exception(
f"{d} from {c}: starts with an underscore, which is not allowed for mldsa-native macros. "
f"If this is an mldsa-native specific macro, please pick a different name. "
f"If this is an external macro, it likely needs removing from `gen_monolithic_undef_all_core()` in `scripts/autogen` -- check this!"
)
yield (c, d)
def gen_monolithic_undef_all_core(filt=None, desc=""):
if filt is None:
def filt(c):
return True
if desc != "":
yield "/*"
yield f" * Undefine macros from {desc}"
yield " */"
defines = list(set(get_checked_defines()))
defines.sort()
last_filename = None
for filename, d in defines:
if filt(filename) is False:
continue
if last_filename != filename:
yield f"/* {filename} */"
last_filename = filename
yield f"#undef {d}"
def native(c):
return "/native/" in c
def fips202(c):
return "/fips202/" in c
def aarch64(c):
return "/aarch64/" in c
def x86_64(c):
return "/x86_64/" in c
def riscv64(c):
return "/riscv64/" in c
def armv81m(c):
return "/armv81m/" in c
def native_fips202(c):
return native(c) and fips202(c)
def native_arith(c):
return native(c) and not fips202(c)
def native_fips202_aarch64(c):
return native_fips202(c) and aarch64(c)
def native_fips202_x86_64(c):
return native_fips202(c) and x86_64(c)
def native_fips202_armv81m(c):
return native_fips202(c) and armv81m(c)
def native_fips202_core(c):
return (
native_fips202(c)
and not native_fips202_x86_64(c)
and not native_fips202_aarch64(c)
and not native_fips202_armv81m(c)
)
def native_arith_aarch64(c):
return native_arith(c) and aarch64(c)
def native_arith_x86_64(c):
return native_arith(c) and x86_64(c)
def native_arith_riscv64(c):
return native_arith(c) and riscv64(c)
def native_arith_core(c):
return (
native_arith(c)
and not native_arith_x86_64(c)
and not native_arith_aarch64(c)
and not native_arith_riscv64(c)
)
def k_specific(c):
k_specific_sources = [
"mldsa_native.h",
"params.h",
"common.h",
"packing.c",
"packing.h",
"poly_kl.c",
"poly_kl.h",
"polyvec.c",
"polyvec.h",
"polyvec_lazy.h",
"rounding.h",
"sign.c",
"sign.h",
]
for f in k_specific_sources:
if c.endswith(f):
return True
return False
def k_generic(c):
return not k_specific(c) and c != "mldsa/mldsa_native_config.h"
def gen_macro_undefs(extra_notes=None):
if extra_notes is None:
extra_notes = []
yield "/* Macro #undef's"
yield " *"
yield " * The following undefines macros from headers"
yield " * included by the source files imported above."
yield " *"
yield " * This is to allow building and linking multiple builds"
yield " * of mldsa-native for varying parameter sets through concatenation"
yield " * of this file, as if the files had been compiled separately."
yield " * If this is not relevant to you, you may remove the following."
for e in extra_notes:
yield f" * {e}"
yield " */"
yield ""
yield from gen_monolithic_undef_all_core(
filt=k_specific, desc="MLD_CONFIG_PARAMETER_SET-specific files"
)
yield ""
yield "#if !defined(MLD_CONFIG_MONOBUILD_KEEP_SHARED_HEADERS)"
yield from gen_monolithic_undef_all_core(
filt=lambda c: (
not native(c) and k_generic(c) and not fips202(c) and "cbmc.h" not in c
),
desc="MLD_CONFIG_PARAMETER_SET-generic files",
)
yield "/* mldsa/src/cbmc.h */"
yield "#undef MLD_CBMC_H"
yield "#undef __contract__"
yield "#undef __loop__"
yield ""
yield "#if !defined(MLD_CONFIG_FIPS202_CUSTOM_HEADER)"
yield from gen_monolithic_undef_all_core(
filt=lambda c: not native(c) and k_generic(c) and fips202(c),
desc="FIPS-202 files",
)
yield "#endif"
yield ""
yield "#if defined(MLD_CONFIG_USE_NATIVE_BACKEND_FIPS202)"
yield from gen_monolithic_undef_all_core(filt=native_fips202_core, desc="")
yield "#if defined(MLD_SYS_AARCH64)"
yield from gen_monolithic_undef_all_core(
filt=native_fips202_aarch64, desc="native code (FIPS202, AArch64)"
)
yield "#endif"
yield "#if defined(MLD_SYS_X86_64)"
yield from gen_monolithic_undef_all_core(
filt=native_fips202_x86_64, desc="native code (FIPS202, x86_64)"
)
yield "#endif"
yield "#if defined(MLD_SYS_ARMV81M_MVE)"
yield from gen_monolithic_undef_all_core(
filt=native_fips202_armv81m, desc="native code (FIPS202, Armv8.1-M)"
)
yield "#endif"
yield "#endif"
yield "#if defined(MLD_CONFIG_USE_NATIVE_BACKEND_ARITH)"
yield from gen_monolithic_undef_all_core(filt=native_arith_core, desc="")
yield "#if defined(MLD_SYS_AARCH64)"
yield from gen_monolithic_undef_all_core(
filt=native_arith_aarch64, desc="native code (Arith, AArch64)"
)
yield "#endif"
yield "#if defined(MLD_SYS_X86_64)"
yield from gen_monolithic_undef_all_core(
filt=native_arith_x86_64, desc="native code (Arith, X86_64)"
)
yield "#endif"
yield "#endif"
yield "#endif"
yield ""
def gen_monolithic_source_file():
def gen():
c_sources = get_c_source_files(main_only=True, strip_mldsa=True)
yield from gen_header()
yield "/******************************************************************************"
yield " *"
yield " * Single compilation unit (SCU) for fixed-level build of mldsa-native"
yield " *"
yield " * This compilation unit bundles together all source files for a build"
yield " * of mldsa-native for a fixed security level (MLDSA-44/65/87)."
yield " *"
yield " * # API"
yield " *"
yield " * The API exposed by this file is described in mldsa_native.h."
yield " *"
yield " * # Multi-level build"
yield " *"
yield " * If you want an SCU build of mldsa-native with support for multiple security"
yield " * levels, you need to include this file multiple times, and set"
yield " * MLD_CONFIG_MULTILEVEL_WITH_SHARED and MLD_CONFIG_MULTILEVEL_NO_SHARED"
yield " * appropriately. This is exemplified in examples/monolithic_build_multilevel"
yield " * and examples/monolithic_build_multilevel_native."
yield " *"
yield " * # Configuration"
yield " *"
yield " * The following options from the mldsa-native configuration are relevant:"
yield " *"
yield " * - MLD_CONFIG_FIPS202_CUSTOM_HEADER"
yield " * Set this option if you use a custom FIPS202 implementation."
yield " *"
yield " * - MLD_CONFIG_USE_NATIVE_BACKEND_ARITH"
yield " * Set this option if you want to include the native arithmetic backends"
yield " * in your build."
yield " *"
yield " * - MLD_CONFIG_USE_NATIVE_BACKEND_FIPS202"
yield " * Set this option if you want to include the native FIPS202 backends"
yield " * in your build."
yield " *"
yield " * - MLD_CONFIG_MONOBUILD_KEEP_SHARED_HEADERS"
yield " * Set this option if you want to keep the directives defined in"
yield " * level-independent headers. This is needed for a multi-level build."
yield " */"
yield " "
yield " /* If parts of the mldsa-native source tree are not used,"
yield " * consider reducing this header via `unifdef`."
yield " *"
yield " * Example:"
yield " * ```bash"
yield " * unifdef -UMLD_CONFIG_USE_NATIVE_BACKEND_ARITH mldsa_native.c"
yield " * ```"
yield " */"
yield ""
yield '#include "src/common.h"'
yield ""
for c in filter(lambda c: not native(c) and not fips202(c), c_sources):
yield f'#include "{c}"'
yield ""
yield "#if !defined(MLD_CONFIG_FIPS202_CUSTOM_HEADER)"
for c in filter(lambda c: not native(c) and fips202(c), c_sources):
yield f'#include "{c}"'
yield "#endif"
yield ""
yield "#if defined(MLD_CONFIG_USE_NATIVE_BACKEND_ARITH)"
yield "#if defined(MLD_SYS_AARCH64)"
for c in filter(native_arith_aarch64, c_sources):
yield f'#include "{c}"'
yield "#endif"
yield "#if defined(MLD_SYS_X86_64)"
for c in filter(native_arith_x86_64, c_sources):
yield f'#include "{c}"'
yield "#endif"
yield "#endif"
yield ""
yield "#if defined(MLD_CONFIG_USE_NATIVE_BACKEND_FIPS202)"
yield "#if defined(MLD_SYS_AARCH64)"
for c in filter(native_fips202_aarch64, c_sources):
yield f'#include "{c}"'
yield "#endif"
yield "#if defined(MLD_SYS_X86_64)"
for c in filter(native_fips202_x86_64, c_sources):
yield f'#include "{c}"'
yield "#endif"
yield "#if defined(MLD_SYS_ARMV81M_MVE)"
for c in filter(native_fips202_armv81m, c_sources):
yield f'#include "{c}"'
yield "#endif"
yield "#endif"
yield ""
yield from gen_macro_undefs()
update_file("mldsa/mldsa_native.c", "\n".join(gen()))
def gen_monolithic_asm_file():
def gen():
asm_sources = get_asm_source_files(main_only=True, strip_mldsa=True)
yield from gen_header()
yield "/******************************************************************************"
yield " *"
yield " * Single assembly unit for fixed-level build of mldsa-native"
yield " *"
yield " * This assembly unit bundles together all assembly files for a build"
yield " * of mldsa-native for a fixed security level (MLDSA-44/65/87)."
yield " *"
yield " * # Multi-level build"
yield " *"
yield " * If you want an SCU build of mldsa-native with support for multiple security"
yield " * levels, you should include this file once with MLD_CONFIG_MULTILEVEL_WITH_SHARED set."
yield " *"
yield " * (You could also follow the same pattern as for mldsa_native_monobuild.c"
yield " * and include it for every level, setting MLD_CONFIG_MULTILEVEL_NO_SHARED"
yield " * for all but one. For builds with MLD_CONFIG_MULTILEVEL_NO_SHARED, this"
yield " * file will then be ignored.)"
yield " *"
yield " * # Configuration"
yield " *"
yield " * The following options from the mldsa-native configuration are relevant:"
yield " *"
yield " * - MLD_CONFIG_FIPS202_CUSTOM_HEADER"
yield " * Set this option if you use a custom FIPS202 implementation."
yield " *"
yield " * - MLD_CONFIG_USE_NATIVE_BACKEND_ARITH"
yield " * Set this option if you want to include the native arithmetic backends"
yield " * in your build."
yield " *"
yield " * - MLD_CONFIG_USE_NATIVE_BACKEND_FIPS202"
yield " * Set this option if you want to include the native FIPS202 backends"
yield " * in your build."
yield " *"
yield " * - MLD_CONFIG_MONOBUILD_KEEP_SHARED_HEADERS"
yield " * Set this option if you want to keep the directives defined in"
yield " * level-independent headers. This is needed for a multi-level build."
yield " */"
yield ""
yield "/* If parts of the mldsa-native source tree are not used,"
yield " * consider reducing this header via `unifdef`."
yield " *"
yield " * Example:"
yield " * ```bash"
yield " * unifdef -UMLD_CONFIG_USE_NATIVE_BACKEND_ARITH mldsa_native_asm.S"
yield " * ```"
yield " */"
yield ""
yield '#include "src/common.h"'
yield ""
yield "#if defined(MLD_CONFIG_USE_NATIVE_BACKEND_ARITH)"
yield "#if defined(MLD_SYS_AARCH64)"
for c in filter(native_arith_aarch64, asm_sources):
yield f'#include "{c}"'
yield "#endif"
yield "#if defined(MLD_SYS_X86_64)"
for c in filter(native_arith_x86_64, asm_sources):
yield f'#include "{c}"'
yield "#endif"
yield "#endif"
yield ""
yield "#if defined(MLD_CONFIG_USE_NATIVE_BACKEND_FIPS202)"
yield "#if defined(MLD_SYS_AARCH64)"
for c in filter(native_fips202_aarch64, asm_sources):
yield f'#include "{c}"'
yield "#endif"
yield "#if defined(MLD_SYS_X86_64)"
for c in filter(native_fips202_x86_64, asm_sources):
yield f'#include "{c}"'
yield "#endif"
yield "#if defined(MLD_SYS_ARMV81M_MVE)"
for c in filter(native_fips202_armv81m, asm_sources):
yield f'#include "{c}"'
yield "#endif"
yield "#endif"
yield ""
yield ""
extra = [
"",
"NOTE: This is not needed for the assembly SCU since, at present,",
"there is no need to include it multiple times.",
"We keep it for uniformity with mldsa_native.c only.",
"",
"NOTE: To avoid having to distinguish between which headers are included",
"from the assembly files, we #undef the same set of directives",
"as in mldsa_native.c",
]
yield from gen_macro_undefs(extra_notes=extra)
update_file("mldsa/mldsa_native_asm.S", "\n".join(gen()), force_format=True)
def get_config_options():
content = read_file("mldsa/mldsa_native_config.h")
config_pattern = r"(?m)^\s*(?:/\*\s*)?#\s*(?:define|ifn?def)\s+(MLD_CONFIG_\w+)\b"
configs = list(set(re.findall(config_pattern, content)))
configs += [
"MLD_FORCE_AARCH64",
"MLD_FORCE_AARCH64_EB",
"MLD_FORCE_X86_64",
"MLD_FORCE_PPC64LE",
"MLD_FORCE_RISCV64",
"MLD_FORCE_RISCV32",
"MLD_SYS_AARCH64_SLOW_BARREL_SHIFTER",
"MLD_SYS_AARCH64_FAST_SHA3",
"MLDSA_DEBUG", "MLD_BREAK_PCT", "MLD_ALLOW_NONCOMPLIANT_SIGNING_BOUND", "MLD_CHECK_APIS",
"MLD_ERR_XXX",
"MLD_USE_NATIVE_XXX",
"MLD_CONFIG_XXX",
"MLD_PREHASH_",
"MLD_UNIT_TEST",
]
return configs
def check_macro_typos_in_file(filename, macro_check):
content = read_file(filename)
for m in _RE_MLKEM_MACRO_CHECK.finditer(content):
txt = m.group(1)
rest = m.group(2)
line_no = content[: m.start()].count("\n") + 1
if filename != "scripts/autogen":
raise Exception(
f"Likely typo {txt} in {filename}:{line_no}? wrongly ported MLK_XXX / MLKEM_XXX macros from mlkem-native."
)
for m in _RE_MACRO_CHECK.finditer(content):
txt = m.group(1)
rest = m.group(2)
if macro_check(txt, rest, filename) is False:
line_no = content[: m.start()].count("\n") + 1
raise Exception(
f"Likely typo {txt} in {filename}:{line_no}? Not a defined macro."
)
def get_syscaps():
return [
"MLD_SYS_CAP_X86_64_AVX2",
"MLD_SYS_CAP_AARCH64_SHA3",
"MLD_SYS_CAP_ARMV81M_MVE",
]
def check_macro_typos():
files = get_all_files()
syscaps = get_syscaps()
macros = set(map(lambda t: t[1], get_defines(all=True)))
macros.update(get_config_options())
def macro_check(m, rest, filename):
if m in macros:
return True
if m.startswith("MLD_TOTAL_ALLOC") or m.startswith("MLD_MAX_TOTAL_ALLOC"):
return True
is_autogen = filename == "scripts/autogen"
if m in syscaps:
return True
if is_autogen or filename.endswith("/Makefile"):
if m.startswith("MLD_SOURCE") or m.startswith("MLD_OBJ"):
return True
if is_autogen or filename.startswith("integration/liboqs"):
if m.startswith("MLDSA_NATIVE_MLDSA") or m in ["MLDSA_DIR"]:
return True
if is_autogen or filename.startswith("proofs/hol_light"):
if filename.endswith(".ml"):
return True
if is_autogen or filename.startswith("proofs/isabelle"):
if filename.endswith(".thy") and m.endswith("_def"):
return True
if is_autogen:
if rest.startswith("\\") or m in ["MLD_XXX", "MLD_SOURCE_XXX"]:
return True
if is_autogen or filename == "integration/aws-lc/post_import.patch":
return True
if (
is_autogen
or filename == "mldsa/src/common.h"
or filename == "mldsa/src/context.h"
):
if m == "MLD_CONTEXT_PARAMETERS_n":
return True
return False
run_parallel(
list(files), partial(check_macro_typos_in_file, macro_check=macro_check)
)
def check_asm_register_aliases_for_file(filename):
def get_alias_def(line):
s = list(filter(lambda s: s != "", line.strip().split(" ")))
if len(s) < 3 or s[1] != ".req":
return None
return s[0]
def get_alias_undef(line):
if line.strip().startswith(".unreq") is False:
return None
return list(filter(lambda s: s != "", line.strip().split(" ")))[1]
content = read_file(filename)
aliases = {}
for i, line in enumerate(content.split("\n")):
alias_def = get_alias_def(line)
alias_undef = get_alias_undef(line)
if alias_def is not None:
if alias_def in aliases.keys():
raise Exception(
f"Invalid assembly file {filename}: Duplicate .req directive for {alias_def} at line {i}"
)
aliases[alias_def] = i
elif alias_undef is not None:
if alias_undef not in aliases.keys():
raise Exception(
f"Invalid assembly file {filename}: .unreq without prior .req for {alias_undef} at line {i}"
)
del aliases[alias_undef]
if len(aliases) > 0:
fixup_suggestion = [
"/****************** REGISTER DEALLOCATIONS *******************/"
]
dangling = list(aliases.items())
dangling.sort(key=lambda s: s[1])
for a, _ in dangling:
fixup_suggestion.append(f" .unreq {a}")
fixup_suggestion.append("")
fixup_suggestion = "\n".join(fixup_suggestion)
raise Exception(
f"Invalid assembly file {filename}: Dangling .req directives {aliases}.\n\nTry adding this?\n\n{fixup_suggestion}"
)
def check_asm_register_aliases():
run_parallel(get_asm_source_files(), check_asm_register_aliases_for_file)
def check_asm_loop_labels_for_file(filename):
content = read_file(filename)
res = _RE_FUNC_SYMBOL.search(content)
if res is None:
raise Exception(f"Could not find function symbol in assembly file {filename}")
funcname = res.group(1)
lbl_prefix = re.sub(r"(_(aarch64|avx2|mve))?_asm$", "", funcname) + "_"
lbl_pattern = r"^(\w+):"
for m in re.finditer(lbl_pattern, content, flags=re.M):
lbl = m.group(1)
if not lbl.startswith(lbl_prefix):
raise Exception(
f"Please change label {lbl} in {filename} to be prefixed by {lbl_prefix}"
)
def check_asm_loop_labels():
files = list(filter(lambda s: s.startswith("dev/"), get_asm_source_files()))
run_parallel(files, check_asm_loop_labels_for_file)
def normalize_comma_separated_args(args_str):
match = _RE_ARGS_COMMENT.match(args_str)
args_only = match.group(1).rstrip()
comment = match.group(2) or ""
if "," in args_only:
result = re.sub(r",(?! )", ", ", args_only)
else:
args = args_only.split()
if not args:
return args_str
result = ", ".join(args)
return result + comment
def normalize_asm_macro_syntax_for_file(filename):
content = read_file(filename)
lines = content.split("\n")
macro_names = set()
for line in lines:
macro_match = _RE_MACRO_DEF.match(line)
if macro_match:
macro_names.add(macro_match.group(1))
new_lines = []
for line in lines:
macro_def_match = _RE_MACRO_DEF_ARGS.match(line)
if macro_def_match:
prefix = macro_def_match.group(1)
args_with_space = macro_def_match.group(2)
leading_space = _RE_LEADING_SPACE.match(args_with_space).group(1)
args = args_with_space.lstrip()
normalized_args = normalize_comma_separated_args(args)
line = prefix + leading_space + normalized_args
else:
for macro_name in macro_names:
pattern = r"^(\s*" + re.escape(macro_name) + r")(\s+.*)$"
invocation_match = re.match(pattern, line)
if not invocation_match:
continue
prefix = invocation_match.group(1)
args_with_space = invocation_match.group(2)
leading_space = _RE_LEADING_SPACE.match(args_with_space).group(1)
args = args_with_space.lstrip()
normalized_args = normalize_comma_separated_args(args)
line = prefix + leading_space + normalized_args
break
new_lines.append(line)
update_file(filename, "\n".join(new_lines))
def normalize_asm_macro_syntax():
files = list(filter(lambda s: s.startswith("dev/"), get_asm_source_files()))
files += list(filter(lambda s: s.endswith(".inc"), get_files("dev/**/*.inc")))
run_parallel(files, normalize_asm_macro_syntax_for_file)
FORCE_CROSS_ALL_ARCHES = {"aarch64", "x86_64", "armv81m"}
def resolve_force_cross(value):
if value is None:
return set()
if value == []:
return set(FORCE_CROSS_ALL_ARCHES)
return set(value)
def update_via_simpasm(
infile_full,
outdir,
outfile=None,
cflags=None,
preserve_header=True,
force_cross=(),
x86_64_syntax="att",
):
_, infile = os.path.split(infile_full)
if outfile is None:
outfile = infile
outfile_full = os.path.join(outdir, outfile)
if cflags is None:
cflags = ""
cflags += " -Imldsa"
if "aarch64" in infile_full:
source_arch = "aarch64"
elif "x86_64" in infile_full:
source_arch = "x86_64"
elif "armv81m" in infile_full:
source_arch = "armv81m"
else:
raise Exception(f"Could not detect architecture of source file {infile_full}.")
if platform.machine().lower() in ["arm64", "aarch64"]:
native_arch = "aarch64"
else:
native_arch = "x86_64"
if source_arch == "armv81m":
cross_prefix = "arm-none-eabi-"
cross_gcc = cross_prefix + "gcc"
if shutil.which(cross_gcc) is None:
if source_arch not in force_cross:
return
raise Exception(f"Could not find cross toolchain {cross_prefix}")
elif native_arch != source_arch:
cross_prefix = f"{source_arch}-unknown-linux-gnu-"
cross_gcc = cross_prefix + "gcc"
if shutil.which(cross_gcc) is None:
if source_arch not in force_cross:
return
raise Exception(f"Could not find cross toolchain {cross_prefix}")
else:
cross_prefix = None
with tempfile.NamedTemporaryFile(suffix=".S") as tmp:
try:
if "aarch64" in infile_full:
arch = "aarch64"
elif "armv81m" in infile_full:
arch = "armv81m"
else:
arch = "x86_64"
cmd = [
"./scripts/simpasm",
"--objdump=llvm-objdump",
"--arch=" + arch,
"-i",
infile_full,
"-o",
tmp.name,
]
cmd += ["--cfify"]
if cross_prefix is not None:
cmd += ["--cc", cross_prefix + "gcc"]
cmd += ["--nm", cross_prefix + "nm"]
if cflags is not None:
cmd += [f'--cflags="{cflags}"']
if preserve_header is True:
cmd += ["-p"]
if arch == "x86_64" and x86_64_syntax != "att":
cmd += ["--syntax", x86_64_syntax]
subprocess.run(
cmd,
stdout=subprocess.DEVNULL,
stderr=subprocess.PIPE,
check=True,
text=True,
)
except subprocess.CalledProcessError as e:
print(f"Command failed: {' '.join(cmd)}")
print(f"Exit code: {e.returncode}")
print(f"stderr: {e.stderr}")
raise Exception("Failed to run simpasm") from e
tmp.seek(0)
new_contents = tmp.read().decode()
update_file(outfile_full, new_contents)
def gen_hol_light_asm_file(job):
infile, indir, cflags, arch = job
update_via_simpasm(
f"{indir}/{infile}",
"proofs/hol_light/" + arch + "/mldsa",
cflags=cflags + " -DMLD_CONFIG_NAMESPACE_PREFIX=mld",
preserve_header=False,
)
def extract_yaml_from_assembly(assembly_file):
with open(assembly_file, "r") as f:
content = f.read()
yaml_match = re.search(r"/\*yaml\s*\n(.*?)\n\*/", content, re.DOTALL)
if not yaml_match:
raise ValueError(f"No YAML metadata found in {assembly_file}")
return yaml.safe_load(yaml_match.group(1))
def upstream_src_dir_for_dev_dir(dev_dir):
if dev_dir.startswith("dev/fips202/"):
arch = dev_dir.split("/")[2]
return f"mldsa/src/fips202/native/{arch}/src"
if dev_dir.startswith("dev/aarch64_"):
return "mldsa/src/native/aarch64/src"
if dev_dir.startswith("dev/x86_64/"):
return "mldsa/src/native/x86_64/src"
raise ValueError(
f"Unrecognised dev/ source directory {dev_dir!r}; "
"extend upstream_src_dir_for_dev_dir() to map it to mldsa/src/..."
)
def hol_light_asm_joblist():
aarch64_flags = "-march=armv8.4-a+sha3"
joblist_aarch64 = [
(
"ntt_aarch64_asm.S",
"dev/aarch64_opt/src",
f"-Imldsa/src/native/aarch64/src {aarch64_flags}",
"aarch64",
),
(
"intt_aarch64_asm.S",
"dev/aarch64_opt/src",
f"-Imldsa/src/native/aarch64/src {aarch64_flags}",
"aarch64",
),
(
"poly_caddq_aarch64_asm.S",
"dev/aarch64_opt/src",
f"-Imldsa/src/native/aarch64/src {aarch64_flags}",
"aarch64",
),
(
"poly_chknorm_aarch64_asm.S",
"dev/aarch64_opt/src",
f"-Imldsa/src/native/aarch64/src {aarch64_flags}",
"aarch64",
),
(
"poly_decompose_32_aarch64_asm.S",
"dev/aarch64_opt/src",
f"-Imldsa/src/native/aarch64/src {aarch64_flags}",
"aarch64",
),
(
"poly_decompose_88_aarch64_asm.S",
"dev/aarch64_opt/src",
f"-Imldsa/src/native/aarch64/src {aarch64_flags}",
"aarch64",
),
(
"poly_use_hint_32_aarch64_asm.S",
"dev/aarch64_opt/src",
f"-Imldsa/src/native/aarch64/src {aarch64_flags}",
"aarch64",
),
(
"poly_use_hint_88_aarch64_asm.S",
"dev/aarch64_opt/src",
f"-Imldsa/src/native/aarch64/src {aarch64_flags}",
"aarch64",
),
(
"mld_polyvecl_pointwise_acc_montgomery_l4_aarch64_asm.S",
"dev/aarch64_opt/src",
f"-Imldsa/src/native/aarch64/src {aarch64_flags}",
"aarch64",
),
(
"mld_polyvecl_pointwise_acc_montgomery_l5_aarch64_asm.S",
"dev/aarch64_opt/src",
f"-Imldsa/src/native/aarch64/src {aarch64_flags}",
"aarch64",
),
(
"mld_polyvecl_pointwise_acc_montgomery_l7_aarch64_asm.S",
"dev/aarch64_opt/src",
f"-Imldsa/src/native/aarch64/src {aarch64_flags}",
"aarch64",
),
(
"keccak_f1600_x1_scalar_aarch64_asm.S",
"dev/fips202/aarch64/src",
f"-Imldsa/src/fips202/native/aarch64/src {aarch64_flags}",
"aarch64",
),
(
"keccak_f1600_x1_v84a_aarch64_asm.S",
"dev/fips202/aarch64/src",
f"-Imldsa/src/fips202/native/aarch64/src {aarch64_flags}",
"aarch64",
),
(
"keccak_f1600_x2_v84a_aarch64_asm.S",
"dev/fips202/aarch64/src",
f"-Imldsa/src/fips202/native/aarch64/src {aarch64_flags}",
"aarch64",
),
(
"keccak_f1600_x4_v8a_v84a_scalar_hybrid_aarch64_asm.S",
"dev/fips202/aarch64/src",
f"-Imldsa/src/fips202/native/aarch64/src {aarch64_flags}",
"aarch64",
),
(
"keccak_f1600_x4_v8a_scalar_hybrid_aarch64_asm.S",
"dev/fips202/aarch64/src",
f"-Imldsa/src/fips202/native/aarch64/src {aarch64_flags}",
"aarch64",
),
(
"pointwise_montgomery_aarch64_asm.S",
"dev/aarch64_opt/src",
f"-Imldsa/src/native/aarch64/src {aarch64_flags}",
"aarch64",
),
(
"polyz_unpack_17_aarch64_asm.S",
"dev/aarch64_opt/src",
f"-Imldsa/src/native/aarch64/src {aarch64_flags}",
"aarch64",
),
(
"polyz_unpack_19_aarch64_asm.S",
"dev/aarch64_opt/src",
f"-Imldsa/src/native/aarch64/src {aarch64_flags}",
"aarch64",
),
(
"rej_uniform_aarch64_asm.S",
"dev/aarch64_opt/src",
f"-Imldsa/src/native/aarch64/src {aarch64_flags}",
"aarch64",
),
(
"rej_uniform_eta2_aarch64_asm.S",
"dev/aarch64_opt/src",
f"-Imldsa/src/native/aarch64/src {aarch64_flags}",
"aarch64",
),
(
"rej_uniform_eta4_aarch64_asm.S",
"dev/aarch64_opt/src",
f"-Imldsa/src/native/aarch64/src {aarch64_flags}",
"aarch64",
),
]
x86_64_flags = "-mavx2 -mbmi2 -msse4 -fcf-protection=full"
joblist_x86_64 = [
(
"poly_caddq_avx2_asm.S",
"dev/x86_64/src",
f"-Imldsa/src/native/x86_64/src -Imldsa/src/common.h {x86_64_flags}",
"x86_64",
),
(
"poly_chknorm_avx2_asm.S",
"dev/x86_64/src",
f"-Imldsa/src/native/x86_64/src -Imldsa/src/common.h {x86_64_flags}",
"x86_64",
),
(
"poly_use_hint_32_avx2_asm.S",
"dev/x86_64/src",
f"-Imldsa/src/native/x86_64/src -Imldsa/src/common.h {x86_64_flags}",
"x86_64",
),
(
"poly_use_hint_88_avx2_asm.S",
"dev/x86_64/src",
f"-Imldsa/src/native/x86_64/src -Imldsa/src/common.h {x86_64_flags}",
"x86_64",
),
(
"ntt_avx2_asm.S",
"dev/x86_64/src",
f"-Imldsa/src/native/x86_64/src -Imldsa/src/common.h {x86_64_flags}",
"x86_64",
),
(
"intt_avx2_asm.S",
"dev/x86_64/src",
f"-Imldsa/src/native/x86_64/src -Imldsa/src/common.h {x86_64_flags}",
"x86_64",
),
(
"nttunpack_avx2_asm.S",
"dev/x86_64/src",
f"-Imldsa/src/native/x86_64/src -Imldsa/src/common.h {x86_64_flags}",
"x86_64",
),
(
"pointwise_avx2_asm.S",
"dev/x86_64/src",
f"-Imldsa/src/native/x86_64/src -Imldsa/src/common.h {x86_64_flags}",
"x86_64",
),
(
"poly_decompose_32_avx2_asm.S",
"dev/x86_64/src",
f"-Imldsa/src/native/x86_64/src -Imldsa/src/common.h {x86_64_flags}",
"x86_64",
),
(
"poly_decompose_88_avx2_asm.S",
"dev/x86_64/src",
f"-Imldsa/src/native/x86_64/src -Imldsa/src/common.h {x86_64_flags}",
"x86_64",
),
(
"polyz_unpack_17_avx2_asm.S",
"dev/x86_64/src",
f"-Imldsa/src/native/x86_64/src -Imldsa/src/common.h {x86_64_flags}",
"x86_64",
),
(
"polyz_unpack_19_avx2_asm.S",
"dev/x86_64/src",
f"-Imldsa/src/native/x86_64/src -Imldsa/src/common.h {x86_64_flags}",
"x86_64",
),
(
"pointwise_acc_l4_avx2_asm.S",
"dev/x86_64/src",
f"-Imldsa/src/native/x86_64/src -Imldsa/src/common.h {x86_64_flags}",
"x86_64",
),
(
"pointwise_acc_l5_avx2_asm.S",
"dev/x86_64/src",
f"-Imldsa/src/native/x86_64/src -Imldsa/src/common.h {x86_64_flags}",
"x86_64",
),
(
"pointwise_acc_l7_avx2_asm.S",
"dev/x86_64/src",
f"-Imldsa/src/native/x86_64/src -Imldsa/src/common.h {x86_64_flags}",
"x86_64",
),
(
"keccak_f1600_x4_avx2_asm.S",
"dev/fips202/x86_64/src",
f"-Idev/fips202/x86_64/src -Imldsa/src/fips202/native/x86_64/src {x86_64_flags}",
"x86_64",
),
]
return joblist_aarch64 + joblist_x86_64
def gen_hol_light_asm():
run_parallel(hol_light_asm_joblist(), gen_hol_light_asm_file)
def check_hol_light_asm_autogenerated():
expected = {
os.path.join("proofs/hol_light", arch, "mldsa", os.path.basename(infile))
for infile, _indir, _cflags, arch in hol_light_asm_joblist()
}
stray = sorted(
os.path.join(d, fn)
for arch in ("aarch64", "x86_64")
for d in [os.path.join("proofs/hol_light", arch, "mldsa")]
if os.path.isdir(d)
for fn in os.listdir(d)
if fn.endswith(".S") and os.path.join(d, fn) not in expected
)
if stray:
raise Exception(
"HOL-Light assembly files not autogenerated from dev/:\n "
+ "\n ".join(stray)
+ "\n\nAdd the corresponding source to the gen_hol_light_asm joblist."
)
def update_via_copy(infile_full, outfile_full, transform=None):
content = read_file(infile_full)
if transform is not None:
content = transform(content)
update_file(outfile_full, content)
def update_via_remove(filename):
update_file(filename, None)
SYNCHRONIZED_EXTENSIONS = (".c", ".h", ".i", ".inc", ".S")
def synchronize_file(f, in_dir, out_dir, delete=False, no_simplify=False, **kwargs):
if not f.endswith(SYNCHRONIZED_EXTENSIONS):
return None
basename = os.path.basename(f)
if delete is True:
return basename
if no_simplify is False and f.endswith(".S"):
update_via_simpasm(f, out_dir, **kwargs)
else:
_, infile = os.path.split(f)
outfile_full = os.path.join(out_dir, infile)
if f.endswith(".h"):
def transform(c):
return adjust_header_guard_for_filename(c, outfile_full)
else:
transform = None
update_via_copy(f, outfile_full, transform=transform)
return basename
def synchronize_backend(in_dir, out_dir, delete=False, no_simplify=False, **kwargs):
copied = []
files = get_files(os.path.join(in_dir, "*"))
pool_results = run_parallel(
files,
partial(
synchronize_file,
in_dir=in_dir,
out_dir=out_dir,
delete=delete,
no_simplify=no_simplify,
**kwargs,
),
)
copied = [r for r in pool_results if r is not None]
if delete is False:
return
for f in get_files(os.path.join(out_dir, "*")):
if os.path.basename(f) in copied:
continue
if not f.endswith(SYNCHRONIZED_EXTENSIONS):
continue
update_via_remove(f)
def synchronize_backends(
*,
force_cross=(),
clean=False,
delete=False,
no_simplify=False,
x86_64_syntax="att",
):
if clean is False:
ty = "opt"
else:
ty = "clean"
if delete is False:
update_via_copy(
f"dev/aarch64_{ty}/meta.h",
"mldsa/src/native/aarch64/meta.h",
transform=lambda c: adjust_header_guard_for_filename(
c, "mldsa/src/native/aarch64/meta.h"
),
)
update_via_copy(
"dev/x86_64/meta.h",
"mldsa/src/native/x86_64/meta.h",
transform=lambda c: adjust_header_guard_for_filename(
c, "mldsa/src/native/x86_64/meta.h"
),
)
synchronize_backend(
f"dev/aarch64_{ty}/src",
"mldsa/src/native/aarch64/src",
delete=delete,
force_cross=force_cross,
no_simplify=no_simplify,
cflags="-Imldsa/src/native/aarch64/src",
)
synchronize_backend(
"dev/fips202/aarch64/src",
"mldsa/src/fips202/native/aarch64/src",
delete=delete,
force_cross=force_cross,
no_simplify=no_simplify,
cflags="-Imldsa/src/fips202/native/aarch64/src -march=armv8.4-a+sha3",
)
synchronize_backend(
"dev/fips202/aarch64",
"mldsa/src/fips202/native/aarch64",
delete=delete,
force_cross=force_cross,
no_simplify=no_simplify,
cflags="-Imldsa/src/fips202/native/aarch64 -march=armv8.4-a+sha3",
)
synchronize_backend(
"dev/x86_64/src",
"mldsa/src/native/x86_64/src",
delete=delete,
force_cross=force_cross,
no_simplify=no_simplify,
x86_64_syntax=x86_64_syntax,
cflags="-Imldsa/src/native/x86_64/src/ -mavx2 -mbmi2 -msse4 -fcf-protection=none",
)
synchronize_backend(
"dev/fips202/x86_64/src",
"mldsa/src/fips202/native/x86_64/src",
delete=delete,
force_cross=force_cross,
no_simplify=no_simplify,
x86_64_syntax=x86_64_syntax,
cflags="-Idev/fips202/x86_64/src -Imldsa/src/fips202/native/x86_64/src -mavx2 -mbmi2 -msse4 -fcf-protection=none",
)
synchronize_backend(
"dev/fips202/x86_64",
"mldsa/src/fips202/native/x86_64",
delete=delete,
force_cross=force_cross,
no_simplify=no_simplify,
x86_64_syntax=x86_64_syntax,
cflags="-Idev/fips202/x86_64 -Imldsa/src/fips202/native/x86_64 -mavx2 -mbmi2 -msse4 -fcf-protection=none",
)
synchronize_backend(
"dev/fips202/armv81m/src",
"mldsa/src/fips202/native/armv81m/src",
delete=delete,
force_cross=force_cross,
no_simplify=no_simplify,
cflags="-Idev/fips202/armv81m/src -Imldsa/src/fips202/native/armv81m/src -march=armv8.1-m.main+mve -mthumb",
)
synchronize_backend(
"dev/fips202/armv81m",
"mldsa/src/fips202/native/armv81m",
delete=delete,
force_cross=force_cross,
no_simplify=no_simplify,
cflags="-Idev/fips202/armv81m -Imldsa/src/fips202/native/armv81m -march=armv8.1-m.main+mve -mthumb",
)
def adjust_header_guard_for_filename(content, header_file):
content = content.split("\n")
exceptions = {
"mldsa/mldsa_native.h": "MLD_H",
"mldsa/mldsa_native_config.h": "MLD_CONFIG_H",
}
guard_name = (
header_file.removeprefix("mldsa/src/")
.replace("/", "_")
.replace(".", "_")
.upper()
)
guard_name = "MLD_" + guard_name
if header_file in exceptions.keys():
guard_name = exceptions[header_file]
def gen_guard():
yield f"#ifndef {guard_name}"
yield f"#define {guard_name}"
def gen_footer():
yield "#endif"
yield ""
guard = list(gen_guard())
footer = list(gen_footer())
insert_at = None
for i, line in enumerate(content):
if line.strip() == "" or line.startswith(("/*", " *")):
continue
insert_at = i
break
i = insert_at
while content[i].strip() == "":
i += 1
if content[i].strip().startswith("#if !defined") or content[i].strip().startswith(
"#ifndef"
):
del content[i]
if content[i].strip().startswith("#define"):
del content[i]
has_guard = True
else:
has_guard = False
content = content[:i] + guard + content[i:]
if (
has_guard is True
and content[-1] == ""
and content[-2].strip().startswith("#endif")
):
del content[-2:]
content = content + footer
return "\n".join(content)
def gen_header_guard(header_file):
content = read_file(header_file)
new_content = adjust_header_guard_for_filename(content, header_file)
update_file(header_file, new_content)
def gen_header_guards():
run_parallel(get_header_files(main_only=True), gen_header_guard)
def gen_source_undefs(source_file):
undef_list = list(map(lambda c: c[1], get_defines_from_file(source_file)))
if not undef_list:
return
header_defs = {d: c for (c, d) in get_defines()}
undefs = []
ignored = []
for d in undef_list:
if d not in header_defs.keys():
undefs.append(f"#undef {d}")
else:
ignored.append((d, header_defs[d]))
if len(ignored) != 0:
undefs.append(
"/* Some macros are kept because they are also defined in a header. */"
)
for d, c in ignored:
undefs.append(f"/* Keep: {d} ({c.split('/')[-1]}) */")
content = read_file(source_file).split("\n")
footer_start = None
if source_file.endswith(".S"):
footer_start_marker = "simpasm: footer-start"
for i in range(len(content) - 1, -1, -1):
if footer_start_marker in content[i]:
footer_start = i
break
if footer_start is not None:
simpasm_footer = content[footer_start:]
content = content[:footer_start]
else:
simpasm_footer = []
while content and (
content[-1].startswith("#undef")
or content[-1].startswith("/* Keep:")
or content[-1].startswith("/* Some macros")
or content[-1] == ""
):
content.pop()
footer = [
"",
"/* To facilitate single-compilation-unit (SCU) builds, undefine all macros.",
" * Don't modify by hand -- this is auto-generated by scripts/autogen. */",
]
if len(content) >= len(footer) and content[-len(footer) :] == footer:
content = content[: -len(footer)]
content.extend(footer)
content.extend(undefs)
content.append("")
new_content = "\n".join(content + simpasm_footer)
update_file(source_file, new_content)
def gen_undefs():
files = get_c_source_files(core_only=True) + get_asm_source_files(core_only=True)
run_parallel(files, gen_source_undefs)
def gen_slothy(funcs):
if not isinstance(funcs, list):
return
targets = list(map(lambda s: s + ".S", funcs))
for t in targets:
if t.startswith("keccak"):
base = "dev/fips202/aarch64/src"
else:
base = "dev/aarch64_opt/src"
if t.endswith(".S"):
subprocess.run(["rm", "-f", f"{base}/{t}"])
p = subprocess.run(["make", t] + ["-C", base])
if p.returncode != 0:
print(f"Failed to run SLOTHY on {t}!")
exit(1)
class BibliographyEntry:
def __init__(self, raw_dict):
self._raw = raw_dict
self._usages = []
def register_usages(self, lst):
self._usages += lst
@property
def usages(self):
return self._usages
@property
def name(self):
return self._raw["name"]
@property
def short(self):
if "short" in self._raw.keys():
return self._raw["short"]
return self.name
@property
def id(self):
return self._raw["id"]
@property
def url(self):
return self._raw["url"]
@staticmethod
def full_name(name):
if "," not in name:
return name
surname, forename = name.split(",")
return forename.strip() + " " + surname.strip()
@property
def authors(self):
authors = self._raw["author"]
if not isinstance(authors, list):
authors = [authors]
authors = list(map(BibliographyEntry.full_name, authors))
return authors
@property
def authors_text(self):
authors = self._raw["author"]
if not isinstance(authors, list):
authors = [authors]
def surname(name):
return name.split(",")[0].strip()
if len(authors) > 1:
authors = ", ".join(map(surname, authors))
else:
authors = BibliographyEntry.full_name(authors[0])
return authors
def gen_markdown_citations_for(filename, bibliography):
if filename == "BIBLIOGRAPHY.md":
return
content = read_file(filename)
if not _RE_MARKDOWN_CITE.search(content):
return
content = content.split("\n")
citations = {}
for i, line in enumerate(content):
for m in _RE_MARKDOWN_CITE.finditer(line):
cite_id = m.group("id")
uses = citations.get(cite_id, [])
uses.append((filename, i))
citations[cite_id] = uses
footnote_footer_start = "<!--- bibliography --->"
try:
i = content.index(footnote_footer_start)
content = content[:i]
except ValueError:
pass
explicit_footnotes = set()
for line in content:
m = re.match(r"^\[\^(\w+)\]:", line)
if m:
explicit_footnotes.add(m.group(1))
cite_ids = [c for c in citations.keys() if c not in explicit_footnotes]
cite_ids.sort()
if len(cite_ids) > 0:
content.append(footnote_footer_start)
for cite_id in cite_ids:
uses = citations[cite_id]
entry = bibliography.get(cite_id, None)
if entry is None:
raise Exception(
f"Could not find bibliography entry {cite_id} referenced in {filename}. Known entries: {list(bibliography.keys())}"
)
content.append(
f"[^{cite_id}]: {entry.authors_text}: {entry.name}, [{entry.url}]({entry.url})"
)
entry.register_usages(uses)
if len(cite_ids) > 0:
content.append("")
update_file(filename, "\n".join(content))
def gen_c_citations_for(filename, bibliography):
content = read_file(filename)
if not _RE_C_CITE.search(content):
return
references_start = [
"/* References",
" * ==========",
]
references_end = [" */"]
ref_pattern = r"/\* (# )?References.*?\*/\n+"
content = re.sub(ref_pattern, "", content, flags=re.DOTALL)
content = content.split("\n")
citations = {}
for i, line in enumerate(content):
for m in _RE_C_CITE.finditer(line):
cite_id = m.group("id")
uses = citations.get(cite_id, [])
uses.append((filename, i + 1))
citations[cite_id] = uses
references = []
references += references_start
cite_ids = list(citations.keys())
cite_ids.sort()
for cite_id in cite_ids:
uses = citations[cite_id]
entry = bibliography.get(cite_id, None)
if entry is None:
raise Exception(
f"Could not find bibliography entry {cite_id} referenced in {filename}"
)
references.append(" *")
references.append(f" * - [{cite_id}]")
prefix = " * "
for line in [entry.name, entry.authors_text, entry.url]:
if len(line) + len(prefix) <= 80:
references.append(f"{prefix}{line}")
else:
words = line.split()
current = prefix
for word in words:
if len(current) + len(word) <= 80 or current == prefix:
current += word + " "
else:
references.append(current.rstrip())
current = prefix + word + " "
if current.rstrip() != prefix.rstrip():
references.append(current.rstrip())
references += references_end
references = "\n".join(references)
references = references.split("\n")
references = [""] + references
if len(cite_ids) > 0:
insert_at = None
for i, line in enumerate(content):
if line.startswith(("/*", " *")):
continue
insert_at = i
break
content = content[:insert_at] + references + content[insert_at:]
for cite_id in cite_ids:
uses = citations[cite_id]
entry = bibliography.get(cite_id, None)
def bump_line_count(x):
return (x[0], x[1] + len(references))
uses = list(map(bump_line_count, uses))
entry.register_usages(uses)
update_file(filename, "\n".join(content))
def gen_citations_for(filename, bibliography):
if filename.endswith(".md"):
gen_markdown_citations_for(filename, bibliography)
elif filename.endswith((".c", ".h", ".S")):
gen_c_citations_for(filename, bibliography)
else:
raise Exception(f"Unexpected file extension in {filename}")
def gen_bib_file(bibliography):
content = [
"[//]: # (SPDX-License-Identifier: CC-BY-4.0)",
"[//]: # (This file is auto-generated from BIBLIOGRAPHY.yml)",
"[//]: # (Do not modify it directly)",
"",
"# Bibliography",
"",
"This file lists the citations made throughout the mldsa-native ",
"source code and documentation.",
"",
]
cite_ids = list(bibliography.keys())
cite_ids.sort()
for cite_id in cite_ids:
entry = bibliography[cite_id]
content.append(f"### `{cite_id}`")
content.append("")
content.append(f"* {entry.name}")
content.append("* Author(s):")
for author in entry.authors:
content.append(f" - {author}")
content.append(f"* URL: {entry.url}")
content.append("* Referenced from:")
files = list(set(map(lambda x: x[0], entry.usages)))
files.sort()
for filename in files:
content.append(f" - [{filename}]({filename})")
content.append("")
update_file("BIBLIOGRAPHY.md", "\n".join(content))
def get_oqs_shared_sources(backend):
mldsa_dir = "mldsa/src/"
sources = [
f"mldsa/src/{f}"
for f in os.listdir(mldsa_dir)
if os.path.isfile(f"{mldsa_dir}/{f}")
and not f.endswith(".o")
and not f == "mldsa_native.h"
]
if backend != "ref":
sources += [
f"mldsa/src/native/{f}"
for f in os.listdir(f"{mldsa_dir}/native")
if os.path.isfile(f"{mldsa_dir}/native/{f}")
]
sources += [
"integration/liboqs/fips202_glue.h",
"integration/liboqs/fips202x4_glue.h",
]
if backend == "ref":
backend = "c"
sources.append(f"integration/liboqs/config_{backend.lower()}.h")
return sources
def get_oqs_native_sources(backend):
return [f"mldsa/src/native/{backend}"]
def gen_oqs_meta_file(filename):
content = read_file(filename)
yml_data = yaml.safe_load(content)
for impl in yml_data["implementations"]:
name = impl["name"]
sources = get_oqs_shared_sources(name)
sources.sort()
if name != "ref":
sources += get_oqs_native_sources(name)
impl["sources"] = " ".join(sources)
yaml_header = "\n".join(gen_yaml_header())
new_content = yaml.dump(
yml_data,
default_flow_style=False,
sort_keys=False,
allow_unicode=True,
encoding=None,
)
new_content = yaml_header + new_content
update_file(filename, new_content)
def gen_oqs_meta_files():
meta_files = [
"integration/liboqs/ML-DSA-44_META.yml",
"integration/liboqs/ML-DSA-65_META.yml",
"integration/liboqs/ML-DSA-87_META.yml",
]
for meta_file in meta_files:
gen_oqs_meta_file(meta_file)
def gen_citations():
with open("BIBLIOGRAPHY.yml", "r") as f:
bibliography_raw = yaml.safe_load(f.read())
bibliography = {}
for r in bibliography_raw:
cite_id = r["id"]
bibliography[cite_id] = BibliographyEntry(r)
files = (
get_markdown_files()
+ get_asm_source_files()
+ get_c_source_files()
+ get_header_files()
)
run_parallel(files, partial(gen_citations_for, bibliography=bibliography))
for e in bibliography.values():
if len(e.usages) == 0:
raise Exception(
f"Bibliography entry {e.id} is unused! "
"Add a citation or remove from BIBLIOGRAPHY.yml."
)
gen_bib_file(bibliography)
def extract_bytecode_from_output(output_text):
bytecode_dict = {}
lines = output_text.split("\n")
i = 0
while i < len(lines):
line = lines[i]
match = _RE_BYTECODE_START.search(line)
if match:
filename = match.group(1)
bytecode_lines = []
i += 1
while i < len(lines) and "==== bytecode end" not in lines[i]:
bytecode_lines.append(lines[i])
i += 1
bytecode = "\n".join(bytecode_lines).strip()
bytecode_dict[filename] = bytecode
i += 1
return bytecode_dict
def update_bytecode_in_proof_script(filepath, bytecode):
content = read_file(filepath)
start_marker = "(*** BYTECODE START ***)"
end_marker = "(*** BYTECODE END ***)"
if start_marker not in content or end_marker not in content:
raise Exception(f"Could not find BYTECODE START/END markers in {filepath}")
pattern = rf"{re.escape(start_marker)}.*?{re.escape(end_marker)}"
replacement = f"{start_marker}\n{bytecode}\n{end_marker}"
updated_content = re.sub(pattern, replacement, content, flags=re.DOTALL)
update_file(filepath, updated_content)
def update_hol_light_bytecode_for_arch(arch, force_cross=()):
source_arch = arch
if platform.machine().lower() in ["arm64", "aarch64"]:
native_arch = "aarch64"
else:
native_arch = "x86_64"
if native_arch != source_arch:
cross_prefix = f"{source_arch}-unknown-linux-gnu-"
cross_gcc = cross_prefix + "gcc"
if shutil.which(cross_gcc) is None:
if source_arch not in force_cross:
return
raise Exception(f"Could not find cross toolchain {cross_prefix}")
result = subprocess.run(
["make", "-C", "proofs/hol_light/" + arch, "dump_bytecode"],
capture_output=True,
text=True,
check=True,
)
output_text = result.stdout
bytecode_dict = extract_bytecode_from_output(output_text)
for obj_name, bytecode in bytecode_dict.items():
ml_file = "proofs/hol_light/" + arch + "/proofs/" + obj_name + ".ml"
update_bytecode_in_proof_script(ml_file, bytecode)
def update_hol_light_bytecode(force_cross=()):
update_hol_light_bytecode_for_arch("aarch64", force_cross=force_cross)
update_hol_light_bytecode_for_arch("x86_64", force_cross=force_cross)
def gen_test_config(config_path, config_spec, default_config_content):
lines = default_config_content.split("\n")
references_start = None
references_end = None
for i, line in enumerate(lines):
if "/* References" in line:
references_start = i
elif (
references_start is not None
and references_end is None
and line.strip() == "*/"
):
references_end = i + 1
break
header = lines[:references_end]
header += list(gen_autogen_warning())
header.append("")
header.append("/*")
header.append(f" * Test configuration: {config_spec['description']}")
header.append(" *")
header.append(
" * This configuration differs from the default mldsa/mldsa_native_config.h in the following places:"
)
def spec_has_value(opt_value):
if not isinstance(opt_value, dict):
return True
else:
return "value" in opt_value.keys() or "content" in opt_value.keys()
for opt_name, opt_value in config_spec["defines"].items():
if not spec_has_value(opt_value):
continue
header.append(f" * - {opt_name}")
header.append(" */")
header.append("")
lines = header + lines[references_end:]
def locate_config_option(lines, opt_name):
define_re = re.compile(
rf"^\s*(/\*\s*)?#\s*(define|ifn?def)\s+{re.escape(opt_name)}\b"
)
impl_line = None
for i, line in enumerate(lines):
if define_re.search(line):
impl_line = i
break
if impl_line is None:
raise Exception(
f"Could not find config option {opt_name} in default config"
)
j = impl_line - 1
while j >= 0 and not lines[j].strip().endswith("*/"):
j -= 1
if j < 0:
raise Exception(
f"Could not find doxygen block before config option {opt_name}"
)
start = j + 1
i = start
while i < len(lines):
if lines[i].strip().startswith("/**"):
while i > start and lines[i - 1].strip() == "":
i -= 1
return (start, i)
i += 1
return (start, len(lines))
def replace_config_option(lines, opt_name, opt_value):
block_start, block_end = locate_config_option(lines, opt_name)
def content_from_value(value):
if value is True:
return [f"#define {opt_name}"]
elif value is False:
return [f"/* #define {opt_name} */"]
else:
return [f"#define {opt_name} {str(value)}"]
if isinstance(opt_value, dict):
if "content" in opt_value:
content = opt_value.get("content").split("\n")
elif "value" in opt_value:
content = content_from_value(opt_value["value"])
else:
content = lines[block_start:block_end]
if "comment" in opt_value:
comment = opt_value.get("comment").split("\n")
else:
comment = []
else:
content = content_from_value(opt_value)
comment = []
lines[block_start:block_end] = comment + content
for opt_name, opt_value in config_spec["defines"].items():
replace_config_option(lines, opt_name, opt_value)
content = "\n".join(lines)
update_file(config_path, content)
def gen_test_configs():
metadata = yaml.safe_load(read_file("test/configs/configs.yml"))
default_config = read_file("mldsa/mldsa_native_config.h")
for config_spec in metadata["configs"]:
gen_test_config(config_spec["path"], config_spec, default_config)
def gen_test_vectors(msg, ctx):
nrb_script = os.path.join(os.path.dirname(__file__), "notrandombytes")
def notrandombytes(num_bytes):
result = subprocess.run(
[sys.executable, nrb_script, str(num_bytes)],
capture_output=True,
encoding="utf-8",
)
if result.returncode != 0:
raise RuntimeError(f"notrandombytes failed: {result.stderr}")
return result.stdout.strip().upper()
def run_acvp(lvl, *args):
acvp_bin = f"./test/build/mldsa{lvl}/bin/acvp_mldsa{lvl}"
if not os.path.isfile(acvp_bin):
console.print(
f"[red]Error:[/] ACVP binary not found: {acvp_bin}\n"
f"Build it first: [bold]make acvp OPT=0[/]"
)
sys.exit(1)
result = subprocess.run(
[acvp_bin, *args],
capture_output=True,
encoding="utf-8",
)
if result.returncode != 0:
raise RuntimeError(f"acvp_mldsa{lvl} {args[0]} failed: {result.stderr}")
out = {}
for line in result.stdout.splitlines():
k, v = line.split("=", 1)
out[k] = v
return out
levels = [44, 65, 87]
msg_hex = msg.encode().hex().upper()
ctx_hex = ctx.encode().hex().upper()
mu_hex = "00" * 64
rnd = notrandombytes(32)
results = {}
for lvl in levels:
keygen_out = run_acvp(lvl, "keyGen", f"seed={rnd}")
sig_out = run_acvp(
lvl,
"sigGen",
f"message={msg_hex}",
f"sk={keygen_out['sk']}",
f"context={ctx_hex}",
f"rnd={rnd}",
)
extmu_out = run_acvp(
lvl,
"sigGenInternal",
f"message={mu_hex}",
f"sk={keygen_out['sk']}",
"externalMu=1",
f"rnd={rnd}",
)
prehash_out = run_acvp(
lvl,
"sigGenPreHashShake256",
f"message={msg_hex}",
f"context={ctx_hex}",
f"sk={keygen_out['sk']}",
f"rnd={rnd}",
)
results[lvl] = {
"pk": keygen_out["pk"],
"sk": keygen_out["sk"],
"sig": sig_out["signature"],
"sig_extmu": extmu_out["signature"],
"sig_pre_hash_shake256": prehash_out["signature"],
}
def hex_to_c_array(hex_str, indent=" "):
raw = bytes.fromhex(hex_str)
lines = []
for i in range(0, len(raw), 12):
chunk = raw[i : i + 12]
elems = ", ".join(f"0x{b:02x}" for b in chunk)
lines.append(f"{indent}{elems},")
if lines:
lines[-1] = lines[-1].rstrip(",")
return "\n".join(lines)
def c_string_literal(s):
return s.replace("\\", "\\\\").replace('"', '\\"')
def hex_len(hex_str):
return len(hex_str) // 2
def gen_msg_ctx_defines():
yield f'#define TEST_VECTOR_MSG "{c_string_literal(msg)}"'
yield "#define TEST_VECTOR_MSG_LEN (sizeof(TEST_VECTOR_MSG) - 1)"
yield f'#define TEST_VECTOR_CTX "{c_string_literal(ctx)}"'
yield "#define TEST_VECTOR_CTX_LEN (sizeof(TEST_VECTOR_CTX) - 1)"
yield ""
yield "/* notrandombytes(32) */"
yield f"static const uint8_t test_vector_rnd[{hex_len(rnd)}] = {{"
yield hex_to_c_array(rnd)
yield "};"
yield "/* 64 zero bytes */"
yield "static const uint8_t test_vector_mu[64] = {0};"
def gen_description():
yield "/*"
yield " * Generated with:"
yield f' * message: "{c_string_literal(msg)}"'
yield f' * context: "{c_string_literal(ctx)}"'
yield " * rnd: notrandombytes(32)"
yield " * mu: 64 zero bytes"
yield " */"
def gen_single():
yield from gen_header()
yield from gen_description()
yield ""
yield "#ifndef EXPECTED_TEST_VECTORS_H"
yield "#define EXPECTED_TEST_VECTORS_H"
yield ""
yield "#include <stdint.h>"
yield ""
yield from gen_msg_ctx_defines()
yield ""
first = True
for lvl in levels:
r = results[lvl]
directive = "#if" if first else "#elif"
yield f"{directive} MLD_CONFIG_PARAMETER_SET == {lvl}"
yield f"static const uint8_t test_vector_pk[{hex_len(r['pk'])}] = {{"
yield hex_to_c_array(r["pk"])
yield "};"
yield f"static const uint8_t test_vector_sk[{hex_len(r['sk'])}] = {{"
yield hex_to_c_array(r["sk"])
yield "};"
yield f"static const uint8_t test_vector_sig[{hex_len(r['sig'])}] = {{"
yield hex_to_c_array(r["sig"])
yield "};"
yield f"static const uint8_t test_vector_sig_extmu[{hex_len(r['sig_extmu'])}] = {{"
yield hex_to_c_array(r["sig_extmu"])
yield "};"
yield f"static const uint8_t test_vector_sig_pre_hash_shake256[{hex_len(r['sig_pre_hash_shake256'])}] = {{"
yield hex_to_c_array(r["sig_pre_hash_shake256"])
yield "};"
first = False
yield f"#endif /* MLD_CONFIG_PARAMETER_SET == {levels[-1]} */"
yield ""
yield "#endif /* !EXPECTED_TEST_VECTORS_H */"
yield ""
update_file(
"test/test_vectors/expected_test_vectors.h",
"\n".join(gen_single()),
)
def gen_multi():
yield from gen_header()
yield "/*"
yield " * Expected test vectors for multilevel builds."
yield " * Generated with per-level array names."
yield " */"
yield ""
yield from gen_description()
yield ""
yield "#ifndef EXPECTED_TEST_VECTORS_MULTILEVEL_H"
yield "#define EXPECTED_TEST_VECTORS_MULTILEVEL_H"
yield ""
yield "#include <stdint.h>"
yield ""
yield from gen_msg_ctx_defines()
yield ""
for lvl in levels:
r = results[lvl]
yield f"/* ML-DSA-{lvl} */"
yield f"static const uint8_t test_vector_pk_{lvl}[{hex_len(r['pk'])}] = {{"
yield hex_to_c_array(r["pk"])
yield "};"
yield f"static const uint8_t test_vector_sk_{lvl}[{hex_len(r['sk'])}] = {{"
yield hex_to_c_array(r["sk"])
yield "};"
yield f"static const uint8_t test_vector_sig_{lvl}[{hex_len(r['sig'])}] = {{"
yield hex_to_c_array(r["sig"])
yield "};"
yield f"static const uint8_t test_vector_sig_extmu_{lvl}[{hex_len(r['sig_extmu'])}] = {{"
yield hex_to_c_array(r["sig_extmu"])
yield "};"
yield f"static const uint8_t test_vector_sig_pre_hash_shake256_{lvl}[{hex_len(r['sig_pre_hash_shake256'])}] = {{"
yield hex_to_c_array(r["sig_pre_hash_shake256"])
yield "};"
yield ""
yield "#endif /* !EXPECTED_TEST_VECTORS_MULTILEVEL_H */"
yield ""
update_file(
"test/test_vectors/expected_test_vectors_multilevel.h",
"\n".join(gen_multi()),
)
ABI_CAPS = {
"AVX2": {
"asm_subdir": "x86_64",
"guard": "__AVX2__",
"syscap": "MLD_SYS_CAP_X86_64_AVX2",
"description": "AVX2",
"cflags": ["-mavx2", "-mbmi2"],
"aux_files": [],
},
"SHA3": {
"asm_subdir": "aarch64",
"guard": "__ARM_FEATURE_SHA3",
"syscap": "MLD_SYS_CAP_AARCH64_SHA3",
"description": "Armv8.4-A SHA3 (eor3, rax1, xar, bcax)",
"cflags": ["-march=armv8.4-a+sha3"],
"aux_files": [],
},
"MVE": {
"asm_subdir": "armv81m",
"guard": "__ARM_FEATURE_MVE",
"syscap": "MLD_SYS_CAP_ARMV81M_MVE",
"description": "Armv8.1-M MVE",
"cflags": ["-march=armv8.1-m.main+mve", "-mthumb"],
"aux_files": [
"test/abicheck/armv81m/callstub_armv81m.S",
"test/abicheck/armv81m/selftest_armv81m.S",
"mldsa/src/fips202/native/armv81m/src/state_extract_bytes_x4_mve.S",
"mldsa/src/fips202/native/armv81m/src/state_xor_bytes_x4_mve.S",
],
},
}
ARCH_CALLINGS = {
("aarch64", "AAPCS64"): {
"arch_guard": "MLD_SYS_AARCH64",
"extra_guards": [],
"asm_subdir": "aarch64",
"state_type": "struct aarch64_register_state",
"gpr_int_type": "uint64_t",
"fn_qualifier": "",
"init_func": "init_aarch64_register_state",
"stub_func": "asm_call_stub_aarch64",
"check_func": "check_aarch64_aapcs_compliance",
"reg_to_field": lambda reg: f"gpr[{int(reg[1:])}]",
},
("x86_64", "SysV"): {
"arch_guard": "MLD_SYS_X86_64",
"extra_guards": ["MLD_SYSV_ABI_SUPPORTED"],
"asm_subdir": "x86_64",
"state_type": "struct x86_64_register_state",
"gpr_int_type": "uint64_t",
"fn_qualifier": "MLD_SYSV_ABI",
"init_func": "init_x86_64_register_state",
"stub_func": "asm_call_stub_x86_64_sysv",
"check_func": "check_x86_64_sysv_compliance",
"reg_to_field": lambda reg: reg,
},
("armv81m", "AAPCS32"): {
"arch_guard": "MLD_SYS_ARMV81M_MVE",
"extra_guards": [],
"asm_subdir": "armv81m",
"state_type": "struct armv81m_register_state",
"gpr_int_type": "uint32_t",
"fn_qualifier": "",
"init_func": "init_armv81m_register_state",
"stub_func": "asm_call_stub_armv81m",
"check_func": "check_armv81m_aapcs32_compliance",
"reg_to_field": lambda reg: f"gpr[{int(reg[1:])}]",
},
}
def _abi_block(yaml_data, dev_source):
abi = yaml_data.get("ABI")
if abi is None:
raise ValueError(f"{dev_source}: YAML missing 'ABI:' block")
if not isinstance(abi, dict):
raise ValueError(
f"{dev_source}: YAML 'ABI:' must be a mapping, got {type(abi).__name__}"
)
return abi
def kernel_arch_calling(yaml_data, dev_source):
abi = _abi_block(yaml_data, dev_source)
arch = abi.get("Architecture")
calling = abi.get("CallingConvention")
if arch is None or calling is None:
raise ValueError(
f"{dev_source}: YAML 'ABI:' must set both 'Architecture:' and "
f"'CallingConvention:' (got Architecture={arch!r}, "
f"CallingConvention={calling!r})"
)
if (arch, calling) not in ARCH_CALLINGS:
known = ", ".join(f"({a},{c})" for (a, c) in sorted(ARCH_CALLINGS))
raise ValueError(
f"{dev_source}: unknown (Architecture, CallingConvention) pair "
f"({arch!r}, {calling!r}); expected one of: {known}. "
f"Add it to ARCH_CALLINGS in scripts/autogen."
)
return (arch, calling)
def kernel_features(yaml_data, dev_source):
abi = _abi_block(yaml_data, dev_source)
feats = abi.get("Features") or []
if not isinstance(feats, list):
raise ValueError(
f"{dev_source}: YAML 'ABI.Features:' must be a list, "
f"got {type(feats).__name__}"
)
for r in feats:
if r not in ABI_CAPS:
known = ", ".join(sorted(ABI_CAPS.keys()))
raise ValueError(
f"{dev_source}: unknown feature {r!r} in 'ABI.Features:'; "
f"expected one of: {known}. Add it to ABI_CAPS in scripts/autogen."
)
return feats
def kernel_registers(yaml_data, dev_source):
abi = _abi_block(yaml_data, dev_source)
abi_metadata_keys = {"Architecture", "CallingConvention", "Features"}
regs = {}
for key, value in abi.items():
if key in abi_metadata_keys:
continue
if not isinstance(value, dict) or "type" not in value:
raise ValueError(
f"{dev_source}: unexpected key {key!r} in 'ABI:' block; "
f"expected one of {sorted(abi_metadata_keys)} or a register "
f"entry with a 'type:' field"
)
regs[key] = value
return regs
def resolve_buffer_size(reg_info, abi_data):
size = reg_info.get("size_bytes")
if isinstance(size, str):
if size in abi_data:
ref_reg = abi_data[size]
if ref_reg.get("type") == "scalar" and "test_with" in ref_reg:
return ref_reg["test_with"]
raise ValueError(f"Buffer {reg_info} references non-scalar register {size}")
return int(size)
elif isinstance(size, int):
return size
raise ValueError(f"Cannot resolve buffer size from {reg_info}")
def gen_abicheck():
joblist_abicheck_extra = [
("keccak_f1600_x4_mve.S", "dev/fips202/armv81m/src", "", "armv81m"),
]
joblist = hol_light_asm_joblist() + joblist_abicheck_extra
jobs_by_arch_calling = {key: [] for key in ARCH_CALLINGS}
cap_to_files = {cap: [] for cap in ABI_CAPS}
for entry in sorted(joblist, key=lambda j: j[0]):
assembly_basename, dev_src_dir, _cflags, joblist_arch = entry
dev_source = f"{dev_src_dir}/{assembly_basename}"
yaml_data = extract_yaml_from_assembly(dev_source)
arch_calling = kernel_arch_calling(yaml_data, dev_source)
if arch_calling[0] != joblist_arch:
raise ValueError(
f"{dev_source}: YAML 'ABI.Architecture: {arch_calling[0]}' "
f"disagrees with joblist arch {joblist_arch!r}"
)
upstream_src_dir = upstream_src_dir_for_dev_dir(dev_src_dir)
for cap in kernel_features(yaml_data, dev_source):
cap_to_files[cap].append(f"{upstream_src_dir}/{assembly_basename}")
jobs_by_arch_calling[arch_calling].append((entry, yaml_data))
all_groups = []
generated_basenames_by_subdir = {}
for arch_calling, jobs in jobs_by_arch_calling.items():
config = ARCH_CALLINGS[arch_calling]
arch_guard = config["arch_guard"]
extra_guards = config["extra_guards"]
generated_functions = []
generated_basenames_by_subdir.setdefault(config["asm_subdir"], set())
for (assembly_basename, dev_src_dir, _cflags, _joblist_arch), yaml_data in jobs:
dev_source = f"{dev_src_dir}/{assembly_basename}"
features = kernel_features(yaml_data, dev_source)
registers = kernel_registers(yaml_data, dev_source)
feature_guards = [
ABI_CAPS[f]["guard"] for f in features if ABI_CAPS[f]["guard"]
]
function_name = yaml_data.get("Name")
c_function_name = "mld_" + function_name
check_name = function_name
def gen_c_test(
function_name=function_name,
check_name=check_name,
c_function_name=c_function_name,
yaml_data=yaml_data,
features=features,
registers=registers,
feature_guards=feature_guards,
config=config,
arch_guard=arch_guard,
extra_guards=extra_guards,
):
yield from gen_header()
yield "#include <stdio.h>"
yield ""
yield f'#include "../abicheck_{config["asm_subdir"]}.h"'
yield f'#include "../checks_{config["asm_subdir"]}_all.h"'
yield ""
conds = (
[f"defined({arch_guard})"]
+ [f"defined({g})" for g in extra_guards]
+ [f"defined({g})" for g in feature_guards]
)
cond = " && ".join(conds)
yield f"#if {cond}"
yield ""
yield '#include "../../../notrandombytes/notrandombytes.h"'
yield ""
yield f"typedef {config['state_type']} reg_state;"
yield ""
qualifier = config["fn_qualifier"]
yield f"{qualifier} {yaml_data.get('Signature')};"
yield ""
yield f"int check_{check_name}(void)"
yield "{"
yield " int test_iter;"
yield " reg_state input_state, output_state;"
yield " int violations;"
sorted_registers = sorted(registers.items())
buffer_info = []
for reg_name, reg_info in sorted_registers:
if reg_info.get("type") == "buffer":
size_bytes = resolve_buffer_size(reg_info, registers)
buffer_name = f"buf_{reg_name}"
description = reg_info.get("description", "")
yield f" MLD_ALIGN uint8_t {buffer_name}[{size_bytes}]; /* {description} */"
buffer_info.append((reg_name, buffer_name, size_bytes))
for cap in features:
cap_enum = ABI_CAPS[cap]["syscap"]
if cap_enum is None:
continue
cap_desc = ABI_CAPS[cap]["description"]
yield ""
yield f" if (!mld_sys_check_capability({cap_enum}))"
yield " {"
yield (
f' fprintf(stderr, "ABI check {check_name}: '
f'host lacks {cap_desc}, skipping\\n");'
)
yield " return MLD_ABICHECK_SKIPPED;"
yield " }"
yield ""
yield " for (test_iter = 0; test_iter < MLD_ABICHECK_NUM_TESTS; test_iter++)"
yield " {"
yield " /* Initialize random register state */"
yield f" {config['init_func']}(&input_state);"
yield ""
for reg_name, buffer_name, size_bytes in buffer_info:
yield f" randombytes({buffer_name}, {size_bytes});"
yield ""
yield " /* Set up register state for function arguments */"
reg_to_field = config["reg_to_field"]
for reg_name, reg_info in sorted_registers:
field = reg_to_field(reg_name)
if reg_info.get("type") == "buffer":
yield f" input_state.{field} = ({config['gpr_int_type']}){f'buf_{reg_name}'};"
elif reg_info.get("type") == "scalar":
test_with = reg_info.get("test_with")
if test_with:
yield f" input_state.{field} = {test_with};"
yield ""
yield " /* Call function through ABI test stub */"
yield (
f" {config['stub_func']}(&input_state, &output_state, "
f"({qualifier} void (*)(void)){c_function_name});"
)
yield ""
yield " /* Check ABI compliance */"
yield (
f" violations = {config['check_func']}("
f"&input_state, &output_state, MLD_ABICHECK_VERBOSE);"
)
yield " if (violations > 0) {"
yield f' fprintf(stderr, "ABI test FAILED for {function_name} (iteration %d): %d violations\\n",'
yield " test_iter + 1, violations);"
yield " return MLD_ABICHECK_FAILED;"
yield " }"
yield " }"
yield ""
yield " return MLD_ABICHECK_PASSED;"
yield "}"
yield ""
yield f"#endif /* {cond} */"
yield ""
output_file = (
f"test/abicheck/{config['asm_subdir']}/checks/check_{check_name}.c"
)
update_file(output_file, "\n".join(gen_c_test()), force_format=True)
generated_functions.append((check_name, feature_guards))
generated_basenames_by_subdir[config["asm_subdir"]].add(
f"check_{check_name}.c"
)
all_groups.append((arch_calling, arch_guard, extra_guards, generated_functions))
for asm_subdir, generated_basenames in generated_basenames_by_subdir.items():
subdir = f"test/abicheck/{asm_subdir}/checks"
for f in get_files(f"{subdir}/check_*.c"):
if os.path.basename(f) not in generated_basenames:
update_via_remove(f)
groups_by_arch = {}
asm_subdir_by_arch = {}
for arch_calling, arch_guard, extra_guards, functions in all_groups:
groups_by_arch.setdefault(arch_guard, []).append(
(arch_calling, extra_guards, functions)
)
asm_subdir_by_arch[arch_guard] = ARCH_CALLINGS[arch_calling]["asm_subdir"]
def gen_checks_arch_header(arch_guard, groups, asm_subdir):
guard_macro = f"MLD_TEST_ABICHECK_CHECKS_{asm_subdir.upper()}_ALL_H"
yield from gen_header()
yield ""
yield f"#ifndef {guard_macro}"
yield f"#define {guard_macro}"
yield ""
yield "#include <stddef.h>"
yield '#include "../abicheck_common.h"'
yield ""
yield f"#if defined({arch_guard})"
yield ""
def emit_feature_grouped(functions, render):
i = 0
while i < len(functions):
guards = functions[i][1]
j = i
while j < len(functions) and functions[j][1] == guards:
j += 1
for g in guards:
yield f"#if defined({g})"
for func_name, _ in functions[i:j]:
yield render(func_name)
for _ in guards:
yield "#endif"
i = j
for _arch_calling, extra_guards, functions in groups:
if extra_guards:
inner = " && ".join(f"defined({g})" for g in extra_guards)
yield f"#if {inner}"
yield from emit_feature_grouped(
functions, lambda fn: f"int check_{fn}(void);"
)
if extra_guards:
yield f"#endif /* {inner} */"
if any(functions for _, _, functions in groups):
yield ""
yield "static const abicheck_entry_t all_checks[] = {"
for _arch_calling, extra_guards, functions in groups:
if not functions:
continue
if extra_guards:
inner = " && ".join(f"defined({g})" for g in extra_guards)
yield f"#if {inner}"
yield from emit_feature_grouped(
functions, lambda fn: f' {{"{fn}", check_{fn}}},'
)
if extra_guards:
yield f"#endif /* {inner} */"
yield " {NULL, NULL} /* Sentinel */"
yield "};"
yield ""
yield f"#endif /* defined({arch_guard}) */"
yield ""
yield f"#endif /* !{guard_macro} */"
yield ""
for arch_guard, groups in groups_by_arch.items():
asm_subdir = asm_subdir_by_arch[arch_guard]
update_file(
f"test/abicheck/{asm_subdir}/checks_{asm_subdir}_all.h",
"\n".join(gen_checks_arch_header(arch_guard, groups, asm_subdir)),
force_format=True,
)
caps_by_subdir = {}
for cap in sorted(ABI_CAPS):
caps_by_subdir.setdefault(ABI_CAPS[cap]["asm_subdir"], []).append(cap)
def gen_features_mk(asm_subdir, caps):
yield from gen_yaml_header()
yield from gen_yaml_autogen_warning()
yield "#"
yield "# Edit the YAML 'ABI.Features:' list in dev/<arch>/src/<kernel>.S"
yield "# and re-run scripts/autogen instead."
yield "#"
yield "# For each capability declared by a kernel's ABI.Features list, this"
yield "# file appends the capability's CFLAGS to that kernel's .S object"
yield "# under mldsa/src/."
yield ""
yield "# Default each cap's file list to empty so the unconditional appends"
yield "# below are safe even when a cap has no kernels on this arch."
for cap in caps:
yield f"ABICHECK_REQ_{cap}_FILES :="
for cap in caps:
files = sorted(
set(cap_to_files[cap]) | set(ABI_CAPS[cap].get("aux_files", []))
)
if not files:
continue
cflags = " ".join(ABI_CAPS[cap]["cflags"])
yield ""
yield f"# {cap}: {ABI_CAPS[cap]['description']}"
yield f"ABICHECK_REQ_{cap}_FILES := \\"
for i, f in enumerate(files):
sep = " \\" if i + 1 < len(files) else ""
yield f" {f}{sep}"
yield (
f"ABICHECK_REQ_{cap}_OBJS := "
f"$(call MAKE_OBJS,$(ABICHECK_DIR),$(ABICHECK_REQ_{cap}_FILES))"
)
yield f"$(ABICHECK_REQ_{cap}_OBJS): CFLAGS += {cflags}"
yield ""
for asm_subdir, caps in caps_by_subdir.items():
update_file(
f"test/abicheck/{asm_subdir}/abicheck_{asm_subdir}.mk",
"\n".join(gen_features_mk(asm_subdir, caps)),
)
def gen_abicheck_mk():
yield from gen_yaml_header()
yield from gen_yaml_autogen_warning()
yield "#"
yield "# Includes the per-arch abicheck_<arch>.mk, each of which appends"
yield "# its capabilities' CFLAGS to the matching .S objects."
yield ""
for asm_subdir in sorted(caps_by_subdir):
yield f"include test/abicheck/{asm_subdir}/abicheck_{asm_subdir}.mk"
yield ""
update_file("test/abicheck/abicheck.mk", "\n".join(gen_abicheck_mk()))
def _main():
slothy_choices = [
"ntt_aarch64_asm",
"intt_aarch64_asm",
"mld_polyvecl_pointwise_acc_montgomery_l4_aarch64_asm",
"mld_polyvecl_pointwise_acc_montgomery_l5_aarch64_asm",
"mld_polyvecl_pointwise_acc_montgomery_l7_aarch64_asm",
"pointwise_montgomery_aarch64_asm",
"poly_caddq_aarch64_asm",
"poly_chknorm_aarch64_asm",
"poly_decompose_32_aarch64_asm",
"poly_decompose_88_aarch64_asm",
"poly_use_hint_32_aarch64_asm",
"poly_use_hint_88_aarch64_asm",
"polyz_unpack_17_aarch64_asm",
"polyz_unpack_19_aarch64_asm",
"rej_uniform_aarch64_asm",
"rej_uniform_eta2_aarch64_asm",
"rej_uniform_eta4_aarch64_asm",
]
parser = argparse.ArgumentParser(
formatter_class=argparse.ArgumentDefaultsHelpFormatter
)
parser.add_argument("--dry-run", default=False, action="store_true")
parser.add_argument(
"--update-hol-light-bytecode", default=False, action="store_true"
)
parser.add_argument("--slothy", nargs="*", default=None, choices=slothy_choices)
parser.add_argument("--aarch64-clean", default=False, action="store_true")
parser.add_argument("--no-simplify", default=False, action="store_true")
parser.add_argument(
"--force-cross",
nargs="*",
default=None,
choices=sorted(FORCE_CROSS_ALL_ARCHES),
metavar="ARCH",
help="Treat a missing cross toolchain as an error instead of silently "
"skipping. Without a value, requires all architectures "
f"({', '.join(sorted(FORCE_CROSS_ALL_ARCHES))}); or pass a "
"space-separated list to require only those, "
"e.g. --force-cross aarch64 x86_64.",
)
parser.add_argument(
"--x86-64-syntax",
type=str,
choices=["att", "intel"],
default="att",
help="Assembly syntax for x86_64 disassembly output (att or intel)",
)
parser.add_argument(
"--test-vectors",
default=False,
action="store_true",
help="Generate expected test vectors using ACVP binaries",
)
parser.add_argument(
"--test-vectors-msg",
type=str,
default="This is a test message for ML-DSA digital signature algorithm!",
help="Message to use for test vector generation",
)
parser.add_argument(
"--test-vectors-ctx",
type=str,
default="test_context_123",
help="Context to use for test vector generation",
)
args = parser.parse_args()
os.chdir(os.path.join(os.path.dirname(__file__), ".."))
if args.slothy == []:
args.slothy = slothy_choices
force_cross = resolve_force_cross(args.force_cross)
def sync_backends():
synchronize_backends(
clean=args.aarch64_clean,
no_simplify=args.no_simplify,
force_cross=force_cross,
x86_64_syntax=args.x86_64_syntax,
)
def sync_backends_final():
synchronize_backends(
clean=args.aarch64_clean,
delete=True,
force_cross=force_cross,
no_simplify=args.no_simplify,
x86_64_syntax=args.x86_64_syntax,
)
def gen_zeta_tables():
gen_c_zeta_file()
gen_aarch64_zeta_file()
gen_aarch64_hol_light_zeta_file()
gen_aarch64_rej_uniform_table()
gen_hol_light_rej_uniform_table()
gen_aarch64_rej_uniform_eta_table()
gen_aarch64_polyz_unpack_table()
gen_hol_light_polyz_unpack_table()
gen_avx2_hol_light_zeta_file()
gen_avx2_zeta_file()
gen_avx2_rej_uniform_table()
gen_hol_light_keccak_constants_file()
gen_aarch64_keccak_constants_c_file()
gen_avx2_keccak_constants_c_file()
gen_avx2_keccak_hol_light_constants_file()
def gen_monolithic():
gen_monolithic_source_file()
gen_monolithic_asm_file()
hol_light_asm_supported = platform.machine().lower() in [
"arm64",
"aarch64",
"x86_64",
]
steps = [
("Generate citations", gen_citations),
("Generate OQS META.yml files", gen_oqs_meta_files),
("Normalize assembly macro syntax", normalize_asm_macro_syntax),
(
"Generate SLOTHY optimized assembly",
lambda: gen_slothy(args.slothy),
args.slothy is not None and not args.dry_run,
),
("Check assembly register aliases", check_asm_register_aliases),
("Check assembly loop labels", check_asm_loop_labels),
("Generate zeta and lookup tables", gen_zeta_tables),
("Generate HOL Light assembly", gen_hol_light_asm, hol_light_asm_supported),
(
"Check HOL Light assembly is autogenerated",
check_hol_light_asm_autogenerated,
),
("Synchronize backends", sync_backends),
("Generate header guards", gen_header_guards),
("Complete final backend synchronization", sync_backends_final),
(
"Update HOL Light bytecode",
partial(update_hol_light_bytecode, force_cross=force_cross),
args.update_hol_light_bytecode,
),
("Generate monolithic source files", gen_monolithic),
("Generate undefs", gen_undefs),
("Generate test configs", gen_test_configs),
(
"Generate test vectors",
lambda: gen_test_vectors(args.test_vectors_msg, args.test_vectors_ctx),
args.test_vectors,
),
("Generate ABI checker tests", gen_abicheck),
("Check macro typos", check_macro_typos),
("Generate preprocessor comments", gen_preprocessor_comments),
("Format files", lambda: format_files(args.dry_run)),
]
if args.test_vectors:
steps = [s for s in steps if s[0] in ("Generate test vectors", "Format files")]
global _progress, _main_task
with Progress(
BarColumn(),
TaskProgressColumn(),
TimeElapsedColumn(),
TextColumn("{task.description}"),
console=console,
) as progress:
_progress = progress
_main_task = progress.add_task("autogen", total=len(steps) + 1)
for step in steps:
desc, func = step[0], step[1]
enabled = step[2] if len(step) > 2 else True
high_level_task(desc)
if enabled:
func()
high_level_status(desc, skipped=not enabled)
high_level_task("Write files")
finalize(args.dry_run)
txt = (
"Finalize and check files are up to date"
if args.dry_run
else "Finalize and write files"
)
high_level_status(txt)
_progress.update(_main_task, description="[green] Done ✓[/]")
_progress = None
return print_check_errors()
if __name__ == "__main__":
sys.exit(0 if _main() else 1)