ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
check_stub_crypto_guarded.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# SPDX-License-Identifier: MIT
3# Copyright (c) 2026 Brighton Sikarskie
4"""Gate: every insecure placeholder-crypto body shall be guarded fail-closed.
5
6Several secure-side translation units ship an INSECURE placeholder body that is
7only safe under an off-target build or an explicitly-declared insecure dev/eval
8image (issue #180):
9
10 - libs/ra8_secure_app/src/secure_trng.c deterministic xorshift PRNG as a "TRNG"
11 - libs/ra8_secure_app/src/key_vault.c plain secure-SRAM key store (no HW vault)
12 - libs/ra8_hal/src/ra8_rsip_key_injection.c non-cryptographic xorshift key-wrap
13 - libs/ra8_hal/src/ra8_rsip_ecc.c fiction-opcode ECDSA / ECDH / Ed25519 asym
14 - libs/ra8_hal/src/ra8_rsip_cipher.c fiction-opcode AES / ChaCha / key-install
15 - libs/ra8_hal/src/ra8_rsip_rsa.c fiction-opcode RSA sign / verify / enc / dec
16 - libs/ra8_hal/src/ra8_rsip_asym.c fiction-opcode hash / HMAC / key vault / wrap / KDF
17 - libs/ra8_hal/src/ra8_rsip_devsec.c fiction-register lifecycle / debug / tamper / DPA
18
19Each such body MUST be wrapped in the guard::
20
21 #if defined(RA8_INSECURE_STUB_CRYPTO) || defined(RA8_OFF_TARGET)
22 <insecure placeholder body>
23 #else
24 <fail-closed: every entry point returns a hard error, never k_ra8_ok>
25 #endif
26
27so a real production/HIL image that sets NEITHER flag compiles the fail-closed
28#else and cannot silently ship the stub. This gate is the compile-time-of-CI
29guarantee the audit asked for: it FAILS if, for any listed TU,
30
31 1. the guard `#if defined(RA8_INSECURE_STUB_CRYPTO) || defined(RA8_OFF_TARGET)`
32 is absent, or has no matching #else / #endif;
33 2. the #else branch is not fail-closed (no `#error` and no `k_ra8_err_` return);
34 3. the TU's insecure signature token escapes the guarded region (appears before
35 the guard, in the #else, or after the #endif) -- i.e. an insecure body that
36 is not actually behind the guard.
37
38There is no allowlist: either guard the insecure body fail-closed, or delete it
39in favour of a real backend.
40
41Run::
42
43 check_stub_crypto_guarded.py
44
45Exit status: 0 if every stub TU is guarded fail-closed, 1 otherwise.
46"""
47
48from __future__ import annotations
49
50import re
51import sys
52import tempfile
53from pathlib import Path
54
55# Repo root: this file is scripts/checks/check_stub_crypto_guarded.py .
56REPO_ROOT = Path(__file__).resolve().parents[2]
57
58# Each stub TU -> a distinctive token that appears ONLY in its insecure body
59# (never in the fail-closed #else nor in surrounding prose). The token must live
60# inside the guarded #if region; if it escapes, the insecure body is unguarded.
61STUB_TUS = {
62 "libs/ra8_secure_app/src/secure_trng.c": "internal_xorshift64",
63 "libs/ra8_secure_app/src/key_vault.c": "s_vault",
64 "libs/ra8_hal/src/ra8_rsip_key_injection.c": "ki_compute_mac",
65 "libs/ra8_hal/src/ra8_rsip_ecc.c": "k_ra8_rsip_asym_op_eddsa_sign",
66 "libs/ra8_hal/src/ra8_rsip_cipher.c": "internal_sym_run",
67 "libs/ra8_hal/src/ra8_rsip_rsa.c": "internal_rsa_dispatch",
68 "libs/ra8_hal/src/ra8_rsip_asym.c": "internal_hash_pull_digest",
69 "libs/ra8_hal/src/ra8_rsip_devsec.c": "k_ra8_rsip_off_life_state",
70}
71
72_RE_IF = re.compile(r"^\s*#\s*if(n?def)?\b")
73_RE_ELSE = re.compile(r"^\s*#\s*else\b")
74_RE_ENDIF = re.compile(r"^\s*#\s*endif\b")
75_RE_ERROR = re.compile(r"^\s*#\s*error\b")
76
77
78def is_guard_open(line: str) -> bool:
79 """Whether ``line`` is the stub-crypto guard opener (either flag order)."""
80 return bool(
81 re.match(r"^\s*#\s*if\b", line)
82 and "RA8_INSECURE_STUB_CRYPTO" in line
83 and "RA8_OFF_TARGET" in line
84 )
85
86
87def find_guard_region(lines: list[str]) -> tuple[int, int, int] | None:
88 """Locate the guard's (#if, #else, #endif) 0-based line indices.
89
90 Returns ``(if_idx, else_idx, endif_idx)`` or ``None`` when the guard opener
91 is absent or its matching #else / #endif cannot be resolved. Nesting of
92 inner #if/#endif is tracked so a nested conditional does not confuse the
93 match.
94 """
95 if_idx = next((i for i, ln in enumerate(lines) if is_guard_open(ln)), None)
96 if if_idx is None:
97 return None
98 depth = 1
99 else_idx = None
100 for i in range(if_idx + 1, len(lines)):
101 ln = lines[i]
102 if _RE_IF.match(ln):
103 depth += 1
104 elif _RE_ENDIF.match(ln):
105 depth -= 1
106 if depth == 0:
107 return (if_idx, else_idx, i) if else_idx is not None else None
108 elif _RE_ELSE.match(ln) and depth == 1:
109 else_idx = i
110 return None
111
112
113def check_file(rel: str, token: str, repo_root: Path = REPO_ROOT) -> list[str]:
114 """Return a list of violation strings for one stub TU (empty when clean)."""
115 path = repo_root / rel
116 if not path.is_file():
117 return [f"{rel}: file not found (expected an insecure stub TU here)"]
118 lines = path.read_text(encoding="utf-8").splitlines()
119
120 region = find_guard_region(lines)
121 if region is None:
122 return [
123 f"{rel}: missing the stub-crypto guard "
124 f"'#if defined(RA8_INSECURE_STUB_CRYPTO) || defined(RA8_OFF_TARGET)' "
125 f"with a matching #else / #endif"
126 ]
127 if_idx, else_idx, endif_idx = region
128
129 problems: list[str] = []
130
131 # (2) the #else branch must be fail-closed: a compile-time #error or a hard
132 # k_ra8_err_ return (never k_ra8_ok).
133 else_body = lines[else_idx + 1 : endif_idx]
134 fail_closed = any(_RE_ERROR.match(ln) or "k_ra8_err_" in ln for ln in else_body)
135 if not fail_closed:
136 problems.append(
137 f"{rel}: the #else branch is not fail-closed "
138 f"(needs a #error or a k_ra8_err_* hard return, not k_ra8_ok)"
139 )
140
141 # (3) the insecure signature token must live INSIDE the guarded #if region
142 # and never escape it.
143 hits = [i for i, ln in enumerate(lines) if token in ln]
144 inside = [i for i in hits if if_idx < i < else_idx]
145 escaped = [i for i in hits if not (if_idx < i < else_idx)]
146 if not inside:
147 problems.append(
148 f"{rel}: insecure signature '{token}' not found inside the guarded "
149 f"#if region (is the insecure body still present and guarded?)"
150 )
151 if escaped:
152 where = ", ".join(f"line {i + 1}" for i in escaped)
153 problems.append(
154 f"{rel}: insecure signature '{token}' appears OUTSIDE the guard "
155 f"({where}) -- the insecure body must be fully inside the "
156 f"#if defined(RA8_INSECURE_STUB_CRYPTO) || defined(RA8_OFF_TARGET) block"
157 )
158
159 return problems
160
161
162def selftest() -> int:
163 """Prove a complete fail-closed guard passes and an escaped stub fires."""
164 rel = "libs/fixture/stub.c"
165 signature = "insecure_fixture_signature"
166 with tempfile.TemporaryDirectory(prefix="stub-crypto-selftest-") as raw:
167 root = Path(raw)
168 path = root / rel
169 path.parent.mkdir(parents=True)
170 path.write_text(
171 "#if defined(RA8_INSECURE_STUB_CRYPTO) || defined(RA8_OFF_TARGET)\n"
172 f"static int {signature};\n"
173 "#else\nreturn k_ra8_err_unsupported;\n#endif\n",
174 encoding="ascii",
175 )
176 good_findings = check_file(rel, signature, root)
177 path.write_text(
178 "#if defined(RA8_INSECURE_STUB_CRYPTO) || defined(RA8_OFF_TARGET)\n"
179 "static int placeholder;\n#else\nreturn k_ra8_ok;\n#endif\n"
180 f"static int {signature};\n",
181 encoding="ascii",
182 )
183 bad_findings = check_file(rel, signature, root)
184 minimum_bad_findings = 2
185 cases = (
186 (not good_findings, "guarded token plus hard-error branch stays quiet"),
187 (
188 len(bad_findings) >= minimum_bad_findings
189 and any("not fail-closed" in item for item in bad_findings)
190 and any("OUTSIDE" in item for item in bad_findings),
191 "non-failing else and escaped insecure token both fire",
192 ),
193 )
194 failed = [label for passed, label in cases if not passed]
195 for passed, label in cases:
196 print(f" [{'ok' if passed else 'FAIL'}] {label}")
197 if failed:
198 print(f"check_stub_crypto_guarded.py --selftest: {len(failed)} failure(s)", file=sys.stderr)
199 return 1
200 print("check_stub_crypto_guarded.py --selftest: all cases pass (both directions).")
201 return 0
202
203
204def main() -> int:
205 """Verify every placeholder-crypto TU is guarded fail-closed.
206
207 These TUs hold deliberately insecure stand-ins for real crypto. The guard
208 must make the ``#else`` half -- the branch taken when the real
209 implementation is absent -- refuse to build or fail closed, so a
210 misconfiguration cannot silently ship the placeholder as if it were the
211 algorithm.
212
213 Returns 1 listing each unguarded TU, 0 when all are fail-closed.
214 """
215 args = sys.argv[1:]
216 if args == ["--selftest"]:
217 return selftest()
218 if args:
219 print("usage: check_stub_crypto_guarded.py [--selftest]", file=sys.stderr)
220 return 2
221 all_problems: list[str] = []
222 for rel, token in STUB_TUS.items():
223 all_problems.extend(check_file(rel, token))
224
225 if all_problems:
226 print("check_stub_crypto_guarded.py: insecure placeholder crypto not guarded fail-closed:")
227 for p in all_problems:
228 print(f" {p}")
229 print("Fix each at the root -- wrap the insecure body in")
230 print(" #if defined(RA8_INSECURE_STUB_CRYPTO) || defined(RA8_OFF_TARGET)")
231 print("and make the #else fail closed (return k_ra8_err_* / #error), or replace")
232 print("the placeholder with a real crypto backend.")
233 return 1
234
235 count = len(STUB_TUS)
236 print(f"check_stub_crypto_guarded.py: PASS -- {count} stub crypto TU(s) guarded fail-closed.")
237 return 0
238
239
240if __name__ == "__main__":
241 sys.exit(main())
void main(void)
The application entry point Reset_Handler hands control to.
Definition main.c:298