mirror of
https://github.com/wolfSSL/wolfssl.git
synced 2026-08-19 14:13:23 +02:00
304 lines
10 KiB
Python
304 lines
10 KiB
Python
#!/usr/bin/env python3
|
|
# puf_bch_genpoly.py
|
|
#
|
|
# Copyright (C) 2006-2026 wolfSSL Inc.
|
|
#
|
|
# This file is part of wolfSSL.
|
|
#
|
|
# wolfSSL is free software; you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation; either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# wolfSSL is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program; if not, write to the Free Software
|
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
|
# 02110-1335, USA
|
|
|
|
"""Generate and validate BCH(127, k, t) generator polynomials and
|
|
known-answer test (KAT) vectors for the wolfCrypt SRAM PUF (puf.c).
|
|
|
|
The field is GF(2^7) built from the primitive polynomial
|
|
p(x) = x^7 + x^3 + 1 (0x89), matching the in-tree gf_exp/gf_log tables.
|
|
|
|
Output for each shipped profile:
|
|
- bch_genpoly[] bytes in puf.c layout (64-bit big-endian, low deg bits,
|
|
leading x^deg coefficient implicit / masked off)
|
|
- a few message -> 127-bit systematic codeword KAT vectors
|
|
|
|
The t=10 genpoly MUST reproduce the existing in-tree constant
|
|
{0x21,0xAB,0x81,0x5B,0xC7,0xEC,0x80,0x25}; the script asserts this before
|
|
emitting any other profile, so a bad field or convention fails loudly.
|
|
|
|
This script is deterministic and takes no input. Run:
|
|
python3 scripts/puf_bch_genpoly.py
|
|
"""
|
|
|
|
import os
|
|
import re
|
|
import sys
|
|
|
|
M = 7
|
|
N = (1 << M) - 1 # 127
|
|
PRIM = 0x89 # x^7 + x^3 + 1
|
|
|
|
# Profiles: t -> k (n-k = deg of generator polynomial). Only valid BCH(127)
|
|
# narrow-sense designed-distance profiles. See puf.h profile table.
|
|
PROFILES = [
|
|
(7, 78),
|
|
(10, 64), # default; must match in-tree constant
|
|
(13, 50),
|
|
(15, 36),
|
|
]
|
|
|
|
# The shipped tables live here; --check parses them out and diffs.
|
|
PUF_C = os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
|
"..", "wolfcrypt", "src", "puf.c")
|
|
|
|
|
|
def parse_puf_c(path):
|
|
"""Extract {t: (genpoly_bytes, deg)} from the #if WC_PUF_BCH_T ladder in
|
|
wolfcrypt/src/puf.c. Parsing the real source is the point: a hardcoded
|
|
copy in this script would still pass if someone edited puf.c."""
|
|
src = open(path).read()
|
|
out = {}
|
|
# each arm: "WC_PUF_BCH_T == <t>" .. "bch_genpoly[..] = { .. };" .. "DEG <d>"
|
|
pat = re.compile(
|
|
r"WC_PUF_BCH_T\s*==\s*(\d+)\s*\n"
|
|
r".*?bch_genpoly\s*\[[^\]]*\]\s*=\s*\{(.*?)\}\s*;"
|
|
r".*?BCH_GENPOLY_DEG\s+(\d+)",
|
|
re.S)
|
|
for m in pat.finditer(src):
|
|
t = int(m.group(1))
|
|
body = m.group(2)
|
|
vals = [int(x, 16) for x in re.findall(r"0[xX]([0-9a-fA-F]{1,2})", body)]
|
|
out[t] = (vals, int(m.group(3)))
|
|
return out
|
|
|
|
|
|
def build_gf():
|
|
"""Return (exp, log) tables for GF(2^7) with primitive poly 0x89."""
|
|
exp = [0] * (N + 1)
|
|
log = [0] * (N + 1)
|
|
x = 1
|
|
for i in range(N):
|
|
exp[i] = x
|
|
log[x] = i
|
|
x <<= 1
|
|
if x & (1 << M):
|
|
x ^= PRIM
|
|
exp[N] = exp[0] # alpha^127 == alpha^0 == 1 (wrap)
|
|
return exp, log
|
|
|
|
|
|
EXP, LOG = build_gf()
|
|
|
|
|
|
def gf_mul(a, b):
|
|
if a == 0 or b == 0:
|
|
return 0
|
|
return EXP[(LOG[a] + LOG[b]) % N]
|
|
|
|
|
|
def poly_mul_gf(p, q):
|
|
"""Multiply two polynomials with GF(2^7) coefficients (lists, index =
|
|
power of x, element 0 = constant term)."""
|
|
r = [0] * (len(p) + len(q) - 1)
|
|
for i, pi in enumerate(p):
|
|
if pi == 0:
|
|
continue
|
|
for j, qj in enumerate(q):
|
|
if qj == 0:
|
|
continue
|
|
r[i + j] ^= gf_mul(pi, qj)
|
|
return r
|
|
|
|
|
|
def cyclotomic_coset(i):
|
|
"""The 2-cyclotomic coset of i mod 127."""
|
|
coset = set()
|
|
v = i % N
|
|
while v not in coset:
|
|
coset.add(v)
|
|
v = (v * 2) % N
|
|
return coset
|
|
|
|
|
|
def minimal_poly(i):
|
|
"""Minimal polynomial of alpha^i: product over its coset of (x + alpha^j).
|
|
Result has binary coefficients (each 0x00 or 0x01 as a GF element)."""
|
|
m = [1] # start with 1
|
|
for j in cyclotomic_coset(i):
|
|
m = poly_mul_gf(m, [EXP[j], 1]) # (alpha^j + x)
|
|
# coefficients must be 0 or 1 (binary); verify
|
|
for c in m:
|
|
if c not in (0, 1):
|
|
raise ValueError("minimal poly has non-binary coefficient")
|
|
return m
|
|
|
|
|
|
def gen_poly(t):
|
|
"""Narrow-sense BCH generator polynomial = lcm of minimal polynomials of
|
|
alpha^1 .. alpha^(2t). Returned as a binary-coefficient list (index =
|
|
power of x)."""
|
|
used = set()
|
|
g = [1]
|
|
for i in range(1, 2 * t + 1):
|
|
coset = frozenset(cyclotomic_coset(i))
|
|
if coset in used:
|
|
continue
|
|
used.add(coset)
|
|
g = poly_mul_gf(g, minimal_poly(i))
|
|
return g
|
|
|
|
|
|
def genpoly_bytes(g):
|
|
"""Pack g(x) into puf.c's 8..16 byte layout: a big-endian integer whose
|
|
bit i is coefficient g_i, with the leading coefficient g_deg masked off
|
|
(implicit). Byte count = ceil(deg / 8)."""
|
|
deg = len(g) - 1
|
|
assert g[deg] == 1, "generator must be monic"
|
|
val = 0
|
|
for i in range(deg): # exclude leading coeff
|
|
if g[i]:
|
|
val |= (1 << i)
|
|
nbytes = (deg + 7) // 8
|
|
# 64-bit style big-endian pack sized to nbytes (matches puf.c: byte 0 is
|
|
# the most significant, top bit of byte 0 is the implicit/masked slot).
|
|
out = []
|
|
for b in range(nbytes):
|
|
shift = 8 * (nbytes - 1 - b)
|
|
out.append((val >> shift) & 0xFF)
|
|
return deg, out
|
|
|
|
|
|
def bch_encode_ref(msg_bits, g):
|
|
"""Reference systematic encoder matching puf.c bch_encode(): codeword =
|
|
[msg(k) | parity(deg)] MSB-first. msg_bits is a list of k bits (MSB-first
|
|
message bit order). Returns list of n bits (MSB-first)."""
|
|
deg = len(g) - 1
|
|
k = len(msg_bits)
|
|
reg = [0] * deg # remainder register, reg[0]=MSB
|
|
for bit in msg_bits:
|
|
fb = bit ^ reg[0]
|
|
reg = reg[1:] + [0] # shift left
|
|
if fb:
|
|
# XOR non-leading generator coefficients g_{deg-1..0} into reg
|
|
for p in range(deg):
|
|
# reg position p holds coefficient of x^(deg-1-p)
|
|
if g[deg - 1 - p]:
|
|
reg[p] ^= 1
|
|
return list(msg_bits) + reg # [msg | parity], n bits
|
|
|
|
|
|
def syndromes_zero(cw_bits, t):
|
|
"""Return True iff every narrow-sense syndrome S_1..S_2t of the codeword is
|
|
zero, i.e. the bit list is a genuine BCH codeword. Mirrors puf.c's
|
|
bch_syndrome_eval: bit j (MSB-first) is the coefficient of x^(n-1-j), so
|
|
S_root = c(alpha^root) = XOR of alpha^(root*(n-1-j)) over the set bits."""
|
|
n = len(cw_bits)
|
|
for root in range(1, 2 * t + 1):
|
|
s = 0
|
|
for j, b in enumerate(cw_bits):
|
|
if b:
|
|
s ^= EXP[(root * (n - 1 - j)) % N]
|
|
if s != 0:
|
|
return False
|
|
return True
|
|
|
|
|
|
def bits_to_hex(bits):
|
|
"""Pack an MSB-first bit list into bytes, byte-rounded."""
|
|
out = bytearray((len(bits) + 7) // 8)
|
|
for i, b in enumerate(bits):
|
|
if b:
|
|
out[i // 8] |= 1 << (7 - (i % 8))
|
|
return list(out)
|
|
|
|
|
|
def fmt_bytes(bs):
|
|
return "{ " + ", ".join("0x%02X" % b for b in bs) + " }"
|
|
|
|
|
|
def main():
|
|
check = "--check" in sys.argv[1:]
|
|
|
|
# Sanity: exp table spot checks against known values.
|
|
assert EXP[1] == 0x02 and EXP[7] == 0x09, "GF(2^7) table mismatch"
|
|
|
|
shipped = parse_puf_c(PUF_C) if check else {}
|
|
if check and len(shipped) != len(PROFILES):
|
|
raise SystemExit("--check: parsed %d tables from %s, expected %d"
|
|
% (len(shipped), PUF_C, len(PROFILES)))
|
|
|
|
if not check:
|
|
print("/* Generated by scripts/puf_bch_genpoly.py - do not hand-edit */")
|
|
print("/* Field GF(2^7), primitive poly 0x89 (x^7 + x^3 + 1) */\n")
|
|
|
|
bad = 0
|
|
for t, k in PROFILES:
|
|
g = gen_poly(t)
|
|
deg, bs = genpoly_bytes(g)
|
|
if deg != N - k:
|
|
raise SystemExit(
|
|
"t=%d: computed deg %d != n-k %d" % (t, deg, N - k))
|
|
|
|
# KAT self-check: the reference encoder must yield a genuine BCH
|
|
# codeword (all 2t syndromes zero), confirming both the generator
|
|
# polynomial and the encoder algebra for this profile.
|
|
kats = []
|
|
for seed in (0, 1, 2):
|
|
msg_bits = [((seed * 131 + i * 37) >> (i % 5)) & 1
|
|
for i in range(k)]
|
|
cw = bch_encode_ref(msg_bits, g)
|
|
if not syndromes_zero(cw, t):
|
|
raise SystemExit(
|
|
"t=%d: KAT codeword has nonzero syndrome" % t)
|
|
kats.append((msg_bits, cw))
|
|
|
|
if check:
|
|
# Diff the freshly derived table against what puf.c actually
|
|
# ships. This is the whole point of --check: a copy of the bytes
|
|
# kept inside this script would pass even if puf.c were edited.
|
|
if t not in shipped:
|
|
print("FAIL t=%d: no table found in puf.c" % t)
|
|
bad += 1
|
|
continue
|
|
got, gotdeg = shipped[t]
|
|
if got != bs:
|
|
print("FAIL t=%d: puf.c has %s, expected %s"
|
|
% (t, fmt_bytes(got), fmt_bytes(bs)))
|
|
bad += 1
|
|
elif gotdeg != deg:
|
|
print("FAIL t=%d: puf.c BCH_GENPOLY_DEG %d, expected %d"
|
|
% (t, gotdeg, deg))
|
|
bad += 1
|
|
else:
|
|
print("ok t=%d k=%d deg=%d %s" % (t, k, deg, fmt_bytes(bs)))
|
|
continue
|
|
|
|
print("/* t=%d k=%d deg=%d (%d parity bytes) */" %
|
|
(t, k, deg, len(bs)))
|
|
print("static const byte bch_genpoly_t%d[%d] =" % (t, len(bs)))
|
|
print(" %s;" % fmt_bytes(bs))
|
|
for msg_bits, cw in kats:
|
|
print("/* KAT msg=%s" % fmt_bytes(bits_to_hex(msg_bits)))
|
|
print(" cw =%s */" % fmt_bytes(bits_to_hex(cw)))
|
|
print()
|
|
|
|
if check:
|
|
if bad:
|
|
raise SystemExit("%d generator polynomial(s) in puf.c do NOT "
|
|
"match; regenerate or revert." % bad)
|
|
print("all %d shipped generator polynomials verified against %s"
|
|
% (len(PROFILES), os.path.normpath(PUF_C)))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|