From d35145d59e66b1fe5a65407f2b78e9555a1015f1 Mon Sep 17 00:00:00 2001 From: mikecarper Date: Sun, 4 Oct 2026 02:04:16 -0700 Subject: [PATCH] Reduce SHA-512 stack usage with a rolling message schedule --- lib/ed25519/sha512.c | 36 ++++++++++---- test/test_ed25519_compact.py | 96 ++++++++++++++++++++++++++++++------ 2 files changed, 106 insertions(+), 26 deletions(-) diff --git a/lib/ed25519/sha512.c b/lib/ed25519/sha512.c index 61bcd1205..e07fb92b2 100644 --- a/lib/ed25519/sha512.c +++ b/lib/ed25519/sha512.c @@ -90,7 +90,7 @@ static const uint64_t K[80] = { /* compress 1024-bits */ static int sha512_compress(sha512_context *md, unsigned char *buf) { - uint64_t S[8], W[80], t0, t1; + uint64_t S[8], W[16], t0, t1; int i; /* copy state into S */ @@ -103,17 +103,25 @@ static int sha512_compress(sha512_context *md, unsigned char *buf) LOAD64H(W[i], buf + (8*i)); } - /* fill W[16..79] */ - for (i = 16; i < 80; i++) { - W[i] = Gamma1(W[i - 2]) + W[i - 7] + Gamma0(W[i - 15]) + W[i - 16]; - } - /* Compress */ + /* Each schedule word only needs the previous sixteen. Expand it before + its round, replacing W[i - 16] after reading all dependencies. + The schedule remains local and every index depends only on the fixed + round number. This saves 512 bytes of stack in both round paths. */ + #define SCHEDULE(i) \ + do { \ + const int round = (i); \ + W[round & 15] = Gamma1(W[(round - 2) & 15]) \ + + W[(round - 7) & 15] + Gamma0(W[(round - 15) & 15]) \ + + W[round & 15]; \ + } while (0) #define RND(a,b,c,d,e,f,g,h,i) \ - t0 = h + Sigma1(e) + Ch(e, f, g) + K[i] + W[i]; \ - t1 = Sigma0(a) + Maj(a, b, c);\ - d += t0; \ - h = t0 + t1; + do { \ + t0 = h + Sigma1(e) + Ch(e, f, g) + K[(i)] + W[(i) & 15]; \ + t1 = Sigma0(a) + Maj(a, b, c); \ + d += t0; \ + h = t0 + t1; \ + } while (0) #if defined(ED25519_COMPACT_SHA512) /* Same 80 rounds, with a fixed state rotation instead of eight copies of @@ -122,6 +130,7 @@ static int sha512_compress(sha512_context *md, unsigned char *buf) for (i = 0; i < 80; ++i) { int j; uint64_t next; + if (i >= 16) SCHEDULE(i); RND(S[0],S[1],S[2],S[3],S[4],S[5],S[6],S[7],i); next = S[7]; for (j = 7; j > 0; --j) S[j] = S[j - 1]; @@ -129,6 +138,12 @@ static int sha512_compress(sha512_context *md, unsigned char *buf) } #else for (i = 0; i < 80; i += 8) { + /* Expand this block in order before the eight unrolled rounds. None + of their schedule slots is overwritten by another word in the block. */ + if (i >= 16) { + int j; + for (j = 0; j < 8; ++j) SCHEDULE(i + j); + } RND(S[0],S[1],S[2],S[3],S[4],S[5],S[6],S[7],i+0); RND(S[7],S[0],S[1],S[2],S[3],S[4],S[5],S[6],i+1); RND(S[6],S[7],S[0],S[1],S[2],S[3],S[4],S[5],i+2); @@ -141,6 +156,7 @@ static int sha512_compress(sha512_context *md, unsigned char *buf) #endif #undef RND + #undef SCHEDULE diff --git a/test/test_ed25519_compact.py b/test/test_ed25519_compact.py index 92b375532..aed61e2c0 100644 --- a/test/test_ed25519_compact.py +++ b/test/test_ed25519_compact.py @@ -1,5 +1,6 @@ import ctypes import hashlib +import random import subprocess import tempfile import unittest @@ -23,22 +24,84 @@ SOURCES = [ ] +class SHA512Context(ctypes.Structure): + _fields_ = [ + ("length", ctypes.c_uint64), + ("state", ctypes.c_uint64 * 8), + ("curlen", ctypes.c_size_t), + ("buf", ctypes.c_ubyte * 128), + ] + + class Ed25519CompactTest(unittest.TestCase): - def test_compact_sha512_matches_hashlib_at_block_boundaries(self): + def build_sha512_library(self, temp_dir, compact): + library = Path(temp_dir) / f"sha512-{int(compact)}.so" + flags = ["-DED25519_COMPACT_SHA512=1"] if compact else [] + subprocess.run(["cc", "-std=c99", "-O2", "-shared", "-fPIC", + *flags, str(ED25519 / "sha512.c"), + "-o", str(library)], check=True) + sha = ctypes.CDLL(str(library)) + sha.sha512.argtypes = [ctypes.c_void_p, ctypes.c_size_t, ctypes.c_void_p] + sha.sha512.restype = ctypes.c_int + sha.sha512_init.argtypes = [ctypes.POINTER(SHA512Context)] + sha.sha512_init.restype = ctypes.c_int + sha.sha512_update.argtypes = [ctypes.POINTER(SHA512Context), + ctypes.c_void_p, ctypes.c_size_t] + sha.sha512_update.restype = ctypes.c_int + sha.sha512_final.argtypes = [ctypes.POINTER(SHA512Context), ctypes.c_void_p] + sha.sha512_final.restype = ctypes.c_int + return sha + + def test_sha512_matches_hashlib_at_block_boundaries(self): with tempfile.TemporaryDirectory() as temp_dir: - library = Path(temp_dir) / "sha512.so" - subprocess.run(["cc", "-std=c99", "-O2", "-shared", "-fPIC", - "-DED25519_COMPACT_SHA512=1", str(ED25519 / "sha512.c"), - "-o", str(library)], check=True) - sha = ctypes.CDLL(str(library)).sha512 - sha.argtypes = [ctypes.c_void_p, ctypes.c_size_t, ctypes.c_void_p] - sha.restype = ctypes.c_int - for length in (0, 1, 63, 64, 111, 112, 127, 128, 129, 255, 256, 1024, 4097): - data = bytes((i * 73 + 19) % 256 for i in range(length)) - output = ctypes.create_string_buffer(64) - with self.subTest(length=length): - self.assertEqual(sha(data, len(data), output), 0) - self.assertEqual(output.raw, hashlib.sha512(data).digest()) + for compact in (False, True): + sha = self.build_sha512_library(temp_dir, compact) + for length in (0, 1, 63, 64, 111, 112, 127, 128, 129, + 239, 240, 255, 256, 257, 1024, 4097): + data = bytes((i * 73 + 19) % 256 for i in range(length)) + output = ctypes.create_string_buffer(64) + with self.subTest(compact=compact, length=length): + self.assertEqual(sha.sha512(data, len(data), output), 0) + self.assertEqual(output.raw, hashlib.sha512(data).digest()) + + def test_sha512_streaming_contexts_match_hashlib(self): + with tempfile.TemporaryDirectory() as temp_dir: + for compact in (False, True): + sha = self.build_sha512_library(temp_dir, compact) + rng = random.Random(0x512) + lengths = [0, 1, 111, 112, 127, 128, 129, 255, 256, 257, + 4096, 16385] + lengths.extend(rng.randrange(1, 32769) for _ in range(20)) + for length in lengths: + with self.subTest(compact=compact, length=length): + # Interleave two live contexts and irregular updates, + # including empty updates and multiple full blocks. + messages = [bytes(rng.randrange(256) for _ in range(n)) + for n in (length, length + 137)] + contexts = [SHA512Context(), SHA512Context()] + positions = [0, 0] + for context in contexts: + self.assertEqual(sha.sha512_init(ctypes.byref(context)), 0) + self.assertEqual(sha.sha512_update(ctypes.byref(context), + b"", 0), 0) + while any(position < len(message) + for position, message in zip(positions, messages)): + for index, (context, message) in enumerate(zip(contexts, messages)): + position = positions[index] + if position == len(message): + continue + size = rng.choice((1, 7, 63, 64, 111, 112, 127, + 128, 129, 257, 1024)) + chunk = message[position:position + size] + self.assertEqual(sha.sha512_update(ctypes.byref(context), + chunk, len(chunk)), 0) + positions[index] += len(chunk) + for context, message in zip(contexts, messages): + output = ctypes.create_string_buffer(64) + self.assertEqual(sha.sha512_update(ctypes.byref(context), + b"", 0), 0) + self.assertEqual(sha.sha512_final(ctypes.byref(context), output), 0) + self.assertEqual(output.raw, hashlib.sha512(message).digest()) def test_stm32_builds_enable_compact_base_table(self): platformio = (ROOT / "platformio.ini").read_text() @@ -50,8 +113,9 @@ class Ed25519CompactTest(unittest.TestCase): for label, extra_flags in ( ("standard", []), ("compact", ["-DED25519_COMPACT_BASE=1"]), - ("compact_sha512", ["-DED25519_COMPACT_BASE=1", - "-DED25519_COMPACT_SHA512=1"]), + ("compact_sha512", ["-DED25519_COMPACT_SHA512=1"]), + ("compact_base_sha512", ["-DED25519_COMPACT_BASE=1", + "-DED25519_COMPACT_SHA512=1"]), ): executable = Path(temp_dir) / label subprocess.run(