Reduce SHA-512 stack usage with a rolling message schedule

This commit is contained in:
mikecarper committed 2026-10-04 02:04:16 -07:00
1 parent a1fa553ebe
commit d35145d59e
2 files changed
+106 -26

No files matched your search

+26 -10
View File
@@ -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
+80 -16
View File
@@ -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(