-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathMakefile
More file actions
4375 lines (4153 loc) · 243 KB
/
Copy pathMakefile
File metadata and controls
4375 lines (4153 loc) · 243 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# `make` / `make help` lists every runnable target that carries a `## ` blurb
# (self-documenting — derived from the Makefile itself, so it can't drift the
# way a hand-maintained count does). Add `## one-liner` to a target's rule line
# to surface it. `make help` also appends the FV / spec-assurance suite from
# contracts/verification/; `make help-verify` shows just that suite.
.DEFAULT_GOAL := help
.PHONY: help help-verify
help: ## Show the main runnable targets (root + the FV suite below)
@grep -hE '^[a-zA-Z0-9_.-]+:.*## ' $(MAKEFILE_LIST) | sort | awk -F':.*## ' '!seen[$$1]++ {printf " \033[36m%-26s\033[0m %s\n", $$1, $$2}'
@printf '\n \033[1mFV / spec-assurance\033[0m (run with: make -C contracts/verification <target>)\n'
@$(MAKE) --no-print-directory -C contracts/verification help | grep -v 'runnable FV targets' || true
help-verify: ## Show only the FV / spec-assurance targets (contracts/verification)
@$(MAKE) --no-print-directory -C contracts/verification help
TARGET = thumbv8m.main-none-eabi
RUSTFLAGS_VAR = CARGO_TARGET_THUMBV8M_MAIN_NONE_EABI_RUSTFLAGS
VENEERS = $(CURDIR)/target/veneers.o
# CMSE veneers only exist on the real STM32U585 build path (the QEMU
# `mps2-an505` transport uses a shared-memory mailbox instead). The
# linker rejects `--cmse-implib` if no `cmse-nonsecure-entry` symbols
# are present in the secure binary, so we only emit the implib when the
# `stm32u585` cargo feature is selected.
ifneq (,$(findstring stm32u585,$(FEATURES)))
SECURE_CMSE_FLAGS = -C link-arg=--cmse-implib -C link-arg=--out-implib=$(VENEERS)
NS_VENEERS_FLAG = -C link-arg=$(VENEERS)
else
SECURE_CMSE_FLAGS =
NS_VENEERS_FLAG =
endif
# Reproducible-build flags. Rebuilding the same commit on a different
# laptop (or inside CI) must produce byte-identical ELFs so that
# `make measure` yields the same 8 BIP-39 words. The flags below
# normalize three sources of build-host variance:
#
# 1. --remap-path-prefix rewrites any absolute file paths that end up
# in panic messages / debug info / OUT_DIR references to a stable
# prefix. Without this, two laptops with different $HOME values
# produce different ELFs. The /nix/store rule covers the rustc
# sysroot path embedded by `core` panic messages: under the
# flake, rust-overlay downloads a per-host prebuilt rustc, so
# the store hash differs between Linux x86_64 and macOS aarch64
# and would otherwise leak into .rodata.
# 2. -Wl,--build-id=none strips the GNU build-id note, which is a
# hash over the other note sections and shifts with any re-link.
# 3. -Wl,--no-insert-timestamp prevents the linker from stamping
# build time into the PE-ish note sections (ld is usually quiet
# about this on ELF, but we still pass the flag as a belt-and-
# braces measure).
#
# SOURCE_DATE_EPOCH is exported for any build script that embeds a
# timestamp. When built from a git checkout it's the commit time
# (deterministic for a given commit); otherwise it falls back to the
# POSIX epoch.
REPRO_REMAP = --remap-path-prefix=$(HOME)/.cargo=/cargo \
--remap-path-prefix=$(HOME)/.rustup=/rustup \
--remap-path-prefix=/nix/store=/nix-store \
--remap-path-prefix=$(CURDIR)=/pqsigner
# The Makefile invokes arm-none-eabi-ld directly (no gcc driver), so linker
# flags are passed bare — not wrapped in -Wl,. arm-none-eabi-ld has
# --build-id= but not --no-insert-timestamp (that one's PE-only).
REPRO_LINK = -C link-arg=--build-id=none
REPRO_FLAGS = $(REPRO_REMAP) $(REPRO_LINK)
export SOURCE_DATE_EPOCH ?= $(shell git log -1 --format=%ct 2>/dev/null || echo 0)
# Factored RUSTFLAGS strings for the two firmware worlds. Every target
# that invokes cargo on the ARM tree uses one of these variables so
# reproducibility flags are applied consistently and can't drift.
# Cargo gives CARGO_TARGET_<TRIPLE>_RUSTFLAGS precedence over
# `.cargo/config.toml`, so that file is only a fallback for ad-hoc
# `cargo build` invocations — the canonical flags live here.
RUSTFLAGS_SECURE = -C linker=arm-none-eabi-ld -C link-arg=-Tlink.x $(REPRO_FLAGS) $(SECURE_CMSE_FLAGS)
RUSTFLAGS_NONSECURE = -C linker=arm-none-eabi-ld -C link-arg=-Tlink.x $(REPRO_FLAGS) $(NS_VENEERS_FLAG)
# Variants for hardware targets that unconditionally emit CMSE veneers
# (independent of the $(FEATURES) content — used by the hw- targets).
RUSTFLAGS_SECURE_HW = -C linker=arm-none-eabi-ld -C link-arg=-Tlink.x $(REPRO_FLAGS) -C link-arg=--cmse-implib -C link-arg=--out-implib=$(VENEERS)
RUSTFLAGS_NONSECURE_HW = -C linker=arm-none-eabi-ld -C link-arg=-Tlink.x $(REPRO_FLAGS) -C link-arg=$(VENEERS)
SECURE_ELF = target/secure/$(TARGET)/release/sphincs-tz-secure
NONSECURE_ELF = target/nonsecure/$(TARGET)/release/sphincs-tz-nonsecure
FSBL_ELF = target/fsbl/$(TARGET)/release/pqsigner-fsbl
RELEASE_FSBL_ELF = target/release-fsbl/$(TARGET)/release/pqsigner-fsbl
PRODUCTION_VENDOR_KEY_POLICY = $(CURDIR)/config/production-firmware-vendor-key.sha256
DEVELOPMENT_VENDOR_KEY_POLICY = $(CURDIR)/config/development-firmware-vendor-pubkey.hex
RELEASE_VENDOR_KEY_SNAPSHOT = $(CURDIR)/target/release-input/vendor-pubkey.bin
RELEASE_ARTIFACT_DIR = $(CURDIR)/target/pqsigner-release
RELEASE_ARTIFACT_TMP = $(CURDIR)/target/pqsigner-release.tmp
# Default: mock secure element + semihosting UI mock (no real hardware needed)
# debug-log enables semihosting output from the secure world.
# Remove it for production builds to eliminate all debug strings.
FEATURES ?= mock-se,debug-log,ui-semihosting
# The generated review file is the host-readable provenance companion to the
# root/fences in secure/src/db_roots.rs. When the selected root is explicitly
# dev-unattested, canonical Make-driven dev builds automatically enable the
# matching trusted-display warning. A production feature set never gets that
# feature: `prod-erc7730-provenance-check` and the generated Rust fence reject
# the root instead. The rollback quarantine remains an independent ship gate.
# These values are security policy inputs, not caller configuration. GNU
# make command-line assignments normally override ordinary makefile values;
# use `override` so an invocation cannot make this process gate false-green
# while the generated Rust fence still embeds the dev catalogue.
override ERC7730_REVIEW := secure/data/erc7730.review.txt
override ERC7730_E2E_GENERATOR_FEATURE := nested-calldata-test-fixture
override ERC7730_CATALOGUE_PROVENANCE := $(strip $(shell awk '/^\# Provenance: / { print $$3; exit }' $(ERC7730_REVIEW) 2>/dev/null))
ifeq ($(ERC7730_CATALOGUE_PROVENANCE),dev-unattested)
ifeq (,$(findstring mode-production,$(FEATURES)))
ifeq (,$(findstring erc7730-dev-unattested,$(FEATURES)))
override FEATURES := $(FEATURES),erc7730-dev-unattested
endif
endif
endif
# Extract features relevant to the nonsecure crate (it doesn't know about
# mock-se, debug-log, ui-semihosting, etc. — only the shared platform,
# transport, test, and watchdog features below).
NS_FEATURES_LIST := $(strip $(foreach f,stm32u585 e2e-test usb iwdg,$(if $(findstring $(f),$(FEATURES)),$(f))))
comma := ,
empty :=
space := $(empty) $(empty)
NS_FEATURES_ARG = $(if $(NS_FEATURES_LIST),--features $(subst $(space),$(comma),$(NS_FEATURES_LIST)),)
.PHONY: all clean secure nonsecure run play play-hw-display run-hw e2e e2e-hw e2e-erc7730-hw e2e-hw-display e2e-hw-dual-se build-hw flash-hw test test-unit test-solidity test-formal-verification verify-theft-free test-key-speed test-update-hw measure factory-reset optiga-reset-oids flash-hw-optiga-reset verify-pins
# Supply-chain audit. Hard-fails if any dependency is not cryptographically
# pinned (Cargo.lock checksums, git rev= pins, foundry.lock matching
# checked-out submodules, dated-nightly rust-toolchain). See
# tools/verify_pins.sh for the exact rules. Every release-path target
# below depends on this.
verify-pins: ## Certify deployed-bytecode codehash pins
@tools/verify_pins.sh
all: verify-pins secure nonsecure
secure:
$(RUSTFLAGS_VAR)="$(RUSTFLAGS_SECURE)" \
cargo build --locked --release --target $(TARGET) --target-dir target/secure \
-p sphincs-tz-secure --no-default-features --features $(FEATURES)
@echo "==> Secure world built (features: $(FEATURES))."
nonsecure: secure
$(RUSTFLAGS_VAR)="$(RUSTFLAGS_NONSECURE)" \
cargo build --locked --release --target $(TARGET) --target-dir target/nonsecure -p sphincs-tz-nonsecure $(NS_FEATURES_ARG)
@echo "==> Non-secure world built."
# Run with mock SE (no real secure element needed).
# We attach semihosting to a dedicated stdio chardev so SYS_READC can read
# from the host terminal — this is what the secure UI mock uses to receive
# "button" input ('l'/'h' = short, 'L'/'H' = long).
run: all ## Non-interactive QEMU smoke (mock SE)
qemu-system-arm \
-M mps2-an505 \
-monitor null \
-serial null \
-chardev stdio,id=hostio \
-semihosting-config enable=on,target=native,chardev=hostio \
-kernel $(SECURE_ELF) \
-device loader,file=$(NONSECURE_ELF)
# Interactive two-button hardware-wallet emulation. Maps your laptop's
# arrow keys to the two physical buttons:
# <- Left button (back / scroll down)
# -> Right button (next / scroll up)
# <- + -> Confirm (press both arrows together within 150 ms)
# Esc Cancel / back
# Ctrl-C Quit
# tools/wallet_run.py spawns QEMU under the hood, owns the terminal in
# raw mode, and forwards button events through the existing semihosting
# single-char protocol.
play: all ## Interactive QEMU (arrow-key UI)
@python3 tools/wallet_run.py
# Interactive two-button wallet on real STM32U585 with SSD1306 OLED display.
# Same arrow-key mapping as `play` (QEMU version), but runs on real hardware.
# Display renders on the physical OLED; button input comes from your laptop
# keyboard via probe-rs semihosting READC.
# Requires: ST-LINK connected, SSD1306 OLED wired to PB8/PB9/3V3/GND.
play-hw-display: ## Interactive OLED + arrow-key forwarding (HW)
@echo "==> Building secure + nonsecure for interactive OLED play"
@FSBL_VENDOR_PUBKEY=$(DEV_VENDOR_PUBKEY) $(RUSTFLAGS_VAR)="$(RUSTFLAGS_SECURE_HW)" \
cargo build --locked --release --target $(TARGET) --target-dir target/secure \
-p sphincs-tz-secure --no-default-features \
--features mock-se,debug-log,ui-lcd,stm32u585,dev-testkey,gpio-buttons
@rm -f $(NONSECURE_ELF) target/nonsecure/$(TARGET)/release/deps/sphincs_tz_nonsecure-*
@$(RUSTFLAGS_VAR)="$(RUSTFLAGS_NONSECURE_HW)" \
cargo build --locked --release --target $(TARGET) --target-dir target/nonsecure \
-p sphincs-tz-nonsecure --features stm32u585
@echo "==> Flashing..."
@probe-rs download --chip STM32U585AIIx $(NONSECURE_ELF)
@probe-rs download --chip STM32U585AIIx $(SECURE_ELF)
@echo "==> Configuring TrustZone option bytes..."
@STM32_Programmer_CLI --connect port=SWD \
--optionbytes TZEN=1 SECWM1_PSTRT=0x0 SECWM1_PEND=0x7F \
SECWM2_PSTRT=0x7F SECWM2_PEND=0x0 SECBOOTADD0=0x180000
@echo "==> Starting interactive wallet (Ctrl-C to quit)..."
@python3 tools/wallet_run_hw.py
# Interactive two-button wallet on the NV3007 SPI LCD — the PRODUCTION display
# path (`ui-lcd` is the shipping backend as of 2026-06-09). Runs the FULL real
# wizard / PIN / confirm flow (no `lcd-test` short-circuit). Input from the
# physical gpio-buttons (LEFT=PC1/D8, RIGHT=PA8/D9). `ui-lcd` pulls in
# `gpio-buttons` + `spi1-arduino`. Requires: ST-LINK + the NV3007 wired per
# docs/hardware/nv3007-wiring.md (SPI on CN13 D10/D11/D13, DC=PE7/D4, RES→3V3, VCC+BLK→3V3,
# GND) + two buttons on the gpio-buttons pins. The OLED equivalent is
# `play-hw-display` (kept for SSD1306 dev boards).
play-hw-lcd:
@echo "==> Building secure + nonsecure for interactive LCD play (NV3007)"
@FSBL_VENDOR_PUBKEY=$(DEV_VENDOR_PUBKEY) $(RUSTFLAGS_VAR)="$(RUSTFLAGS_SECURE_HW)" \
cargo build --locked --release --target $(TARGET) --target-dir target/secure \
-p sphincs-tz-secure --no-default-features \
--features mock-se,debug-log,ui-lcd,stm32u585,dev-testkey
@rm -f $(NONSECURE_ELF) target/nonsecure/$(TARGET)/release/deps/sphincs_tz_nonsecure-*
@$(RUSTFLAGS_VAR)="$(RUSTFLAGS_NONSECURE_HW)" \
cargo build --locked --release --target $(TARGET) --target-dir target/nonsecure \
-p sphincs-tz-nonsecure --features stm32u585
@echo "==> Flashing..."
@probe-rs download --chip STM32U585AIIx $(NONSECURE_ELF)
@probe-rs download --chip STM32U585AIIx $(SECURE_ELF)
@echo "==> Configuring TrustZone option bytes..."
@STM32_Programmer_CLI --connect port=SWD \
--optionbytes TZEN=1 SECWM1_PSTRT=0x0 SECWM1_PEND=0x7F \
SECWM2_PSTRT=0x7F SECWM2_PEND=0x0 SECBOOTADD0=0x180000
@echo "==> Drive the wizard with the physical buttons; streaming logs (Ctrl-C to quit)..."
@python3 tools/wallet_run_hw.py
# §32 P4/P5 interactive UI test — drive JUST the duress-PIN setup dialogs
# on the real OLED. No SE, no provisioning (mock-se + duress-ui-test
# short-circuits into a dialog loop at boot). Driven by the PHYSICAL
# perfboard buttons (gpio-buttons: LEFT=PC1/D8, RIGHT=PA8/D9; both = OK,
# long-left = cancel) — same input path as `play-hw-display`, so no host
# key-forwarder is needed. wallet_run_hw.py still streams the OLED debug
# lines if you want them.
play-hw-duress-ui:
@echo "==> Building §32 duress-PIN UI harness (mock-se, dialogs only)"
@FSBL_VENDOR_PUBKEY=$(DEV_VENDOR_PUBKEY) $(RUSTFLAGS_VAR)="$(RUSTFLAGS_SECURE_HW)" \
cargo build --locked --release --target $(TARGET) --target-dir target/secure \
-p sphincs-tz-secure --no-default-features \
--features mock-se,debug-log,ui-lcd,stm32u585,dev-testkey,duress-ui-test,gpio-buttons
@rm -f $(NONSECURE_ELF) target/nonsecure/$(TARGET)/release/deps/sphincs_tz_nonsecure-*
@$(RUSTFLAGS_VAR)="$(RUSTFLAGS_NONSECURE_HW)" \
cargo build --locked --release --target $(TARGET) --target-dir target/nonsecure \
-p sphincs-tz-nonsecure --features stm32u585
@echo "==> Flashing..."
@probe-rs download --chip STM32U585AIIx $(NONSECURE_ELF)
@probe-rs download --chip STM32U585AIIx $(SECURE_ELF)
@echo "==> Configuring TrustZone option bytes..."
@STM32_Programmer_CLI --connect port=SWD \
--optionbytes TZEN=1 SECWM1_PSTRT=0x0 SECWM1_PEND=0x7F \
SECWM2_PSTRT=0x7F SECWM2_PEND=0x0 SECBOOTADD0=0x180000
@echo "==> Starting interactive duress-UI harness (Ctrl-C to quit)..."
@python3 tools/wallet_run_hw.py
# One-time chip hardening: set brown-out supervision + SRAM2 auto-erase
# option bytes. Run once per device during provisioning; no need to
# repeat unless the chip has been fully option-byte-reset.
#
# Changes:
# BOR_LEV = 3 (~2.7V) — flash writes abort cleanly below this
# SRAM2_RST = 0 — silicon erases SRAM2 on every reset
# (POR, BOR, SW, watchdog)
#
# Triggers an Option Byte Load (OBL_LAUNCH), which resets the chip.
# Expected side effects: next boot classifies as ResetCause::OptionByte
# in the semihosting log.
#
# After running this once, every subsequent reset hardware-zeroizes
# SRAM2 — put sensitive active-window state there (Stage 2 of the
# brownout hardening roadmap; see docs/security/brownout-hardening.md).
stm32-harden-opts:
@echo "==> Configuring brown-out supervision + SRAM2 auto-erase"
@echo " BOR_LEV=3 (~2.7V), SRAM2_RST=0 (auto-erase on reset)"
@echo " This triggers an Option Byte Load — the chip will reset."
@STM32_Programmer_CLI --connect port=SWD \
--optionbytes BOR_LEV=3 SRAM2_RST=0
@echo "==> Option bytes written. Reset triggered. Chip state: hardened."
# Real STM32U585 hardware build (full): real chips + real LCD + real buttons.
# This target only BUILDS — flashing is done with probe-rs / openocd / etc.
run-hw: ## Run on real hardware via probe-rs
$(MAKE) FEATURES=dual-se,ui-lcd,consumption-mask,stm32u585,legacy-fw-rollback-unsafe all
# Real STM32U585 hardware build (semihosting smoke): mock SE + semihosting UI.
# The `e2e-test` escape is REQUIRED, not optional: this is a RELEASE (debug_assertions
# OFF) stm32u585 build carrying dev-only features (mock-se/debug-log/ui-semihosting),
# which the nsc/mod.rs ship-blocker fences correctly reject without a non-shippable
# marker; `e2e-test` defuses both fences AND short-circuits enter_pin() so the image
# doesn't hang on probe-rs's missing SYS_READC (the CLAUDE.md HW gotcha) — mirrors the
# known-good `test-key-speed` target. Both `e2e-test` and `dev-testkey` are on the CI
# production-OFF denylist, so the image stays un-shippable by construction. For an
# INTERACTIVE hardware session, use `play-hw-lcd` (real LCD + arrow-key buttons).
build-hw: ## Build the real-hardware STM32U585 smoke image (non-shippable)
$(MAKE) FEATURES=mock-se,debug-log,ui-semihosting,e2e-test,stm32u585 all
# Flash and run on real STM32U585 via probe-rs + OpenOCD.
# Requires: ST-LINK connected, openocd installed.
#
# Workflow:
# 1. Flash both ELFs via probe-rs (it may clear TZEN during flash)
# 2. (Re-)configure TrustZone option bytes via OpenOCD
# 3. Run the secure world with semihosting I/O
#
# The option byte setup (TZEN, SECWM, SECBOOTADD0) only needs to be done
# once after a chip erase. Subsequent flashes can skip step 2 if OBs are
# already configured.
flash-hw: build-hw ## Flash + run on real STM32U585 (probe-rs/OpenOCD)
probe-rs download --chip STM32U585AIIx $(NONSECURE_ELF)
probe-rs download --chip STM32U585AIIx $(SECURE_ELF)
@echo "==> Configuring TrustZone option bytes..."
STM32_Programmer_CLI --connect port=SWD \
--optionbytes TZEN=1 SECWM1_PSTRT=0x0 SECWM1_PEND=0x7F \
SECWM2_PSTRT=0x7F SECWM2_PEND=0x0 SECBOOTADD0=0x180000
@echo "==> Resetting and attaching (Ctrl-C to quit)..."
probe-rs reset --chip STM32U585AIIx
probe-rs attach --chip STM32U585AIIx $(SECURE_ELF)
# Non-interactive automated end-to-end test for the sign dispatch logic.
# Builds both worlds with the `e2e-test` cargo feature, runs them in QEMU
# with stdin closed (no semihosting input needed), captures stdout, and
# asserts that the secure-world dispatcher routed each scenario to the
# right TxKind variant + that every scenario returned its expected status
# (positive cases `Ok`; negative binding/framing cases the pinned refusal).
#
# The authoritative scenario list is the `for line in ...` assertion
# block below (Scenarios 0a–6, including every emitted Scenario 5
# sub-scenario currently present in the firmware test driver). It
# covers value transfers, known/unknown ERC-20, blind-sign, slot
# rotation, Safe approveHash / exec clear-sign, selector + self-attest
# + ERC-7730 typed render, atomic batch sign, and Safe-wrapped CoW
# pre-sign, all through on-device native decode.
#
# Pass → exits 0. Any missing assertion or non-zero status → exits 1.
e2e: ## Automated unified-sign E2E (QEMU)
@echo "==> Building secure + nonsecure with e2e-test feature (QEMU mailbox transport)"
@$(RUSTFLAGS_VAR)="-C linker=arm-none-eabi-ld -C link-arg=-Tlink.x $(REPRO_FLAGS)" \
cargo build --locked --release --target $(TARGET) --target-dir target/secure \
-p sphincs-tz-secure --no-default-features \
--features mock-se,debug-log,ui-semihosting,e2e-test
@$(RUSTFLAGS_VAR)="-C linker=arm-none-eabi-ld -C link-arg=-Tlink.x $(REPRO_FLAGS)" \
cargo build --locked --release --target $(TARGET) --target-dir target/nonsecure \
-p sphincs-tz-nonsecure --features e2e-test
@echo "==> Running e2e suite under QEMU"
@# Route semihosting output through a stdio chardev so we see live
@# progress AND can grep for assertions. `chardev null` (the previous
@# setting) silently discarded every `hprintln!` line, which masked
@# real test failures and made hangs invisible. The NS panic handler
@# uses panic-semihosting's `exit` feature (enabled via the
@# `e2e-test` cargo feature) so a failed assertion terminates QEMU
@# instead of looping forever — without that, this target would
@# never return on any test bug.
@log=$$(mktemp); \
qemu-system-arm \
-M mps2-an505 \
-monitor null \
-serial null \
-nographic \
-chardev stdio,id=hostio \
-semihosting-config enable=on,target=native,chardev=hostio \
-kernel $(SECURE_ELF) \
-device loader,file=$(NONSECURE_ELF) </dev/null 2>&1 | tee $$log; \
echo "===================================="; \
fail=0; \
for line in \
"\\[NS\\]\\[e2e\\] Scenario 0a: mismatched single sender is refused" \
"\\[NS\\]\\[e2e\\] Scenario 0b: mismatched batch sender is refused" \
"\\[NS\\]\\[e2e\\] Scenario 0c: cross-account sender is refused" \
"\\[NS\\]\\[e2e\\] Scenario 0d: mismatched single EntryPoint is refused" \
"\\[NS\\]\\[e2e\\] Scenario 0e: mismatched batch EntryPoint is refused" \
"\\[NS\\]\\[e2e\\] Scenario 0f: selector-only Safe exec is refused" \
"\\[NS\\]\\[e2e\\] Scenario 0g: selector-only Safe exec batch is refused" \
"\\[NS\\]\\[e2e\\] Scenario 0h: canonical Safe exec remains signable" \
"\\[NS\\]\\[e2e\\] Scenario 1: register slot 1 on chain A" \
"\\[NS\\]\\[e2e\\] Scenario 2: repeat sign on chain A slot 1" \
"\\[NS\\]\\[e2e\\] Scenario 3: rotate to slot 2 on chain A" \
"\\[NS\\]\\[e2e\\] Scenario 4: register slot 1 on chain B" \
"\\[NS\\]\\[e2e\\] Scenario 5: Safe approveHash clear-sign" \
"\\[NS\\]\\[e2e\\] Scenario 5b: verified function-selector bundle" \
"\\[NS\\]\\[e2e\\] Scenario 5v: companion-supplied ERC-20 metadata trailer" \
"\\[NS\\]\\[e2e\\] Scenario 5w: companion-supplied address-name trailer" \
"\\[NS\\]\\[e2e\\] Scenario 5c: cross-check rejects mismatched selector" \
"\\[NS\\]\\[e2e\\] Scenario 5d: typed walker declines, blind-sign fallback" \
"\\[NS\\]\\[e2e\\] Scenario 5e: atomic batch sign" \
"\\[NS\\]\\[e2e\\] Scenario 5e-7730: batch ERC-7730 trailer matches + signs" \
"\\[NS\\]\\[e2e\\] Scenario 5e-rt-erc20: invalid Safe cannot gate ERC-7730 token metadata" \
"\\[NS\\]\\[e2e\\] Scenario 5e-7730-mismatch: batch mis-bound descriptor is refused" \
"\\[NS\\]\\[e2e\\] Scenario 5f: degenerate 1-tx batch" \
"\\[NS\\]\\[e2e\\] Scenario 5g: max-size batch" \
"\\[NS\\]\\[e2e\\] Scenario 5h: empty batch is refused" \
"\\[NS\\]\\[e2e\\] Scenario 5i: truncated inner-tx block is refused" \
"\\[NS\\]\\[e2e\\] Scenario 5j: self-attest typed render" \
"\\[NS\\]\\[e2e\\] Scenario 5k: self-attest keccak mismatch dropped" \
"\\[NS\\]\\[e2e\\] Scenario 5l: both selector trailers refused" \
"\\[NS\\]\\[e2e\\] Scenario 5m: ERC-7730 trailer matches + signs" \
"\\[NS\\]\\[e2e\\] Scenario 5m-nested: ERC-7730 nested proof set matches + signs" \
"\\[NS\\]\\[e2e\\] Scenario 5m-multi-tail: ERC-7730 two-string tails match + signs" \
"\\[NS\\]\\[e2e\\] Scenario 5p: EIP-712 typed sign + binding differential" \
"\\[NS\\]\\[e2e\\] Scenario 5n: known-call mis-bound descriptor is refused" \
"\\[NS\\]\\[e2e\\] Scenario 5q: Safe-wrapped CoW presign clear-sign" \
"\\[NS\\]\\[e2e\\] Scenario 5r: safe-wrapped presign without cow_order is refused" \
"\\[NS\\]\\[e2e\\] Scenario 5s: multiSend (approve+presign) safe-wrapped CoW clear-sign" \
"\\[NS\\]\\[e2e\\] Scenario 5t: multiSend with a delegatecall record is refused" \
"\\[NS\\]\\[e2e\\] Scenario 5u: multiSend presign without cow_order is refused" \
"\\[NS\\]\\[e2e\\] Scenario 6: brute-force protection" \
"\\[NS\\]\\[e2e\\] === All scenarios passed! ==="; do \
if grep -q "$$line" $$log; then \
echo " PASS $$line"; \
else \
echo " MISS $$line"; \
fail=1; \
fi; \
done; \
names_region=$$(mktemp); \
awk '/Scenario 5w:/{capture=1} capture{print} /names bundle verified/{exit}' $$log > $$names_region; \
for text in \
"+ Uniswap V3 Rou" \
" ter" \
"0xE59242..861564" \
"0.000001 ETH"; do \
if ! grep -Fq "$$text" $$names_region; then \
echo " MISS names trailer trusted row: $$text"; \
fail=1; \
fi; \
done; \
rm -f $$names_region; \
if ! grep -q "\\[ERC-7730\\] matched: chain=31337 contract=0x34343434..34343434 .* nested=true" $$log; then \
echo " MISS secure nested ERC-7730 dispatch receipt"; fail=1; \
fi; \
rt_region=$$(mktemp); \
awk '/Scenario 5e-rt-erc20:/{capture=1} capture{print} /RT-ERC20 trusted pages complete/{exit}' $$log > $$rt_region; \
for text in \
"0x1CDD2EaB611126" \
"97626F7b4bB0e23D" \
"a4FeBF7B7C" \
"0xdAC17F958D2ee5" \
"23a2206206994597" \
"C13D831ec7"; do \
if ! grep -Fq "$$text" $$rt_region; then \
echo " MISS RT-ERC20 trusted row: $$text"; \
fail=1; \
fi; \
done; \
if [ $$(grep -Fc "Token contract" $$rt_region) -lt 2 ]; then \
echo " MISS RT-ERC20 two exact token-identity pages"; fail=1; \
fi; \
if [ $$(grep -Fc "Amount" $$rt_region) -lt 2 ] || [ $$(grep -Fc "USDT" $$rt_region) -lt 2 ]; then \
echo " MISS RT-ERC20 two decoded amount+ticker displays"; fail=1; \
fi; \
if grep -Fq "Token (UNVERI" $$rt_region || grep -Fq "Approve Safe TX" $$rt_region; then \
echo " FAIL RT-ERC20 regressed to unverified or Safe-attributed display"; fail=1; \
fi; \
rm -f $$rt_region; \
rm -f $$log; \
if [ $$fail -eq 0 ]; then \
echo "==> e2e: ALL ASSERTIONS PASSED"; \
exit 0; \
else \
echo "==> e2e: ASSERTIONS FAILED"; \
exit 1; \
fi
# Fully-automated signing benchmark on real STM32U585.
#
# Clocks the MCU to 160 MHz (hw::rcc::init, the default for the stm32u585
# build path) and uses the DWT cycle counter (armed in secure main.rs
# before booting NS) to measure wall-clock time for:
#
# A) first-sign on a fresh chain — Type 1 (C11) + slot keygen + Type 2
# B) 5 x subsequent signs on same chain — Type 2 only (slot cached)
# C) first-sign on a second chain — another Type 1 data point
#
# The secure crate builds with `e2e-test` so the wallet auto-provisions
# a fixed mnemonic and pre-unlocks the gateway (no PIN UI). The NS crate
# builds with `bench-key-speed`, which swaps main() for the bench runner
# in `nonsecure/src/bench_key_speed.rs`.
#
# Why this exists: motivates evaluating the SHA-256 hash variant (see
# `docs/SHA256_VARIANT.md`), where the STM32U585 HASH peripheral would
# accelerate signing ~10x vs software Keccak. This target establishes
# the baseline number.
#
# Requires: ST-LINK connected, STM32_Programmer_CLI on PATH.
# Pass: exits 0 with "[NS][bench] === PASS ===" on stdout.
# Fail: exits 1 if any sign returns non-Ok or the PASS line is missing.
test-key-speed: ## DWT-timed signing bench on HW
@echo "==> Building secure (e2e-test auto-provision) + NS (bench-key-speed) + SHA-256 HW accel"
@FSBL_VENDOR_PUBKEY=$(DEV_VENDOR_PUBKEY) $(RUSTFLAGS_VAR)="$(RUSTFLAGS_SECURE_HW)" \
cargo build --locked --release --target $(TARGET) --target-dir target/secure \
-p sphincs-tz-secure --no-default-features \
--features mock-se,debug-log,ui-semihosting,e2e-test,stm32u585,hw-sha256
@rm -f $(NONSECURE_ELF) target/nonsecure/$(TARGET)/release/deps/sphincs_tz_nonsecure-*
@$(RUSTFLAGS_VAR)="$(RUSTFLAGS_NONSECURE_HW)" \
cargo build --locked --release --target $(TARGET) --target-dir target/nonsecure \
-p sphincs-tz-nonsecure --features bench-key-speed,stm32u585
@echo "==> Flashing..."
@probe-rs download --chip STM32U585AIIx $(NONSECURE_ELF)
@probe-rs download --chip STM32U585AIIx $(SECURE_ELF)
@echo "==> Configuring TrustZone option bytes..."
@STM32_Programmer_CLI --connect port=SWD \
--optionbytes TZEN=1 SECWM1_PSTRT=0x0 SECWM1_PEND=0x7F \
SECWM2_PSTRT=0x7F SECWM2_PEND=0x0 SECBOOTADD0=0x180000
@echo "==> Running key-speed bench on hardware (160 MHz)..."
@echo " (streaming semihosting output; each [NS][bench] line = one measurement)"
@log=$$(mktemp -t test-key-speed.XXXXXX.log); \
trap 'rm -f "$$log"' EXIT; \
rc_file=$$(mktemp -t test-key-speed-rc.XXXXXX); \
trap 'rm -f "$$log" "$$rc_file"' EXIT; \
{ probe-rs run --chip STM32U585AIIx $(SECURE_ELF) 2>&1; echo $$? >"$$rc_file"; } | tee "$$log"; \
rc=$$(cat "$$rc_file"); \
echo "===================================="; \
if [ "$$rc" != "0" ] && [ "$$rc" != "130" ]; then \
echo "==> test-key-speed: FAIL (probe-rs exited $$rc)"; \
exit 1; \
fi; \
if grep -q "\[NS\]\[bench\] === PASS ===" "$$log"; then \
echo "==> test-key-speed: PASS"; \
exit 0; \
else \
echo "==> test-key-speed: FAIL (missing PASS marker)"; \
exit 1; \
fi
# Automated, non-destructive test of the firmware-update (CMD_FW_*)
# logic on real STM32U585 hardware. NS side runs `fwup_hw_test.rs`,
# which walks every FW_* command through its verify chain and rejects
# paths — including a full-chain "valid-but-rollback-rejected" manifest
# that proves structural + CRC + digest + vendor-fpr all work end-to-end.
#
# WHAT THIS DOES NOT DO (on purpose — both are irreversible / destructive
# to the currently-running firmware on the pre-A/B-split branch):
#
# * Never calls CMD_FW_COMMIT → no OTP program command is launched.
# The legacy per-bit tally is invalid on STM32U585 ECC quad-words and is
# production-fenced; this test neither exercises nor consumes the still-
# open Draft-1.1 research-candidate epoch-floor backend.
# * Never lets CMD_FW_BEGIN reach `flash::erase_slot(inactive)`. On
# the current linker layout the inactive slot's manifest page (page
# 5 @ 0x0C00_A000) still sits inside the running secure firmware's
# .text region — erasing it would hard-fault the CPU. We craft the
# happy-path test manifest with fw_version=0 so it exercises
# structural / CRC / digest / fpr checks and then rejects at the
# rollback-floor gate (fw_version > floor is strict, floor >= 0),
# which is the last check before `erase_slot` would run.
#
# The only first-boot one-way side-effect is `otp::ensure_device_master`
# (burns the per-device OTP master key on first-boot of a blank MCU —
# this happens on every hardware boot of this firmware, not just this
# target, so there is nothing new here).
#
# Requires: ST-LINK connected, STM32_Programmer_CLI on PATH.
# Pass: exits 0 with "[NS][fwup-test] === PASS ===" on stdout.
# Fail: exits 1 if any test case fails or the PASS marker is missing.
test-update-hw: ## Firmware-update (CMD_FW_*) E2E on HW (non-destructive)
@echo "==> Building secure (e2e-test auto-unlock) + NS (fwup-hw-test) + SHA-256 HW accel"
@FSBL_VENDOR_PUBKEY=$(DEV_VENDOR_PUBKEY) $(RUSTFLAGS_VAR)="$(RUSTFLAGS_SECURE_HW)" \
cargo build --locked --release --target $(TARGET) --target-dir target/secure \
-p sphincs-tz-secure --no-default-features \
--features mock-se,debug-log,ui-semihosting,e2e-test,stm32u585,hw-sha256
@rm -f $(NONSECURE_ELF) target/nonsecure/$(TARGET)/release/deps/sphincs_tz_nonsecure-*
@$(RUSTFLAGS_VAR)="$(RUSTFLAGS_NONSECURE_HW)" \
cargo build --locked --release --target $(TARGET) --target-dir target/nonsecure \
-p sphincs-tz-nonsecure --features fwup-hw-test,stm32u585
@echo "==> Flashing..."
@probe-rs download --chip STM32U585AIIx $(NONSECURE_ELF)
@probe-rs download --chip STM32U585AIIx $(SECURE_ELF)
@echo "==> Configuring TrustZone option bytes..."
@STM32_Programmer_CLI --connect port=SWD \
--optionbytes TZEN=1 SECWM1_PSTRT=0x0 SECWM1_PEND=0x7F \
SECWM2_PSTRT=0x7F SECWM2_PEND=0x0 SECBOOTADD0=0x180000
@echo "==> Running firmware-update logic test (safe mode)..."
@echo " (no COMMIT, no slot erase — nothing irreversible will happen)"
@log=$$(mktemp -t test-update-hw.XXXXXX.log); \
rc_file=$$(mktemp -t test-update-hw-rc.XXXXXX); \
trap 'rm -f "$$log" "$$rc_file"' EXIT; \
{ probe-rs run --chip STM32U585AIIx $(SECURE_ELF) 2>&1; echo $$? >"$$rc_file"; } | tee "$$log"; \
rc=$$(cat "$$rc_file"); \
echo "===================================="; \
if [ "$$rc" != "0" ] && [ "$$rc" != "130" ]; then \
echo "==> test-update-hw: FAIL (probe-rs exited $$rc)"; \
exit 1; \
fi; \
if grep -q "\[NS\]\[fwup-test\] === PASS ===" "$$log"; then \
echo "==> test-update-hw: PASS"; \
exit 0; \
else \
echo "==> test-update-hw: FAIL (missing PASS marker)"; \
exit 1; \
fi
# Same e2e suite but on real STM32U585 hardware via probe-rs semihosting.
# Requires: ST-LINK connected, STM32_Programmer_CLI on PATH.
# Phase 5 item 8 — ERC-7730 e2e on real STM32U585 hardware. Drives the
# Scenario 5m + 5p clear-signing paths through probe-rs semihosting +
# arrow-key forwarder. Requires the same hardware bench as `e2e-hw`
# plus a UI device for descriptor confirmation. Stubbed — implementation
# defers to the Phase 5+ EIP-712 descriptor mirror landing first so
# Scenario 5p has a happy path to assert against.
e2e-erc7730-hw:
@echo "HW required — run on STM32U585 host with probe-rs + ST-LINK +"
@echo " the Phase 5+ EIP-712 descriptor mirror landed (handoff item 2)."
@echo "Until then this target fails by design so CI doesn't silently skip"
@echo " the hardware parity gate. See docs/archive/handoff-erc7730-phase5.md item 8."
@false
e2e-hw: ## Unified-sign E2E on real STM32U585 (probe-rs)
@echo "==> Building e2e + stm32u585"
@FSBL_VENDOR_PUBKEY=$(DEV_VENDOR_PUBKEY) $(RUSTFLAGS_VAR)="$(RUSTFLAGS_SECURE_HW)" \
cargo build --locked --release --target $(TARGET) --target-dir target/secure \
-p sphincs-tz-secure --no-default-features \
--features mock-se,debug-log,ui-semihosting,e2e-test,stm32u585
@rm -f $(NONSECURE_ELF) target/nonsecure/$(TARGET)/release/deps/sphincs_tz_nonsecure-*
@$(RUSTFLAGS_VAR)="$(RUSTFLAGS_NONSECURE_HW)" \
cargo build --locked --release --target $(TARGET) --target-dir target/nonsecure \
-p sphincs-tz-nonsecure --features e2e-test,stm32u585
@echo "==> Flashing..."
@probe-rs download --chip STM32U585AIIx $(NONSECURE_ELF)
@probe-rs download --chip STM32U585AIIx $(SECURE_ELF)
@echo "==> Configuring TrustZone option bytes..."
@STM32_Programmer_CLI --connect port=SWD \
--optionbytes TZEN=1 SECWM1_PSTRT=0x0 SECWM1_PEND=0x7F \
SECWM2_PSTRT=0x7F SECWM2_PEND=0x0 SECBOOTADD0=0x180000
@echo "==> Running e2e on hardware (Ctrl-C to abort)..."
@probe-rs run --chip STM32U585AIIx $(SECURE_ELF)
# Same e2e suite on real STM32U585, but with OLED display output.
# The SSD1306 128x64 OLED is driven via I2C1 (PB8=SCL, PB9=SDA).
# Uses ui-lcd instead of ui-semihosting so the UI renders on the
# physical display rather than the probe-rs console.
# Requires: ST-LINK connected, SSD1306 OLED wired to PB8/PB9/3V3/GND.
e2e-hw-display:
@echo "==> Building e2e + stm32u585 + OLED display"
@FSBL_VENDOR_PUBKEY=$(DEV_VENDOR_PUBKEY) $(RUSTFLAGS_VAR)="$(RUSTFLAGS_SECURE_HW)" \
cargo build --locked --release --target $(TARGET) --target-dir target/secure \
-p sphincs-tz-secure --no-default-features \
--features mock-se,debug-log,ui-lcd,e2e-test,stm32u585
@rm -f $(NONSECURE_ELF) target/nonsecure/$(TARGET)/release/deps/sphincs_tz_nonsecure-*
@$(RUSTFLAGS_VAR)="$(RUSTFLAGS_NONSECURE_HW)" \
cargo build --locked --release --target $(TARGET) --target-dir target/nonsecure \
-p sphincs-tz-nonsecure --features e2e-test,stm32u585
@echo "==> Flashing..."
@probe-rs download --chip STM32U585AIIx $(NONSECURE_ELF)
@probe-rs download --chip STM32U585AIIx $(SECURE_ELF)
@echo "==> Configuring TrustZone option bytes..."
@STM32_Programmer_CLI --connect port=SWD \
--optionbytes TZEN=1 SECWM1_PSTRT=0x0 SECWM1_PEND=0x7F \
SECWM2_PSTRT=0x7F SECWM2_PEND=0x0 SECBOOTADD0=0x180000
@echo "==> Running e2e on hardware with OLED display (Ctrl-C to abort)..."
@probe-rs run --chip STM32U585AIIx $(SECURE_ELF)
# Full sign e2e on real STM32U585 with *both* real SEs (OPTIGA
# Trust M + SE050, XOR-split entropy) driving the SSD1306 OLED.
#
# Exercises the post-cutover stateless-slot flow (Type 1 + Type 2,
# cross-chain slot rotation) end-to-end through real silicon:
# * dual-se — OPTIGA + SE050 XOR-split provision + unlock
# * ui-lcd — status on the physical SSD1306 (PB8=SCL, PB9=SDA)
# * e2e-test — auto-provisions fixed mnemonic + PIN, pre-unlocks
# the gateway (probe-rs cannot serve SYS_READC)
# * otp-hardcoded-master-key — avoids burning real OTP each run
# (same choice as dual-se-admin-wipe-e2e)
#
# Requires: ST-LINK, STM32_Programmer_CLI, OPTIGA Trust M + SE050 on
# the I2C bus, SSD1306 OLED wired to PB8/PB9/3V3/GND.
#
# Watch semihosting for "[NS][e2e] === All scenarios passed! ===".
# OLED will show "e2e Sign N/4" + "T1+T2"/"T2 only" on each sign.
e2e-hw-dual-se:
@echo "==> Building e2e + stm32u585 + dual-SE (OPTIGA + SE050) + OLED"
@echo " WARNING: re-provisions wallet state on BOTH chips with the"
@echo " fixed e2e test mnemonic (abandon × 23 || art, PIN 00000000)."
@FSBL_VENDOR_PUBKEY=$(DEV_VENDOR_PUBKEY) $(RUSTFLAGS_VAR)="$(RUSTFLAGS_SECURE_HW)" \
cargo build --locked --release --target $(TARGET) --target-dir target/secure \
-p sphincs-tz-secure --no-default-features \
--features dual-se,ui-lcd,debug-log,e2e-test,e2e-skip-admin-wipe,stm32u585,otp-hardcoded-master-key
@rm -f $(NONSECURE_ELF) target/nonsecure/$(TARGET)/release/deps/sphincs_tz_nonsecure-*
@$(RUSTFLAGS_VAR)="$(RUSTFLAGS_NONSECURE_HW)" \
cargo build --locked --release --target $(TARGET) --target-dir target/nonsecure \
-p sphincs-tz-nonsecure --features e2e-test,stm32u585
@echo "==> Flashing..."
@probe-rs download --chip STM32U585AIIx $(NONSECURE_ELF)
@probe-rs download --chip STM32U585AIIx $(SECURE_ELF)
@echo "==> Configuring TrustZone option bytes..."
@STM32_Programmer_CLI --connect port=SWD \
--optionbytes TZEN=1 SECWM1_PSTRT=0x0 SECWM1_PEND=0x7F \
SECWM2_PSTRT=0x7F SECWM2_PEND=0x0 SECBOOTADD0=0x180000
@echo "==> Running dual-SE e2e on hardware..."
@echo " (streaming semihosting; looks for 'All scenarios passed!'"
@echo " then exits — hit Ctrl-C if it hangs past 2 min)"
@log=$$(mktemp -t e2e-hw-dual-se.XXXXXX.log); \
rc_file=$$(mktemp -t e2e-hw-dual-se-rc.XXXXXX); \
trap 'rm -f "$$log" "$$rc_file"' EXIT; \
{ timeout 300 probe-rs run --chip STM32U585AIIx $(SECURE_ELF) 2>&1; \
echo $$? >"$$rc_file"; } | tee "$$log"; \
rc=$$(cat "$$rc_file"); \
echo "===================================="; \
if grep -q "All scenarios passed!" "$$log"; then \
echo "==> e2e-hw-dual-se: PASS"; \
exit 0; \
elif grep -q "PANIC\|FAIL" "$$log"; then \
echo "==> e2e-hw-dual-se: FAIL (see log above)"; \
exit 1; \
else \
echo "==> e2e-hw-dual-se: FAIL (no PASS/FAIL marker; rc=$$rc)"; \
exit 1; \
fi
# GTZC1 TZSC + TZIC enforcement validation on real STM32U585 silicon.
#
# Production gate for invariant #4 ("all secrets live ONLY in TrustZone
# secure world"). NS probes each peripheral the secure-world boot path
# marked SECURE in `secure/src/sau.rs` (I2C1/2, AES, HASH, RNG, PKA,
# SAES) via its NS-aliased control register. Each access should be
# RAZ-gated by the AHB bridge and raise NVIC IRQ 8 (GTZC), bumping the
# secure-world `hw::tzic::VIOLATION_COUNT`. The NS driver reads the
# counter back via `nsc_tzic_status` CMSE veneer and asserts.
#
# Secure side:
# mock-se — skips dual-SE provisioning (we're not signing)
# ui-semihosting — secure-side `[S][TZIC]` lines come out on probe-rs
# debug-log — `secure_log!` enabled
# e2e-test — pre-unlock + exposes `nsc_tzic_status` veneer
# stm32u585 — real GTZC1 (not the QEMU MPC fallback)
# otp-hardcoded-master-key — stable OTP master across re-flashes
#
# NS side:
# gtzc-test — replaces interactive main() with probe + assert
# stm32u585 — real hardware target
#
# Greps for `[NS][gtzc] === PASS ===` on stdout; missing marker = FAIL.
#
# Requires: ST-LINK on B-U585I-IOT02A. Non-destructive (no SE writes,
# no PIN attempts). Safe to re-run.
gtzc-enforcement-hw: ## 7/7 secure-peripheral RAZ-fault on NS access (HW)
@echo "==> Building GTZC1 enforcement test (secure + stm32u585 + e2e-test + mock-se)"
@FSBL_VENDOR_PUBKEY=$(DEV_VENDOR_PUBKEY) $(RUSTFLAGS_VAR)="$(RUSTFLAGS_SECURE_HW)" \
cargo build --locked --release --target $(TARGET) --target-dir target/secure \
-p sphincs-tz-secure --no-default-features \
--features mock-se,ui-semihosting,debug-log,e2e-test,stm32u585,otp-hardcoded-master-key
@echo "==> Building GTZC1 enforcement test (NS + gtzc-test + stm32u585)"
@rm -f $(NONSECURE_ELF) target/nonsecure/$(TARGET)/release/deps/sphincs_tz_nonsecure-*
@$(RUSTFLAGS_VAR)="$(RUSTFLAGS_NONSECURE_HW)" \
cargo build --locked --release --target $(TARGET) --target-dir target/nonsecure \
-p sphincs-tz-nonsecure --features gtzc-test,stm32u585
@echo "==> Flashing..."
@probe-rs download --chip STM32U585AIIx $(NONSECURE_ELF)
@probe-rs download --chip STM32U585AIIx $(SECURE_ELF)
@echo "==> Configuring TrustZone option bytes..."
@STM32_Programmer_CLI --connect port=SWD \
--optionbytes TZEN=1 SECWM1_PSTRT=0x0 SECWM1_PEND=0x7F \
SECWM2_PSTRT=0x7F SECWM2_PEND=0x0 SECBOOTADD0=0x180000
@echo "==> Running GTZC enforcement validation on hardware..."
@log=$$(mktemp -t gtzc-enforcement-hw.XXXXXX.log); \
rc_file=$$(mktemp -t gtzc-enforcement-hw-rc.XXXXXX); \
trap 'rm -f "$$log" "$$rc_file"' EXIT; \
{ timeout 120 probe-rs run --chip STM32U585AIIx $(SECURE_ELF) 2>&1; \
echo $$? >"$$rc_file"; } | tee "$$log"; \
rc=$$(cat "$$rc_file"); \
echo "===================================="; \
if grep -q "\[NS\]\[gtzc\] === PASS ===" "$$log"; then \
echo "==> gtzc-enforcement-hw: PASS — GTZC1 TZSC + TZIC enforcement confirmed"; \
exit 0; \
elif grep -q "\[NS\]\[gtzc\] === FAIL" "$$log"; then \
echo "==> gtzc-enforcement-hw: FAIL — violation counter mismatch (see log above)"; \
exit 1; \
else \
echo "==> gtzc-enforcement-hw: FAIL (no PASS/FAIL marker; rc=$$rc)"; \
exit 1; \
fi
# Slice 2 demo: GTZC1 illegal-access → wipe escalation on real
# STM32U585 silicon.
#
# Builds with `tzic-wipe` ON in the secure crate. NS does a single
# probe of HASH_CR's NS alias; the TZIC IRQ fires, runs
# `hw::tzic::trigger_tzic_wipe()` (zeroize SRAM → arm page-125 wipe
# flag → SCB::sys_reset). The NS driver never reaches its
# `SURVIVED` log line — its absence is the pass marker.
#
# probe-rs note: `probe-rs run` arms vector-catch-on-reset, so a
# successful `SCB::sys_reset` from the IRQ is intercepted and
# surfaces as "Firmware exited unexpectedly: Exception" rather
# than a clean reboot loop. That's the EXPECTED success state for
# this harness — the chip *did* reset, probe-rs just caught it.
# On stand-alone power-up the chip reboots normally and the boot-
# time wipe-resume path drives the full SE wipe.
#
# Pass criteria (host-side):
# * `[NS][gtzc-wipe] probing` appears exactly 1 time
# * `[NS][gtzc-wipe] SURVIVED` appears 0 times (wipe preempted)
#
# Side-effect: leaves the page-125 wipe-armed flag set. Subsequent
# boots of a `se050` or `dual-se` build will finish the SE wipe on
# the next boot. Run `make wipe-for-wizard` to clear deliberately,
# or any normal e2e target that includes `factory_reset_admin`.
#
# Requires: ST-LINK on B-U585I-IOT02A.
tzic-wipe-hw:
@echo "==> Building TZIC wipe demo (secure + stm32u585 + tzic-wipe + e2e-test + mock-se)"
@FSBL_VENDOR_PUBKEY=$(DEV_VENDOR_PUBKEY) $(RUSTFLAGS_VAR)="$(RUSTFLAGS_SECURE_HW)" \
cargo build --locked --release --target $(TARGET) --target-dir target/secure \
-p sphincs-tz-secure --no-default-features \
--features mock-se,ui-semihosting,debug-log,e2e-test,stm32u585,otp-hardcoded-master-key,tzic-wipe
@echo "==> Building TZIC wipe demo (NS + tzic-wipe-test + stm32u585)"
@rm -f $(NONSECURE_ELF) target/nonsecure/$(TARGET)/release/deps/sphincs_tz_nonsecure-*
@$(RUSTFLAGS_VAR)="$(RUSTFLAGS_NONSECURE_HW)" \
cargo build --locked --release --target $(TARGET) --target-dir target/nonsecure \
-p sphincs-tz-nonsecure --features tzic-wipe-test,stm32u585
@echo "==> Flashing..."
@probe-rs download --chip STM32U585AIIx $(NONSECURE_ELF)
@probe-rs download --chip STM32U585AIIx $(SECURE_ELF)
@echo "==> Configuring TrustZone option bytes..."
@STM32_Programmer_CLI --connect port=SWD \
--optionbytes TZEN=1 SECWM1_PSTRT=0x0 SECWM1_PEND=0x7F \
SECWM2_PSTRT=0x7F SECWM2_PEND=0x0 SECBOOTADD0=0x180000
@echo "==> Running TZIC wipe demo on hardware (30 s probe-then-reset)..."
@log=$$(mktemp -t tzic-wipe-hw.XXXXXX.log); \
trap 'rm -f "$$log"' EXIT; \
timeout 30 probe-rs run --chip STM32U585AIIx $(SECURE_ELF) 2>&1 | tee "$$log" || true; \
probes=$$(grep -c '\[NS\]\[gtzc-wipe\] probing' "$$log" || true); \
survived=$$(grep -c '\[NS\]\[gtzc-wipe\] SURVIVED' "$$log" || true); \
reset_seen=$$(grep -c 'Exception\|Firmware exited' "$$log" || true); \
echo "===================================="; \
echo "==> Observed: probes=$$probes survived=$$survived reset_intercepted=$$reset_seen"; \
if [ "$$survived" -gt 0 ]; then \
echo "==> tzic-wipe-hw: FAIL — saw SURVIVED line; IRQ did not preempt"; \
exit 1; \
elif [ "$$probes" -ge 1 ] && [ "$$reset_seen" -ge 1 ]; then \
echo "==> tzic-wipe-hw: PASS — TZIC IRQ ran wipe path and chip reset (probe-rs intercepted)"; \
exit 0; \
else \
echo "==> tzic-wipe-hw: FAIL — probes=$$probes reset_seen=$$reset_seen"; \
exit 1; \
fi
# Real STM32U585 hardware build with USB HID host communication.
# Uses mock SE + semihosting debug output + USB transport.
build-hw-usb:
$(MAKE) FEATURES=mock-se,debug-log,ui-semihosting,stm32u585,usb all
# USB build with auto-provisioning for standalone testing.
# No debug-log (semihosting BKPT faults without debugger attached).
# Secure world: e2e-test auto-provisions, ui-semihosting for compile compat.
# NS world: usb feature for USB HID main loop.
build-hw-usb-test:
$(RUSTFLAGS_VAR)="$(RUSTFLAGS_SECURE_HW)" \
cargo build --locked --release --target $(TARGET) --target-dir target/secure \
-p sphincs-tz-secure --no-default-features --features mock-se,ui-noop,stm32u585,usb,e2e-test
@rm -f $(NONSECURE_ELF) target/nonsecure/$(TARGET)/release/deps/sphincs_tz_nonsecure-*
$(RUSTFLAGS_VAR)="$(RUSTFLAGS_NONSECURE_HW)" \
cargo build --locked --release --target $(TARGET) --target-dir target/nonsecure -p sphincs-tz-nonsecure --features stm32u585,usb
@echo "==> USB test build ready (auto-provisioned, no semihosting)."
# Flash auto-provisioned USB build.
flash-hw-usb-test: build-hw-usb-test
probe-rs download --chip STM32U585AIIx $(NONSECURE_ELF)
probe-rs download --chip STM32U585AIIx $(SECURE_ELF)
@echo "==> Configuring TrustZone option bytes..."
STM32_Programmer_CLI --connect port=SWD \
--optionbytes TZEN=1 SECWM1_PSTRT=0x0 SECWM1_PEND=0x7F \
SECWM2_PSTRT=0x7F SECWM2_PEND=0x0 SECBOOTADD0=0x180000
@echo "==> Resetting and attaching (Ctrl-C to quit)..."
probe-rs reset --chip STM32U585AIIx
probe-rs attach --chip STM32U585AIIx $(SECURE_ELF)
# mock-se USB build WITH debug-log — boot-trace the USB path over probe-rs
# semihosting (does boot reach USB init / does it fault?). Diagnostic only.
build-hw-usb-test-debug:
$(RUSTFLAGS_VAR)="$(RUSTFLAGS_SECURE_HW)" \
cargo build --locked --release --target $(TARGET) --target-dir target/secure \
-p sphincs-tz-secure --no-default-features --features mock-se,ui-noop,stm32u585,usb,e2e-test,debug-log
@rm -f $(NONSECURE_ELF) target/nonsecure/$(TARGET)/release/deps/sphincs_tz_nonsecure-*
$(RUSTFLAGS_VAR)="$(RUSTFLAGS_NONSECURE_HW)" \
cargo build --locked --release --target $(TARGET) --target-dir target/nonsecure -p sphincs-tz-nonsecure --features stm32u585,usb
@echo "==> mock-se USB test (debug) build ready."
# SE050 + USB build with auto-provisioning for testing.
# Secure world: se050 (real SE via I2C1), ui-noop, USB hardware init, e2e-test auto-provision.
# NS world: usb feature for USB HID main loop.
build-hw-se050-usb-test:
$(RUSTFLAGS_VAR)="$(RUSTFLAGS_SECURE_HW)" \
cargo build --locked --release --target $(TARGET) --target-dir target/secure \
-p sphincs-tz-secure --no-default-features --features se050,ui-noop,stm32u585,usb,e2e-test
@rm -f $(NONSECURE_ELF) target/nonsecure/$(TARGET)/release/deps/sphincs_tz_nonsecure-*
$(RUSTFLAGS_VAR)="$(RUSTFLAGS_NONSECURE_HW)" \
cargo build --locked --release --target $(TARGET) --target-dir target/nonsecure -p sphincs-tz-nonsecure --features stm32u585,usb
@echo "==> SE050 + USB test build ready."
flash-hw-se050-usb-test: build-hw-se050-usb-test
probe-rs download --chip STM32U585AIIx $(NONSECURE_ELF)
probe-rs download --chip STM32U585AIIx $(SECURE_ELF)
@echo "==> Configuring TrustZone option bytes..."
STM32_Programmer_CLI --connect port=SWD \
--optionbytes TZEN=1 SECWM1_PSTRT=0x0 SECWM1_PEND=0x7F \
SECWM2_PSTRT=0x7F SECWM2_PEND=0x0 SECBOOTADD0=0x180000
@echo "==> Resetting and attaching (Ctrl-C to quit)..."
probe-rs reset --chip STM32U585AIIx
probe-rs attach --chip STM32U585AIIx $(SECURE_ELF)
# SE050 + USB test with semihosting debug output (requires probe-rs attach).
build-hw-se050-usb-test-debug:
$(RUSTFLAGS_VAR)="$(RUSTFLAGS_SECURE_HW)" \
cargo build --locked --release --target $(TARGET) --target-dir target/secure \
-p sphincs-tz-secure --no-default-features --features se050,ui-noop,stm32u585,usb,e2e-test,debug-log
@rm -f $(NONSECURE_ELF) target/nonsecure/$(TARGET)/release/deps/sphincs_tz_nonsecure-*
$(RUSTFLAGS_VAR)="$(RUSTFLAGS_NONSECURE_HW)" \
cargo build --locked --release --target $(TARGET) --target-dir target/nonsecure -p sphincs-tz-nonsecure --features stm32u585,usb
@echo "==> SE050 + USB test (debug) build ready."
flash-hw-se050-usb-test-debug: build-hw-se050-usb-test-debug
probe-rs download --chip STM32U585AIIx $(NONSECURE_ELF)
probe-rs download --chip STM32U585AIIx $(SECURE_ELF)
@echo "==> Configuring TrustZone option bytes..."
STM32_Programmer_CLI --connect port=SWD \
--optionbytes TZEN=1 SECWM1_PSTRT=0x0 SECWM1_PEND=0x7F \
SECWM2_PSTRT=0x7F SECWM2_PEND=0x0 SECBOOTADD0=0x180000
@echo "==> Resetting and attaching with semihosting (Ctrl-C to quit)..."
probe-rs reset --chip STM32U585AIIx
probe-rs attach --chip STM32U585AIIx $(SECURE_ELF)
# Real SE050 + GPIO hardware buttons + semihosting display.
# The SE050 runs over I2C1 (PB8/PB9 on the Arduino shield), buttons on
# CN13 D8/D9 jumper wires, and the UI renders via probe-rs semihosting.
# Interactive: PIN entry, seed wizard, signing — all on real hardware.
flash-hw-se050-buttons:
@echo "==> Building SE050 + GPIO buttons + semihosting UI"
$(RUSTFLAGS_VAR)="$(RUSTFLAGS_SECURE_HW)" \
cargo build --locked --release --target $(TARGET) --target-dir target/secure \
-p sphincs-tz-secure --no-default-features --features se050,gpio-buttons,debug-log,ui-semihosting,stm32u585,usb
@rm -f $(NONSECURE_ELF) target/nonsecure/$(TARGET)/release/deps/sphincs_tz_nonsecure-*
$(RUSTFLAGS_VAR)="$(RUSTFLAGS_NONSECURE_HW)" \
cargo build --locked --release --target $(TARGET) --target-dir target/nonsecure -p sphincs-tz-nonsecure --features stm32u585,usb
@echo "==> Flashing..."
@probe-rs download --chip STM32U585AIIx $(NONSECURE_ELF)
@probe-rs download --chip STM32U585AIIx $(SECURE_ELF)
@echo "==> Configuring TrustZone option bytes..."
@STM32_Programmer_CLI --connect port=SWD \
--optionbytes TZEN=1 SECWM1_PSTRT=0x0 SECWM1_PEND=0x7F \
SECWM2_PSTRT=0x7F SECWM2_PEND=0x0 SECBOOTADD0=0x180000
@echo "==> Running SE050 + buttons wallet (Ctrl-C to quit)..."
@echo " LEFT=CN13 pin1 (D8), RIGHT=CN13 pin2 (D9), GND=CN13 pin7"
probe-rs run --chip STM32U585AIIx $(SECURE_ELF)
# GPIO button test: scan Arduino header pins, then test debounced events.
# Requires: jumper wires on CN14 (D8=LEFT, D9=RIGHT, pin7=GND).
button-test:
@echo "==> Building GPIO button test firmware..."
$(RUSTFLAGS_VAR)="$(RUSTFLAGS_SECURE_HW)" \
cargo build --locked --release --target $(TARGET) --target-dir target/secure \
-p sphincs-tz-secure --no-default-features --features button-test,debug-log,ui-semihosting
@echo "==> Flashing button test firmware..."
probe-rs download --chip STM32U585AIIx $(SECURE_ELF)
@echo "==> Running button test (Ctrl-C to quit)..."
probe-rs run --chip STM32U585AIIx $(SECURE_ELF)
# STSAFE-A110 I2C2 bus probe: detect on-board secure element.
# Scans I2C2 (PH4/PH5) for the STSAFE-A110 at 0x20 and any other devices.
stsafe-probe:
@echo "==> Building STSAFE-A110 I2C2 probe firmware..."
$(RUSTFLAGS_VAR)="$(RUSTFLAGS_SECURE_HW)" \
cargo build --locked --release --target $(TARGET) --target-dir target/secure \
-p sphincs-tz-secure --no-default-features --features stsafe-probe,debug-log,ui-semihosting
@echo "==> Flashing probe firmware..."
probe-rs download --chip STM32U585AIIx $(SECURE_ELF)
@echo "==> Running I2C2 bus scan (Ctrl-C to quit)..."
probe-rs run --chip STM32U585AIIx $(SECURE_ELF)
# SE050 factory reset: wipe all objects, then halt.
# Run this once to clear stale SE050 state, then flash normal firmware.
# The firmware sweeps UserIDs 0x7B10_0000 (current v6 range), 0x7B0E_0000,
# 0x7B06_0000, 0x7B00_2000 (legacy) against PINs {00000000, 12345678,
# 11111111}. Each wrong attempt consumes one of the SE050's 10 PIN tries
# against that UserID; a correct PIN auto-resets the counter. Status
# reported on LCD + semihosting: clean / wrong-PIN / blocked.
#
# Feature notes: this is a hardware (stm32u585) release image, so it MUST
# carry `dev-testkey` to clear the `nsc/mod.rs` ship-blocker fences
# (debug-log / factory-default-SCP03 / consumption-mask) — the same fences
# the normal `*-standalone-debug` builds satisfy. It also needs `usb` so
# `hw::usb_hw` (referenced unconditionally by cmd_fw_begin/commit) compiles.
# `dev-testkey` substitutes the OTP master with the same compile-time
# constant the bench firmware uses, so the SCP03 channel + admin keys match
# the provisioned chip. The wipe itself only needs the user PIN, not admin.
se050-reset:
@echo "==> Building SE050 factory-reset firmware..."
@echo " Sweeps UserIDs {0x7B10_0000, 0x7B0E_0000, 0x7B06_0000, 0x7B00_2000}"
@echo " against dev PINs {00000000, 12345678, 11111111}."
@# Redirect the CMSE import-library to a reset-specific path so this
@# secure relink does NOT clobber the shared $(VENEERS). Otherwise a
@# later cache-HIT `flash-hw-*` (secure not rebuilt → veneers.o NOT
@# regenerated) would link the NS image against THIS build's veneer
@# addresses → NS calls land on non-SG addresses → SecureFault INVEP
@# at the first gateway call. (Hit on bench board #1, 2026-06-29.)
$(RUSTFLAGS_VAR)="$(subst $(VENEERS),$(CURDIR)/target/veneers-se050-reset.o,$(RUSTFLAGS_SECURE_HW))" \
cargo build --locked --release --target $(TARGET) --target-dir target/secure \
-p sphincs-tz-secure --no-default-features \
--features se050-factory-reset,dev-testkey,ui-lcd,stm32u585,usb,debug-log
@echo "==> Flashing reset firmware..."
probe-rs download --chip STM32U585AIIx $(SECURE_ELF)