| ## @file | |
| # Generate precomputed SLH-DSA-SHAKE-256s signatures for unit test vectors. | |
| # | |
| # Copyright (c) 2026, Intel Corporation. All rights reserved.<BR> | |
| # SPDX-License-Identifier: BSD-2-Clause-Patent | |
| # | |
| ## | |
| """Script to generate SLH-DSA-SHAKE-256s test signatures using OpenSSL 3.5+. | |
| This script extracts the private key from SlhDsaTestVectors.h (or accepts a PEM file) | |
| and generates precomputed signatures for unit test messages using OpenSSL CLI. | |
| The signatures are formatted as C constant arrays suitable for inclusion in | |
| SlhDsaTestVectors.h. | |
| """ | |
| import argparse | |
| import os | |
| import re | |
| import subprocess | |
| import tempfile | |
| DEFAULT_MESSAGE = b"Test message for SLH-DSA signing and verification" | |
| DEFAULT_CONTEXT = b"SLH-DSA test context" | |
| SECOND_MESSAGE = b"Second message" | |
| MAX_CONTEXT = bytes(range(255)) | |
| def format_c_array(name: str, data: bytes, comment: str = "") -> str: | |
| """Format binary data as a C constant byte array.""" | |
| lines = [] | |
| if comment: | |
| lines.append(f"//\n// {comment}\n//") | |
| lines.append(f"GLOBAL_REMOVE_IF_UNREFERENCED CONST UINT8 {name}[] = {{") | |
| for i in range(0, len(data), 12): | |
| chunk = data[i:i + 12] | |
| hex_vals = ", ".join(f"0x{b:02x}" for b in chunk) | |
| lines.append(f" {hex_vals},") | |
| lines.append("};\n") | |
| return "\n".join(lines) | |
| def sign_message( | |
| openssl_bin: str, | |
| key_file: str, | |
| message: bytes, | |
| context: bytes = None | |
| ) -> bytes: | |
| """Sign a message using OpenSSL CLI with SLH-DSA-SHAKE-256s.""" | |
| with tempfile.NamedTemporaryFile(delete=False) as msg_f: | |
| msg_f.write(message) | |
| msg_path = msg_f.name | |
| with tempfile.NamedTemporaryFile(delete=False) as sig_f: | |
| sig_path = sig_f.name | |
| try: | |
| cmd = [ | |
| openssl_bin, | |
| "pkeyutl", | |
| "-sign", | |
| "-rawin", | |
| "-inkey", | |
| key_file, | |
| "-in", | |
| msg_path, | |
| "-out", | |
| sig_path, | |
| ] | |
| if context is not None and len(context) > 0: | |
| cmd.extend(["-pkeyopt", f"context-string:{context.decode('latin1')}"]) | |
| subprocess.run(cmd, check=True, capture_output=True) | |
| with open(sig_path, "rb") as f: | |
| signature = f.read() | |
| return signature | |
| finally: | |
| if os.path.exists(msg_path): | |
| os.remove(msg_path) | |
| if os.path.exists(sig_path): | |
| os.remove(sig_path) | |
| def main(): | |
| parser = argparse.ArgumentParser( | |
| description="Generate precomputed SLH-DSA test signatures." | |
| ) | |
| parser.add_argument( | |
| "--openssl", | |
| default="openssl", | |
| help="Path to OpenSSL 3.5+ binary (default: openssl)", | |
| ) | |
| parser.add_argument( | |
| "--key", | |
| default=None, | |
| help="Path to SLH-DSA-SHAKE-256s PEM private key file (optional, extracted from SlhDsaTestVectors.h if omitted)", | |
| ) | |
| parser.add_argument( | |
| "--output", | |
| default=None, | |
| help="Output file for C arrays (default: stdout)", | |
| ) | |
| args = parser.parse_args() | |
| temp_key_file = None | |
| key_path = args.key | |
| if not key_path: | |
| # Extract PEM key from SlhDsaTestVectors.h | |
| header_path = os.path.join( | |
| os.path.dirname(__file__), "SlhDsaTestVectors.h" | |
| ) | |
| if not os.path.exists(header_path): | |
| raise FileNotFoundError(f"Cannot find {header_path}") | |
| with open(header_path, "r") as f: | |
| content = f.read() | |
| match = re.search( | |
| r"mSlhDsaShake256sTestPemKey\[\]\s*=\s*\{([^}]+)\};", content | |
| ) | |
| if not match: | |
| raise ValueError("mSlhDsaShake256sTestPemKey array not found") | |
| hex_bytes = [ | |
| int(x.strip(), 16) | |
| for x in match.group(1).split(",") | |
| if x.strip().startswith("0x") | |
| ] | |
| pem_data = bytes(hex_bytes) | |
| with tempfile.NamedTemporaryFile( | |
| delete=False, suffix=".pem" | |
| ) as key_f: | |
| key_f.write(pem_data) | |
| temp_key_file = key_f.name | |
| key_path = temp_key_file | |
| try: | |
| results = [] | |
| # 1. Default signature (no context) | |
| sig_default = sign_message( | |
| args.openssl, key_path, DEFAULT_MESSAGE, context=None | |
| ) | |
| results.append( | |
| format_c_array( | |
| "mSlhDsaShake256sTestSignature", | |
| sig_default, | |
| "Precomputed signature for mSlhDsaTestMessage (no context)", | |
| ) | |
| ) | |
| # 2. Context-bound signature | |
| sig_ctx = sign_message( | |
| args.openssl, key_path, DEFAULT_MESSAGE, context=DEFAULT_CONTEXT | |
| ) | |
| results.append( | |
| format_c_array( | |
| "mSlhDsaShake256sTestContextSignature", | |
| sig_ctx, | |
| "Precomputed signature for mSlhDsaTestMessage with mSlhDsaTestContext", | |
| ) | |
| ) | |
| # 3. Empty message signature | |
| sig_empty = sign_message( | |
| args.openssl, key_path, b"", context=None | |
| ) | |
| results.append( | |
| format_c_array( | |
| "mSlhDsaShake256sTestEmptyMsgSignature", | |
| sig_empty, | |
| "Precomputed signature for empty message (no context)", | |
| ) | |
| ) | |
| # 4. Max context signature | |
| sig_max_ctx = sign_message( | |
| args.openssl, key_path, DEFAULT_MESSAGE, context=MAX_CONTEXT | |
| ) | |
| results.append( | |
| format_c_array( | |
| "mSlhDsaShake256sTestMaxContextSignature", | |
| sig_max_ctx, | |
| "Precomputed signature for mSlhDsaTestMessage with 255-byte max context", | |
| ) | |
| ) | |
| # 5. Second message signature | |
| sig_msg2 = sign_message( | |
| args.openssl, key_path, SECOND_MESSAGE, context=None | |
| ) | |
| results.append( | |
| format_c_array( | |
| "mSlhDsaShake256sTestMsg2Signature", | |
| sig_msg2, | |
| "Precomputed signature for Second message (no context)", | |
| ) | |
| ) | |
| output_text = "\n".join(results) | |
| if args.output: | |
| with open(args.output, "w") as f: | |
| f.write(output_text) | |
| print(f"Signatures written to {args.output}") | |
| else: | |
| print(output_text) | |
| finally: | |
| if temp_key_file and os.path.exists(temp_key_file): | |
| os.remove(temp_key_file) | |
| if __name__ == "__main__": | |
| main() |