mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-11-04 12:07:42 +00:00
240 lines
12 KiB
Python
240 lines
12 KiB
Python
"""Exercise both Ed25519 signing hash backends with the real Crypto verifier."""
|
|
from pathlib import Path
|
|
import hashlib
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import tempfile
|
|
import unittest
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
ED25519 = ROOT / "lib/ed25519"
|
|
LENGTHS = (0, 1, 63, 64, 111, 112, 127, 128, 129, 239, 240, 255,
|
|
256, 257, 1024, 4097, 16385)
|
|
|
|
HARNESS = r'''
|
|
#include "ed_sha512.h"
|
|
#include "ed_25519.h"
|
|
#include <Ed25519.h>
|
|
#include <array>
|
|
#include <cassert>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <cstring>
|
|
#include <vector>
|
|
#if defined(_WIN32)
|
|
#include <RNG.h>
|
|
// PE linkers resolve the library's unused key-generation references before
|
|
// discarding them. Verification and seeded signing must never call this API.
|
|
RNGClass::RNGClass() {}
|
|
RNGClass::~RNGClass() {}
|
|
void RNGClass::rand(uint8_t*, size_t) { std::abort(); }
|
|
RNGClass RNG;
|
|
#endif
|
|
|
|
static void printHex(const unsigned char* bytes, size_t length) {
|
|
for (size_t i = 0; i < length; ++i) printf("%02x", bytes[i]);
|
|
putchar('\n');
|
|
}
|
|
static const unsigned char* bytes(const std::vector<unsigned char>& data) {
|
|
static const unsigned char empty = 0;
|
|
return data.empty() ? &empty : data.data();
|
|
}
|
|
static std::vector<unsigned char> message(size_t length, unsigned salt) {
|
|
std::vector<unsigned char> result(length);
|
|
for (size_t i = 0; i < length; ++i) result[i] = (i * 73 + salt) & 255;
|
|
return result;
|
|
}
|
|
static void decode(unsigned char* output, size_t length, const char* hex) {
|
|
assert(strlen(hex) == length * 2);
|
|
for (size_t i = 0; i < length; ++i) {
|
|
unsigned value;
|
|
assert(sscanf(hex + i * 2, "%2x", &value) == 1);
|
|
output[i] = value;
|
|
}
|
|
}
|
|
static void verifySignature(unsigned char* signature, const unsigned char* key,
|
|
const unsigned char* data, size_t length) {
|
|
// Retain the production C++ verification path; do not use the bundled
|
|
// C verifier which Identity deliberately excludes.
|
|
assert(Ed25519::verify(signature, key, data, length));
|
|
for (size_t index : {size_t(0), size_t(17), size_t(32), size_t(63)}) {
|
|
signature[index] ^= 1;
|
|
assert(!Ed25519::verify(signature, key, data, length));
|
|
signature[index] ^= 1;
|
|
}
|
|
std::vector<unsigned char> changed(data, data + length);
|
|
if (changed.empty()) changed.push_back(1); else changed[0] ^= 1;
|
|
assert(!Ed25519::verify(signature, key, bytes(changed), changed.size()));
|
|
unsigned char wrong_key[32];
|
|
memcpy(wrong_key, key, 32); wrong_key[0] ^= 1;
|
|
assert(!Ed25519::verify(signature, wrong_key, data, length));
|
|
}
|
|
int main() {
|
|
const size_t lengths[] = {@LENGTHS@};
|
|
const size_t chunks[] = {1, 7, 63, 64, 111, 112, 127, 128, 129, 257, 1024};
|
|
unsigned char result[64];
|
|
assert(sha512_init(nullptr) == 1);
|
|
assert(sha512_update(nullptr, result, 0) == 1);
|
|
assert(sha512_final(nullptr, result) == 1);
|
|
assert(sha512(nullptr, 0, result) == 1);
|
|
assert(sha512(result, 0, nullptr) == 1);
|
|
for (size_t length : lengths) {
|
|
const auto first = message(length, 19), second = message(length + 137, 91);
|
|
assert(sha512(bytes(first), first.size(), result) == 0);
|
|
printHex(result, sizeof(result));
|
|
sha512_context contexts[2];
|
|
size_t positions[2] = {}, turn = 0;
|
|
for (auto& context : contexts) {
|
|
assert(sha512_init(&context) == 0);
|
|
assert(sha512_update(&context, nullptr, 0) == 1);
|
|
assert(sha512_final(&context, nullptr) == 1);
|
|
assert(sha512_update(&context, result, 0) == 0);
|
|
}
|
|
while (positions[0] < first.size() || positions[1] < second.size()) {
|
|
for (unsigned i = 0; i < 2; ++i) {
|
|
const auto& data = i ? second : first;
|
|
size_t count = chunks[turn++ % (sizeof(chunks) / sizeof(chunks[0]))];
|
|
if (count > data.size() - positions[i]) count = data.size() - positions[i];
|
|
assert(sha512_update(&contexts[i], bytes(data) + positions[i], count) == 0);
|
|
positions[i] += count;
|
|
}
|
|
}
|
|
for (auto& context : contexts) {
|
|
assert(sha512_final(&context, result) == 0);
|
|
printHex(result, sizeof(result));
|
|
// Restart a completed context and reset an unfinished one. Both
|
|
// must discard the old message without touching another context.
|
|
assert(sha512_init(&context) == 0);
|
|
assert(sha512_update(&context, bytes(second), second.size()) == 0);
|
|
assert(sha512_init(&context) == 0);
|
|
assert(sha512_update(&context, bytes(first), first.size()) == 0);
|
|
assert(sha512_final(&context, result) == 0);
|
|
unsigned char expected[64];
|
|
assert(sha512(bytes(first), first.size(), expected) == 0);
|
|
assert(!memcmp(result, expected, 64));
|
|
}
|
|
}
|
|
struct Vector { const char* seed; const char* key; const char* data; const char* signature; };
|
|
// RFC 8032 section 7.1, tests 1-3.
|
|
// https://www.rfc-editor.org/rfc/rfc8032.html#section-7.1
|
|
const Vector vectors[] = {
|
|
{"9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60",
|
|
"d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a", "",
|
|
"e5564300c360ac729086e2cc806e828a84877f1eb8e5d974d873e065224901555f"
|
|
"b8821590a33bacc61e39701cf9b46bd25bf5f0595bbe24655141438e7a100b"},
|
|
{"4ccd089b28ff96da9db6c346ec114e0f5b8a319f35aba624da8cf6ed4fb8a6fb",
|
|
"3d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c", "72",
|
|
"92a009a9f0d4cab8720e820b5f642540a2b27b5416503f8fb3762223ebdb69da085"
|
|
"ac1e43e15996e458f3613d0f11d8c387b2eaeb4302aeeb00d291612bb0c00"},
|
|
{"c5aa8df43f9f837bedb7442f31dcb7b166d38535076f094b85ce3a2e0b4458f7",
|
|
"fc51cd8e6218a1a38da47ed00230f0580816ed13ba3303ac5deb911548908025", "af82",
|
|
"6291d657deec24024827e69c3abe01a30ce548a284743a445e3680d7db5ac3ac"
|
|
"18ff9b538d16f290ae67f760984dc6594a7c15e9716ed28dc027beceea1ec40a"}
|
|
};
|
|
for (const auto& vector : vectors) {
|
|
unsigned char seed[32], key[32], expected_key[32], secret[64], signature[64], expected[64];
|
|
std::vector<unsigned char> data(strlen(vector.data) / 2);
|
|
decode(seed, 32, vector.seed); decode(expected_key, 32, vector.key);
|
|
decode(expected, 64, vector.signature);
|
|
if (!data.empty()) decode(data.data(), data.size(), vector.data);
|
|
ed25519_create_keypair(key, secret, seed);
|
|
assert(!memcmp(key, expected_key, 32));
|
|
ed25519_sign(signature, bytes(data), data.size(), key, secret);
|
|
assert(!memcmp(signature, expected, 64));
|
|
verifySignature(signature, key, bytes(data), data.size());
|
|
}
|
|
// Cross-backend byte-for-byte comparison covers long, irregular messages
|
|
// and existing expanded private-key storage, beyond the short RFC cases.
|
|
for (size_t length : lengths) {
|
|
unsigned char seed[32], key[32], secret[64], signature[64];
|
|
const auto data = message(length, 37);
|
|
for (unsigned i = 0; i < 32; ++i) seed[i] = (length + i * 17) & 255;
|
|
ed25519_create_keypair(key, secret, seed);
|
|
ed25519_sign(signature, bytes(data), data.size(), key, secret);
|
|
verifySignature(signature, key, bytes(data), data.size());
|
|
printHex(key, 32); printHex(secret, 64); printHex(signature, 64);
|
|
}
|
|
puts("Shared SHA512 and RFC8032 checks passed");
|
|
}
|
|
'''
|
|
|
|
|
|
class SharedSHA512Test(unittest.TestCase):
|
|
def test_c_api_hashes_and_signatures_match_original_and_rfc8032(self):
|
|
compiler = shutil.which("gcc")
|
|
cpp = shutil.which("g++")
|
|
self.assertIsNotNone(compiler, "C compiler required")
|
|
self.assertIsNotNone(cpp, "C++ compiler required")
|
|
override = os.environ.get("MESHCORE_CRYPTO_DIR")
|
|
candidates = [Path(override)] if override else sorted((ROOT / ".pio/libdeps").glob("*/Crypto"))
|
|
names = ("SHA512.cpp", "Hash.cpp", "Crypto.cpp", "Ed25519.cpp", "Curve25519.cpp",
|
|
"BigNumberUtil.cpp")
|
|
crypto = next((path for path in candidates if all((path / name).is_file() for name in names)), None)
|
|
self.assertIsNotNone(crypto, "Install rweather/Crypto or set MESHCORE_CRYPTO_DIR (no crypto mocks)")
|
|
flags = ["-O1", "-g", "-Wall", "-Wextra", "-Werror", "-DHOST_BUILD=1",
|
|
"-DED25519_NO_SEED=1", "-DED25519_SMALL_BASE=1",
|
|
"-DED25519_COMPACT_SHA512=1", "-DED25519_COMPACT_SCALAR=1",
|
|
"-DED25519_COMPACT_FIELD=1", "-ffunction-sections", "-fdata-sections",
|
|
"-I" + str(ED25519), "-I" + str(crypto)]
|
|
if os.name != "nt":
|
|
flags += ["-fsanitize=address,undefined", "-fno-sanitize-recover=all", "-fno-pie", "-no-pie"]
|
|
with tempfile.TemporaryDirectory(prefix="ed25519-shared-sha512-") as directory:
|
|
work = Path(directory)
|
|
if os.name == "nt":
|
|
# Crypto's HOST_BUILD expects the POSIX byte-order header.
|
|
# MinGW supplies the same conversions as compiler intrinsics.
|
|
endian = "#pragma once\n#include <stdint.h>\n"
|
|
endian += "static_assert(__BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__, \"host byte order\");\n"
|
|
for bits in (16, 32, 64):
|
|
for name in (f"htobe{bits}", f"be{bits}toh"):
|
|
endian += f"static inline uint{bits}_t {name}(uint{bits}_t n) {{ return __builtin_bswap{bits}(n); }}\n"
|
|
for name in (f"htole{bits}", f"le{bits}toh"):
|
|
endian += f"static inline uint{bits}_t {name}(uint{bits}_t n) {{ return n; }}\n"
|
|
(work / "endian.h").write_text(endian)
|
|
flags += ["-I" + str(work)]
|
|
harness = work / "harness.cpp"
|
|
source = HARNESS.replace("@LENGTHS@", ",".join(map(str, LENGTHS)))
|
|
harness.write_text(source)
|
|
outputs = []
|
|
for shared in (0, 1):
|
|
with self.subTest(shared=shared):
|
|
build_flags = flags + [f"-DED25519_USE_CRYPTO_SHA512={shared}"]
|
|
objects = []
|
|
for name in ("fe.c", "ge.c", "keypair.c", "sc.c", "sha512.c", "sign.c"):
|
|
obj = work / (name + f"-{shared}.o")
|
|
# Legacy ref10 carries shift negative signed values.
|
|
# Keep ASan and all other UBSan checks on those files;
|
|
# the new adapter and both hash implementations retain
|
|
# the complete sanitizer set.
|
|
legacy = ["-fno-sanitize=shift"] if os.name != "nt" and name in ("fe.c", "ge.c") else []
|
|
built = subprocess.run([compiler, "-std=c99", *build_flags, *legacy, "-c", str(ED25519 / name),
|
|
"-o", str(obj)], capture_output=True, text=True, timeout=60)
|
|
self.assertEqual(built.returncode, 0, built.stdout + built.stderr)
|
|
objects.append(str(obj))
|
|
binary = work / f"shared-{shared}"
|
|
built = subprocess.run([cpp, "-std=c++11", *build_flags, str(harness),
|
|
str(ED25519 / "ed_sha512_crypto.cpp"),
|
|
*[str(crypto / name) for name in names], *objects,
|
|
"-Wl,--gc-sections", "-o", str(binary)],
|
|
capture_output=True, text=True, timeout=90)
|
|
self.assertEqual(built.returncode, 0, built.stdout + built.stderr)
|
|
run = subprocess.run([str(binary)], capture_output=True, text=True, timeout=60)
|
|
self.assertEqual(run.returncode, 0, run.stdout + run.stderr)
|
|
lines = run.stdout.splitlines()
|
|
expected = []
|
|
for length in LENGTHS:
|
|
first = bytes((i * 73 + 19) & 255 for i in range(length))
|
|
second = bytes((i * 73 + 91) & 255 for i in range(length + 137))
|
|
expected += [hashlib.sha512(first).hexdigest()] * 2
|
|
expected += [hashlib.sha512(second).hexdigest()]
|
|
self.assertEqual(lines[:len(expected)], expected)
|
|
self.assertEqual(lines[-1], "Shared SHA512 and RFC8032 checks passed")
|
|
outputs.append(lines)
|
|
self.assertEqual(len(outputs), 2)
|
|
self.assertEqual(outputs[0], outputs[1])
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|