oxicrypt-module 0.23.2

Module structure targeting FIPS 140-3 Level 1: state machine, self-test runner, approved-mode indicator
Documentation
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
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
2130
2131
2132
2133
2134
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
2145
2146
2147
2148
2149
2150
2151
2152
2153
2154
2155
2156
2157
2158
2159
2160
2161
2162
2163
2164
2165
2166
2167
2168
2169
2170
2171
2172
2173
2174
2175
2176
2177
2178
2179
2180
2181
2182
2183
2184
2185
2186
2187
2188
2189
2190
2191
2192
2193
2194
2195
2196
2197
2198
2199
2200
2201
2202
2203
2204
2205
2206
2207
2208
2209
2210
2211
2212
2213
2214
2215
2216
2217
2218
2219
2220
2221
2222
2223
2224
2225
2226
2227
2228
2229
2230
2231
2232
2233
2234
2235
2236
2237
2238
2239
2240
2241
2242
2243
2244
2245
2246
2247
2248
2249
2250
2251
2252
2253
2254
2255
2256
2257
2258
2259
2260
2261
2262
2263
2264
2265
2266
2267
2268
2269
2270
2271
2272
2273
2274
2275
2276
2277
2278
2279
2280
2281
2282
2283
2284
2285
2286
2287
2288
2289
2290
2291
2292
2293
2294
2295
2296
2297
2298
2299
2300
2301
2302
2303
2304
2305
2306
2307
2308
2309
2310
2311
2312
2313
2314
2315
2316
2317
2318
2319
2320
2321
2322
2323
2324
2325
2326
2327
2328
2329
2330
2331
2332
2333
2334
2335
2336
2337
2338
2339
2340
2341
2342
2343
2344
2345
2346
2347
2348
2349
2350
2351
2352
2353
2354
2355
2356
2357
2358
2359
2360
2361
2362
2363
2364
2365
2366
2367
2368
2369
2370
2371
2372
2373
2374
2375
2376
2377
2378
2379
2380
2381
2382
2383
2384
2385
2386
2387
2388
2389
2390
2391
2392
2393
2394
2395
2396
2397
2398
2399
2400
2401
2402
2403
2404
2405
2406
2407
2408
2409
2410
2411
2412
2413
2414
2415
2416
2417
2418
2419
2420
2421
2422
2423
2424
2425
2426
2427
2428
2429
2430
2431
//! FIPS 140-3 Level 1 module boundary.
//!
//! This crate defines the **cryptographic module boundary** per
//! FIPS 140-3 Section 7.2 and the service-gating plumbing every
//! other crate in the workspace depends on. No algorithm is
//! permitted to produce output until the power-up self-tests have
//! run and the module has entered the `Operational` state; on any
//! self-test failure the module latches into a terminal `Error`
//! state and rejects every subsequent call for the remainder of
//! the process lifetime.
//!
//! # State machine (FIPS 140-3 §7.2)
//!
//! ```text
//!    ┌──────────┐  initialize() ┌───────────┐  all KATs pass  ┌──────────────┐
//!    │ PowerOff │──────────────▶│ SelfTest  │────────────────▶│ Operational  │
//!    └──────────┘               └───────────┘                 └──────────────┘
//!                                     │                              │
//!                                 KAT failure                conditional self-test
//!                                     │                            failure
//!                                     ▼                              │
//!                                ┌─────────┐◀───────────────────────┘
//!                                │  Error  │
//!                                └─────────┘
//! ```
//!
//! # What this crate ships
//!
//! - [`State`] and the transition machine (`PowerOff →
//!   SelfTest → Operational | Error`).
//! - [`initialize_with_tests`] — the canonical one-shot entry
//!   point that the top-level caller uses to run every approved
//!   algorithm's power-up KAT before opening the service
//!   interface. [`initialize`] is a thin wrapper for the empty
//!   registry case (used only by this crate's own unit tests).
//! - [`require_operational`] — the gate that every approved
//!   service in every other crate calls on entry. Returns
//!   [`Error::NotOperational`] (with the observed state) on any
//!   pre-`Operational` or post-`Error` call.
//! - [`enter_error_state`] — irreversible transition into the
//!   terminal error state, called by conditional self-tests
//!   (pairwise consistency, DRBG health) when they detect a
//!   failure.
//! - [`KatEntry`] and the [`SelfTest`] trait — the registry
//!   shape that algorithm crates use to expose their power-up
//!   KATs.
//! - [`AlgorithmProfile`] — runtime selection of an algorithm
//!   restriction policy (Unrestricted, CNSA 2.0, CNSA 1.0).
//!   Set once at initialization via
//!   [`initialize_with_profile`].
//! - [`Service`] — per-algorithm-and-parameter enumeration of
//!   every approved service in the module. Used with
//!   [`require_allowed`] to enforce profile restrictions.
//!
//! The test registry is **not** assembled by linker-section
//! tricks: callers pass an explicit `&[KatEntry]` slice. That
//! means the full set of power-up tests is visible in source at
//! every call site, which makes it straightforward to audit the
//! module's power-up inventory against the Security Policy.
//!
//! # Algorithm profiles
//!
//! A single binary serves general FIPS 140-3 consumers
//! and CNSA-restricted deployments via a runtime
//! [`AlgorithmProfile`] selection. The operator passes the
//! desired profile to [`initialize_with_profile`]; subsequent
//! calls to [`require_allowed`] enforce it. Services not
//! permitted by the active profile return
//! [`Error::AlgorithmRestricted`].
//!
//! Three profiles are defined:
//!
//! - **Unrestricted** — all FIPS-approved algorithms are
//!   available. This is the default, matching the behavior of
//!   [`initialize_with_tests`].
//! - **CNSA 2.0** (CNSSP 15) — only quantum-resistant
//!   algorithms: AES-256, SHA-384/512, ML-KEM-1024, ML-DSA-87,
//!   LMS and XMSS for software and firmware signing, plus
//!   SHA3-384/512 and 256-bit SP 800-185 variants. Every
//!   approved SP 800-208 LMS parameter set is permitted;
//!   CNSSP-15 Annex B places no subset restriction on LMS.
//!   No SLH-DSA parameter set is permitted — CNSSP-15 lists
//!   none.
//! - **CNSA 1.0** — classical algorithms for the transition
//!   period: AES-256, SHA-384, ECDSA/ECDH P-384, RSA >= 3072,
//!   DH >= 3072.
//!
//! Profiles nest: CNSA 2.0 is the most restrictive, CNSA 1.0
//! is intermediate, Unrestricted allows everything. KATs still
//! run all algorithms regardless of profile — the profile
//! only restricts post-initialization service access.
//!
//! # Sensitive security parameters (SSPs)
//!
//! This crate holds **no SSPs** of its own. It only gates
//! access to other crates that do. Secret material lives in the
//! algorithm crates that own it (e.g. `oxicrypt-rsa`'s
//! `RsaPrivateKey2048`, `oxicrypt-drbg`'s DRBG states). The error
//! latch here does not perform SSP zeroization on its own — each
//! owning crate is responsible for ensuring its SSPs are
//! dropped when the process restarts following a transition into
//! the `Error` state.
//!
//! # FIPS 140-3 / SP 800-140B mapping
//!
//! | SP 800-140B / IG clause | Implementation |
//! |-------------------------|----------------|
//! | §7.10 power-up self-tests | [`initialize_with_tests`] runs every registered [`KatEntry`] sequentially; the first failure latches [`State::Error`]. |
//! | §7.10 conditional self-tests | Algorithm crates call [`enter_error_state`] on detecting a pairwise-consistency or DRBG-health failure. |
//! | IG 9.5.A approved-mode indicator | [`is_operational`] / [`state`] — queryable at runtime. |
//! | IG 10.3.A software integrity | Delegated to `oxicrypt-integrity` (separate crate), wired in as the first KAT by the top-level caller. |
//!
//! # Thread safety
//!
//! State is stored in a single `AtomicU8` and is safe to read
//! from any thread. [`initialize_with_tests`] uses a
//! compare-and-swap to guarantee the self-test phase runs
//! exactly once per process lifetime even under concurrent
//! first-calls; racing losers spin on the `SelfTest` state byte
//! so they do not observe the transient phase and then receive
//! [`Error::AlreadyInitialized`].

#![no_std]
#![forbid(unsafe_code)]

use core::fmt;
use core::sync::atomic::{AtomicU8, Ordering};

/// Module lifecycle state per FIPS 140-3 Section 7.2.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum State {
    /// Module has not yet been initialized. No services are available.
    PowerOff = 0,
    /// Power-up self-tests are currently executing. No services are
    /// available. This state is transient.
    SelfTest = 1,
    /// All power-up self-tests have passed. Approved services are
    /// available.
    Operational = 2,
    /// A self-test has failed (either a power-up KAT or a conditional
    /// test). This state is **terminal** within a process: no further
    /// services are offered and all cached secret material must be
    /// treated as zeroized. The only recovery is to restart the
    /// containing process.
    Error = 3,
}

impl State {
    const fn from_u8(raw: u8) -> Self {
        match raw {
            0 => Self::PowerOff,
            1 => Self::SelfTest,
            2 => Self::Operational,
            // Any unknown discriminant collapses to `Error` so that a
            // corrupted state byte can never be interpreted as
            // `Operational`. This is defence-in-depth: `AtomicU8`
            // should only ever hold values written through this
            // module.
            _ => Self::Error,
        }
    }
}

impl fmt::Display for State {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let s = match self {
            Self::PowerOff => "PowerOff",
            Self::SelfTest => "SelfTest",
            Self::Operational => "Operational",
            Self::Error => "Error",
        };
        f.write_str(s)
    }
}

/// Global module state. Written only by [`initialize`] and
/// [`enter_error_state`]; read by [`state`] and [`require_operational`].
static STATE: AtomicU8 = AtomicU8::new(State::PowerOff as u8);

/// Returns the current module state.
pub fn state() -> State {
    State::from_u8(STATE.load(Ordering::Acquire))
}

/// Returns `true` iff the module has passed its power-up self-tests and is
/// currently in the `Operational` state.
pub fn is_operational() -> bool {
    state() == State::Operational
}

/// Guard used at the entry point of every approved service. Returns
/// `Ok(())` only when the module is `Operational`; otherwise returns a
/// [`Error::NotOperational`] describing the current state so callers
/// cannot accidentally ignore it.
pub fn require_operational() -> Result<(), Error> {
    let current = state();
    if current == State::Operational {
        Ok(())
    } else {
        Err(Error::NotOperational { current })
    }
}

/// Errors surfaced by the module boundary.
///
/// Every variant is constructed with a `&'static str` description so
/// that formatting is allocation-free; this lets the module report
/// failures even from `no_std` contexts and from within the self-test
/// runner itself.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Error {
    /// A service was invoked while the module was not in the
    /// `Operational` state.
    NotOperational {
        /// The state the module was actually in when the call arrived.
        current: State,
    },
    /// A power-up self-test failed. The module is now in the `Error`
    /// state.
    SelfTestFailed {
        /// Name of the failing test, supplied by its
        /// [`SelfTest`] implementation.
        test: &'static str,
    },
    /// A conditional self-test (e.g. DRBG CRNGT, pairwise consistency)
    /// failed during operation. The module is now in the `Error` state.
    ConditionalTestFailed {
        /// Short description of the failing check.
        reason: &'static str,
    },
    /// [`initialize`] was called while the module was already past
    /// `PowerOff`. This is not itself a FIPS violation — it just means
    /// a second caller raced with the first. The first call's outcome
    /// is authoritative; the race loser should read [`state`] to find
    /// out.
    AlreadyInitialized,
    /// A cryptographic service was called with input bytes that do not
    /// encode a valid domain element (for example, a scalar that is
    /// zero or not less than the group order, or a point that is not
    /// a canonical encoding on the curve). This is distinct from a
    /// self-test failure: the module is still operational; the caller
    /// supplied out-of-range bytes. Algorithm primitives should return
    /// this rather than silently substituting a default.
    InvalidInput,
    /// A service was invoked that is not permitted under the active
    /// [`AlgorithmProfile`]. The module is still operational — the
    /// algorithm exists but is restricted by policy. Switch to a
    /// less restrictive profile (which requires a process restart)
    /// or use an alternative algorithm that is permitted.
    AlgorithmRestricted {
        /// The specific service that was blocked.
        service: Service,
    },
    /// A service was invoked for an algorithm that exists in the
    /// [`Service`] enum but whose implementation has not yet been
    /// completed. Stub crates return this. The algorithm is
    /// recognized by the profile system and its gate will pass if
    /// the profile permits it, but the actual cryptographic
    /// operation is not yet available.
    NotImplemented,
}

impl fmt::Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::NotOperational { current } => {
                write!(f, "FIPS module not operational (state = {current})")
            }
            Self::SelfTestFailed { test } => {
                write!(f, "FIPS power-up self-test failed: {test}")
            }
            Self::ConditionalTestFailed { reason } => {
                write!(f, "FIPS conditional self-test failed: {reason}")
            }
            Self::AlreadyInitialized => f.write_str("FIPS module already initialized"),
            Self::InvalidInput => f.write_str("invalid input to FIPS service"),
            Self::AlgorithmRestricted { service } => {
                write!(
                    f,
                    "algorithm {service} is not permitted under the {} profile",
                    active_profile()
                )
            }
            Self::NotImplemented => f.write_str("algorithm not yet implemented"),
        }
    }
}

/// Marker returned by a failing [`SelfTest::run`].
///
/// Self-tests are allowed to describe *what* failed, but they are not
/// allowed to exfiltrate the intermediate values that triggered the
/// failure — including them in an error payload would potentially leak
/// information about internal state. The runner treats the mere
/// presence of this value as "test failed" and converts it into
/// [`Error::SelfTestFailed`] using the test's own [`SelfTest::NAME`].
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SelfTestFailure;

impl fmt::Display for SelfTestFailure {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str("self-test failure")
    }
}

/// A single power-up self-test.
///
/// Each approved algorithm crate will implement this trait for at least
/// one test vector, per SP 800-140B. The [`initialize`] routine runs
/// every registered test sequentially before the module can leave the
/// `SelfTest` state.
///
/// Implementations **must** be deterministic, **must not** allocate,
/// and **must not** read from any input the caller controls: the
/// whole point of a KAT is that its inputs and expected outputs are
/// baked into the module binary.
pub trait SelfTest {
    /// Stable, human-readable identifier for this test. Appears in
    /// [`Error::SelfTestFailed`] and in audit logs.
    const NAME: &'static str;

    /// Runs the test. Returns `Ok(())` on success, `Err(SelfTestFailure)`
    /// on any mismatch. The runner converts the `Err` into
    /// [`Error::SelfTestFailed`] using [`Self::NAME`].
    fn run() -> Result<(), SelfTestFailure>;
}

/// A power-up KAT registered with the module.
///
/// Algorithm crates expose one or more `KatEntry` values that the
/// top-level caller passes into [`initialize_with_tests`] at startup.
/// The runner executes every entry sequentially before the module
/// leaves the `SelfTest` state; a single failure latches the module
/// into `Error` for the remainder of the process lifetime, per
/// FIPS 140-3 Section 7.10 and SP 800-140B.
///
/// The function pointer signature takes no inputs on purpose: a KAT
/// must run against compile-time-baked vectors only, never against
/// caller-supplied data.
#[derive(Debug, Clone, Copy)]
pub struct KatEntry {
    /// Stable identifier for the test. Surfaces in
    /// [`Error::SelfTestFailed`] and audit logs. Must match the
    /// [`SelfTest::NAME`] used for the underlying vector.
    pub name: &'static str,
    /// The test routine itself.
    pub run: fn() -> Result<(), SelfTestFailure>,
}

/// Initializes the module with an empty KAT registry.
///
/// Convenience wrapper around [`initialize_with_tests`] for contexts
/// that want to exercise only the state machine (notably
/// `fips-module`'s own unit tests). Production callers of this crate
/// must use [`initialize_with_tests`] with the full approved-service
/// KAT set — a FIPS module that ships no self-tests is not compliant
/// with SP 800-140B regardless of whether the state machine runs.
pub fn initialize() -> Result<(), Error> {
    initialize_with_tests(&[])
}

/// Initializes the module and runs every supplied power-up KAT.
///
/// This is the canonical entry point. It transitions
/// `PowerOff -> SelfTest`, executes every entry in `tests` in order,
/// and then transitions to `Operational` on success or to the
/// terminal `Error` state on the first failure. The returned
/// [`Error::SelfTestFailed`] carries the name of the failing test.
///
/// Concurrent calls are safe: exactly one caller wins the
/// `PowerOff -> SelfTest` CAS; losers receive
/// [`Error::AlreadyInitialized`] and should consult [`state`] to
/// discover whether the winning call succeeded.
///
/// Note: the slice is passed **by reference**, not built up by magic.
/// This repository deliberately avoids linker-section registration
/// tricks: the full set of power-up tests is visible in source at
/// every call site, which makes it straightforward to audit the
/// module's test inventory against the Security Policy.
pub fn initialize_with_tests(tests: &[KatEntry]) -> Result<(), Error> {
    // Try to claim the SelfTest phase. Only the first caller in the
    // process lifetime succeeds.
    let cas = STATE.compare_exchange(
        State::PowerOff as u8,
        State::SelfTest as u8,
        Ordering::AcqRel,
        Ordering::Acquire,
    );
    if cas.is_err() {
        // Another thread is running (or has run) the self-tests. If
        // it is still inside `SelfTest`, spin until it publishes a
        // terminal state so racing callers don't observe the
        // transient phase and trip `require_operational`.
        while STATE.load(Ordering::Acquire) == State::SelfTest as u8 {
            core::hint::spin_loop();
        }
        return Err(Error::AlreadyInitialized);
    }

    for entry in tests {
        let result = (entry.run)();
        if result.is_err() {
            // Latch the module into Error before returning. Any
            // subsequent service call will be rejected by
            // require_operational().
            STATE.store(State::Error as u8, Ordering::Release);
            return Err(Error::SelfTestFailed { test: entry.name });
        }
    }

    STATE.store(State::Operational as u8, Ordering::Release);
    Ok(())
}

/// Forces the module into the terminal `Error` state.
///
/// Called by conditional self-tests (DRBG CRNGT, pairwise consistency
/// on keygen, etc.) when they detect a failure. Once invoked the
/// module will reject every subsequent [`require_operational`] call
/// for the remainder of the process lifetime.
///
/// This function is intentionally irreversible: FIPS 140-3 requires
/// that a module which has entered an error condition stay there
/// until it is re-initialized, and our "re-initialization" is a
/// process restart.
pub fn enter_error_state(_reason: &'static str) {
    STATE.store(State::Error as u8, Ordering::Release);
}

// =========================================================================
// Algorithm profile gating (CNSA 2.0 / CNSA 1.0)
// =========================================================================

/// Algorithm restriction profile selected at module initialization.
///
/// A single binary can serve general FIPS 140-3 consumers
/// (`Unrestricted`) and CNSA-restricted deployments (`Cnsa2`,
/// `Cnsa1`) by choosing the appropriate profile at init time.
///
/// The profile is immutable once set: it is stored alongside the
/// module `State` before the first KAT runs and cannot be changed
/// without restarting the process (which resets the state machine).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum AlgorithmProfile {
    /// All FIPS-approved algorithms are available. This is the
    /// default and preserves backward compatibility with callers
    /// that use [`initialize_with_tests`].
    Unrestricted = 0,
    /// CNSA 2.0 (CNSSP 15): quantum-resistant algorithms only.
    /// AES-256, SHA-384/512, ML-KEM-1024, ML-DSA-87, LMS, XMSS.
    /// SHA3-384/512 and 256-bit SP 800-185 variants are also
    /// allowed. All other algorithms return
    /// [`Error::AlgorithmRestricted`].
    Cnsa2 = 1,
    /// CNSA 1.0: classical algorithms for the transition period.
    /// AES-256, SHA-384, ECDSA/ECDH P-384, RSA >= 3072, DH >= 3072.
    Cnsa1 = 2,
}

impl AlgorithmProfile {
    const fn from_u8(raw: u8) -> Self {
        match raw {
            0 => Self::Unrestricted,
            2 => Self::Cnsa1,
            // Unknown discriminant (including 1 = Cnsa2) defaults to
            // the most restrictive profile as defence-in-depth.
            _ => Self::Cnsa2,
        }
    }
}

impl fmt::Display for AlgorithmProfile {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let s = match self {
            Self::Unrestricted => "Unrestricted",
            Self::Cnsa2 => "CNSA 2.0",
            Self::Cnsa1 => "CNSA 1.0",
        };
        f.write_str(s)
    }
}

/// Global algorithm profile. Written once by [`initialize_with_profile`];
/// read by [`require_allowed`] and [`active_profile`].
static PROFILE: AtomicU8 = AtomicU8::new(AlgorithmProfile::Unrestricted as u8);

/// Returns the active algorithm profile.
pub fn active_profile() -> AlgorithmProfile {
    AlgorithmProfile::from_u8(PROFILE.load(Ordering::Acquire))
}

/// Initializes the module with a specific algorithm profile and runs
/// every supplied power-up KAT.
///
/// This is the profile-aware entry point. It stores the selected
/// profile before running KATs, then transitions to `Operational` on
/// success. KATs always run all algorithms regardless of the profile —
/// the profile only restricts post-initialization service access via
/// [`require_allowed`].
///
/// See [`initialize_with_tests`] for the backward-compatible wrapper
/// that uses [`AlgorithmProfile::Unrestricted`].
pub fn initialize_with_profile(tests: &[KatEntry], profile: AlgorithmProfile) -> Result<(), Error> {
    // Store the profile before running KATs. This is safe even if
    // initialization fails: a failed init latches State::Error, so
    // no service call can reach require_allowed() anyway.
    PROFILE.store(profile as u8, Ordering::Release);
    initialize_with_tests(tests)
}

/// Enumeration of every approved service in the module, at the
/// algorithm-and-parameter level.
///
/// Each variant represents a specific algorithm instantiation (e.g.
/// `Aes128Ecb` not just `Aes`). This granularity is necessary
/// because CNSA restrictions operate at the key-size and hash-size
/// level.
///
/// Algorithm crates pass the appropriate `Service` variant to
/// [`require_allowed`] at their public entry points.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u16)]
#[allow(missing_docs)]
pub enum Service {
    // ----- oxicrypt-sha: FIPS 180-4 / FIPS 202 -----
    Sha1 = 0,
    Sha224 = 1,
    Sha256 = 2,
    Sha384 = 3,
    Sha512 = 4,
    Sha512_224 = 5,
    Sha512_256 = 6,

    Sha3_224 = 10,
    Sha3_256 = 11,
    Sha3_384 = 12,
    Sha3_512 = 13,

    // ----- oxicrypt-xof: FIPS 202 / SP 800-185 -----
    Shake128 = 20,
    Shake256 = 21,
    CShake128 = 22,
    CShake256 = 23,
    Kmac128 = 24,
    Kmac256 = 25,
    KmacXof128 = 26,
    KmacXof256 = 27,
    TupleHash128 = 28,
    TupleHash256 = 29,
    TupleHashXof128 = 30,
    TupleHashXof256 = 31,
    ParallelHash128 = 32,
    ParallelHash256 = 33,
    ParallelHashXof128 = 34,
    ParallelHashXof256 = 35,

    // ----- oxicrypt-hmac: FIPS 198-1 -----
    HmacSha1 = 40,
    HmacSha224 = 41,
    HmacSha256 = 42,
    HmacSha384 = 43,
    HmacSha512 = 44,
    HmacSha512_224 = 45,
    HmacSha512_256 = 46,
    HmacSha3_224 = 47,
    HmacSha3_256 = 48,
    HmacSha3_384 = 49,
    HmacSha3_512 = 50,

    // ----- oxicrypt-cmac: SP 800-38B -----
    CmacAes128 = 60,
    CmacAes192 = 61,
    CmacAes256 = 62,

    // ----- oxicrypt-aes: FIPS 197 / SP 800-38A/D/C/F/Fp -----
    Aes128Ecb = 70,
    Aes128Cbc = 71,
    Aes128Ctr = 72,
    Aes128Gcm = 73,
    Aes128Ccm = 74,
    Aes128Kw = 75,
    Aes128Kwp = 76,
    Aes192Ecb = 80,
    Aes192Cbc = 81,
    Aes192Ctr = 82,
    Aes192Gcm = 83,
    Aes192Ccm = 84,
    Aes192Kw = 85,
    Aes192Kwp = 86,
    Aes256Ecb = 90,
    Aes256Cbc = 91,
    Aes256Ctr = 92,
    Aes256Gcm = 93,
    Aes256Ccm = 94,
    Aes256Kw = 95,
    Aes256Kwp = 96,

    // Key-construction gates — checked when building an AES key
    // object before the caller selects a mode.  All per-mode
    // variants of the same key size share identical profile
    // permissions, so a single gate at key construction is
    // sufficient.
    /// AES-128 key construction (gates all AES-128 mode services).
    Aes128 = 97,
    /// AES-192 key construction (gates all AES-192 mode services).
    Aes192 = 98,
    /// AES-256 key construction (gates all AES-256 mode services).
    Aes256 = 99,

    // ----- oxicrypt-drbg: SP 800-90A -----
    CtrDrbgAes128 = 100,
    CtrDrbgAes192 = 101,
    CtrDrbgAes256 = 102,
    HashDrbgSha256 = 103,
    HashDrbgSha384 = 104,
    HashDrbgSha512 = 105,
    HmacDrbgSha256 = 106,
    HmacDrbgSha384 = 107,
    HmacDrbgSha512 = 108,

    // ----- oxicrypt-kdf: SP 800-56C / SP 800-108 / SP 800-132 -----
    HkdfSha1 = 120,
    HkdfSha256 = 121,
    HkdfSha384 = 122,
    HkdfSha512 = 123,
    KbkdfHmacSha256 = 130,
    KbkdfHmacSha384 = 131,
    KbkdfHmacSha512 = 132,
    KbkdfCmacAes128 = 133,
    KbkdfCmacAes192 = 134,
    KbkdfCmacAes256 = 135,
    Pbkdf2HmacSha1 = 140,
    Pbkdf2HmacSha224 = 141,
    Pbkdf2HmacSha256 = 142,
    Pbkdf2HmacSha384 = 143,
    Pbkdf2HmacSha512 = 144,

    // ----- oxicrypt-rsa: FIPS 186-5 / SP 800-56Br2 -----
    RsaKeygen2048 = 150,
    RsaPkcs1v15Sign2048 = 151,
    RsaPssSign2048 = 152,
    RsaOaep2048 = 153,
    RsaPkcs1v15Verify2048 = 154,
    RsaPssVerify2048 = 155,
    RsaKeygen3072 = 160,
    RsaPkcs1v15Sign3072 = 161,
    RsaPssSign3072 = 162,
    RsaOaep3072 = 163,
    RsaPkcs1v15Verify3072 = 164,
    RsaPssVerify3072 = 165,
    RsaKeygen4096 = 170,
    RsaPkcs1v15Sign4096 = 171,
    RsaPssSign4096 = 172,
    RsaOaep4096 = 173,
    RsaPkcs1v15Verify4096 = 174,
    RsaPssVerify4096 = 175,

    // ----- oxicrypt-ecdsa: FIPS 186-5 -----
    EcdsaP256Sign = 200,
    EcdsaP256Verify = 201,
    EcdsaP256Keygen = 202,
    EcdsaP384Sign = 210,
    EcdsaP384Verify = 211,
    EcdsaP384Keygen = 212,

    // ----- oxicrypt-ecdh: SP 800-56Ar3 -----
    EcdhP256 = 220,
    EcdhP384 = 221,

    // ----- oxicrypt-eddsa: FIPS 186-5 / RFC 8032 -----
    Ed25519Sign = 230,
    Ed25519Verify = 231,
    Ed25519Keygen = 232,

    // ----- oxicrypt-tls-kdf: SP 800-135r1 + RFC 8446 §7.1 -----
    Tls12Kdf = 240,
    Tls13Kdf = 241,

    // ----- oxicrypt-ml-kem: FIPS 203 -----
    MlKem1024Encaps = 300,
    MlKem1024Decaps = 301,
    MlKem1024Keygen = 302,
    MlKem512Encaps = 303,
    MlKem512Decaps = 304,
    MlKem512Keygen = 305,
    MlKem768Encaps = 306,
    MlKem768Decaps = 307,
    MlKem768Keygen = 308,

    // ----- oxicrypt-ml-dsa: FIPS 204 -----
    MlDsa87Sign = 310,
    MlDsa87Verify = 311,
    MlDsa87Keygen = 312,
    MlDsa44Sign = 313,
    MlDsa44Verify = 314,
    MlDsa44Keygen = 315,
    MlDsa65Sign = 316,
    MlDsa65Verify = 317,
    MlDsa65Keygen = 318,

    // ----- oxicrypt-slh-dsa: FIPS 205 (12 parameter sets × 3 ops = 36 variants) -----
    // SHA-2 256s claims 320-322 for historical reasons, then the rest of the
    // SHA-2 family (128s/f, 192s/f, 256f), then the SHAKE family. No SLH-DSA
    // parameter set is permitted under either CNSA profile.
    SlhDsaSha2256sKeygen = 320,
    SlhDsaSha2256sSign = 321,
    SlhDsaSha2256sVerify = 322,
    SlhDsaSha2128sKeygen = 323,
    SlhDsaSha2128sSign = 324,
    SlhDsaSha2128sVerify = 325,
    SlhDsaSha2128fKeygen = 326,
    SlhDsaSha2128fSign = 327,
    SlhDsaSha2128fVerify = 328,
    SlhDsaSha2192sKeygen = 329,
    SlhDsaSha2192sSign = 330,
    SlhDsaSha2192sVerify = 331,
    SlhDsaSha2192fKeygen = 332,
    SlhDsaSha2192fSign = 333,
    SlhDsaSha2192fVerify = 334,
    SlhDsaSha2256fKeygen = 335,
    SlhDsaSha2256fSign = 336,
    SlhDsaSha2256fVerify = 337,
    SlhDsaShake128sKeygen = 338,
    SlhDsaShake128sSign = 339,
    SlhDsaShake128sVerify = 340,
    SlhDsaShake128fKeygen = 341,
    SlhDsaShake128fSign = 342,
    SlhDsaShake128fVerify = 343,
    SlhDsaShake192sKeygen = 344,
    SlhDsaShake192sSign = 345,
    SlhDsaShake192sVerify = 346,
    SlhDsaShake192fKeygen = 347,
    SlhDsaShake192fSign = 348,
    SlhDsaShake192fVerify = 349,
    SlhDsaShake256sKeygen = 350,
    SlhDsaShake256sSign = 351,
    SlhDsaShake256sVerify = 352,
    SlhDsaShake256fKeygen = 353,
    SlhDsaShake256fSign = 354,
    SlhDsaShake256fVerify = 355,

    // ----- oxicrypt-lms: SP 800-208 (RFC 8554 / RFC 8708) -----
    // 160 per-pair entries (80 pairs × Sign + Verify), discriminants 500-659.
    // Layout: 500-539 SHA-256/M=32, 540-579 SHA-256/M=24,
    // 580-619 SHAKE/M=32, 620-659 SHAKE/M=24.
    // Within each family: (H ascending, W ascending), Sign/Verify alternating.
    // The block is contiguous and holds nothing but LMS, which is what lets
    // `is_lms_service` range-test it; `lms_discriminant_block_is_contiguous_and_pure`
    // pins that. All 80 pairs are permitted under every profile.

    // SHA-256 / N=32 family (RFC 8554 §A.1+§A.2)
    LmsSha256M32H5W1Sign = 500,
    LmsSha256M32H5W1Verify = 501,
    LmsSha256M32H5W2Sign = 502,
    LmsSha256M32H5W2Verify = 503,
    LmsSha256M32H5W4Sign = 504,
    LmsSha256M32H5W4Verify = 505,
    LmsSha256M32H5W8Sign = 506,
    LmsSha256M32H5W8Verify = 507,
    LmsSha256M32H10W1Sign = 508,
    LmsSha256M32H10W1Verify = 509,
    LmsSha256M32H10W2Sign = 510,
    LmsSha256M32H10W2Verify = 511,
    LmsSha256M32H10W4Sign = 512,
    LmsSha256M32H10W4Verify = 513,
    LmsSha256M32H10W8Sign = 514,
    LmsSha256M32H10W8Verify = 515,
    LmsSha256M32H15W1Sign = 516,
    LmsSha256M32H15W1Verify = 517,
    LmsSha256M32H15W2Sign = 518,
    LmsSha256M32H15W2Verify = 519,
    LmsSha256M32H15W4Sign = 520,
    LmsSha256M32H15W4Verify = 521,
    LmsSha256M32H15W8Sign = 522,
    LmsSha256M32H15W8Verify = 523,
    LmsSha256M32H20W1Sign = 524,
    LmsSha256M32H20W1Verify = 525,
    LmsSha256M32H20W2Sign = 526,
    LmsSha256M32H20W2Verify = 527,
    LmsSha256M32H20W4Sign = 528,
    LmsSha256M32H20W4Verify = 529,
    LmsSha256M32H20W8Sign = 530,
    LmsSha256M32H20W8Verify = 531,
    LmsSha256M32H25W1Sign = 532,
    LmsSha256M32H25W1Verify = 533,
    LmsSha256M32H25W2Sign = 534,
    LmsSha256M32H25W2Verify = 535,
    LmsSha256M32H25W4Sign = 536,
    LmsSha256M32H25W4Verify = 537,
    LmsSha256M32H25W8Sign = 538,
    LmsSha256M32H25W8Verify = 539,

    // SHA-256 / N=24 family (RFC 8708 §4.1)
    LmsSha256M24H5W1Sign = 540,
    LmsSha256M24H5W1Verify = 541,
    LmsSha256M24H5W2Sign = 542,
    LmsSha256M24H5W2Verify = 543,
    LmsSha256M24H5W4Sign = 544,
    LmsSha256M24H5W4Verify = 545,
    LmsSha256M24H5W8Sign = 546,
    LmsSha256M24H5W8Verify = 547,
    LmsSha256M24H10W1Sign = 548,
    LmsSha256M24H10W1Verify = 549,
    LmsSha256M24H10W2Sign = 550,
    LmsSha256M24H10W2Verify = 551,
    LmsSha256M24H10W4Sign = 552,
    LmsSha256M24H10W4Verify = 553,
    LmsSha256M24H10W8Sign = 554,
    LmsSha256M24H10W8Verify = 555,
    LmsSha256M24H15W1Sign = 556,
    LmsSha256M24H15W1Verify = 557,
    LmsSha256M24H15W2Sign = 558,
    LmsSha256M24H15W2Verify = 559,
    LmsSha256M24H15W4Sign = 560,
    LmsSha256M24H15W4Verify = 561,
    LmsSha256M24H15W8Sign = 562,
    LmsSha256M24H15W8Verify = 563,
    LmsSha256M24H20W1Sign = 564,
    LmsSha256M24H20W1Verify = 565,
    LmsSha256M24H20W2Sign = 566,
    LmsSha256M24H20W2Verify = 567,
    LmsSha256M24H20W4Sign = 568,
    LmsSha256M24H20W4Verify = 569,
    LmsSha256M24H20W8Sign = 570,
    LmsSha256M24H20W8Verify = 571,
    LmsSha256M24H25W1Sign = 572,
    LmsSha256M24H25W1Verify = 573,
    LmsSha256M24H25W2Sign = 574,
    LmsSha256M24H25W2Verify = 575,
    LmsSha256M24H25W4Sign = 576,
    LmsSha256M24H25W4Verify = 577,
    LmsSha256M24H25W8Sign = 578,
    LmsSha256M24H25W8Verify = 579,

    // SHAKE-256 / N=32 family (RFC 8708 §3.1)
    LmsShakeM32H5W1Sign = 580,
    LmsShakeM32H5W1Verify = 581,
    LmsShakeM32H5W2Sign = 582,
    LmsShakeM32H5W2Verify = 583,
    LmsShakeM32H5W4Sign = 584,
    LmsShakeM32H5W4Verify = 585,
    LmsShakeM32H5W8Sign = 586,
    LmsShakeM32H5W8Verify = 587,
    LmsShakeM32H10W1Sign = 588,
    LmsShakeM32H10W1Verify = 589,
    LmsShakeM32H10W2Sign = 590,
    LmsShakeM32H10W2Verify = 591,
    LmsShakeM32H10W4Sign = 592,
    LmsShakeM32H10W4Verify = 593,
    LmsShakeM32H10W8Sign = 594,
    LmsShakeM32H10W8Verify = 595,
    LmsShakeM32H15W1Sign = 596,
    LmsShakeM32H15W1Verify = 597,
    LmsShakeM32H15W2Sign = 598,
    LmsShakeM32H15W2Verify = 599,
    LmsShakeM32H15W4Sign = 600,
    LmsShakeM32H15W4Verify = 601,
    LmsShakeM32H15W8Sign = 602,
    LmsShakeM32H15W8Verify = 603,
    LmsShakeM32H20W1Sign = 604,
    LmsShakeM32H20W1Verify = 605,
    LmsShakeM32H20W2Sign = 606,
    LmsShakeM32H20W2Verify = 607,
    LmsShakeM32H20W4Sign = 608,
    LmsShakeM32H20W4Verify = 609,
    LmsShakeM32H20W8Sign = 610,
    LmsShakeM32H20W8Verify = 611,
    LmsShakeM32H25W1Sign = 612,
    LmsShakeM32H25W1Verify = 613,
    LmsShakeM32H25W2Sign = 614,
    LmsShakeM32H25W2Verify = 615,
    LmsShakeM32H25W4Sign = 616,
    LmsShakeM32H25W4Verify = 617,
    LmsShakeM32H25W8Sign = 618,
    LmsShakeM32H25W8Verify = 619,

    // SHAKE-256 / N=24 family (RFC 8708 §4.2)
    LmsShakeM24H5W1Sign = 620,
    LmsShakeM24H5W1Verify = 621,
    LmsShakeM24H5W2Sign = 622,
    LmsShakeM24H5W2Verify = 623,
    LmsShakeM24H5W4Sign = 624,
    LmsShakeM24H5W4Verify = 625,
    LmsShakeM24H5W8Sign = 626,
    LmsShakeM24H5W8Verify = 627,
    LmsShakeM24H10W1Sign = 628,
    LmsShakeM24H10W1Verify = 629,
    LmsShakeM24H10W2Sign = 630,
    LmsShakeM24H10W2Verify = 631,
    LmsShakeM24H10W4Sign = 632,
    LmsShakeM24H10W4Verify = 633,
    LmsShakeM24H10W8Sign = 634,
    LmsShakeM24H10W8Verify = 635,
    LmsShakeM24H15W1Sign = 636,
    LmsShakeM24H15W1Verify = 637,
    LmsShakeM24H15W2Sign = 638,
    LmsShakeM24H15W2Verify = 639,
    LmsShakeM24H15W4Sign = 640,
    LmsShakeM24H15W4Verify = 641,
    LmsShakeM24H15W8Sign = 642,
    LmsShakeM24H15W8Verify = 643,
    LmsShakeM24H20W1Sign = 644,
    LmsShakeM24H20W1Verify = 645,
    LmsShakeM24H20W2Sign = 646,
    LmsShakeM24H20W2Verify = 647,
    LmsShakeM24H20W4Sign = 648,
    LmsShakeM24H20W4Verify = 649,
    LmsShakeM24H20W8Sign = 650,
    LmsShakeM24H20W8Verify = 651,
    LmsShakeM24H25W1Sign = 652,
    LmsShakeM24H25W1Verify = 653,
    LmsShakeM24H25W2Sign = 654,
    LmsShakeM24H25W2Verify = 655,
    LmsShakeM24H25W4Sign = 656,
    LmsShakeM24H25W4Verify = 657,
    LmsShakeM24H25W8Sign = 658,
    LmsShakeM24H25W8Verify = 659,

    // ----- oxicrypt-xmss: SP 800-208 (stub) -----
    // Renumbered 340/341 → 370/371 in Batch 4 to make room for the SLH-DSA block.
    XmssSign = 370,
    XmssVerify = 371,

    // ----- oxicrypt-dh: RFC 3526 (stub) -----
    // Renumbered 350 → 380 in Batch 4 to make room for the SLH-DSA block.
    Dh3072 = 380,
}

impl fmt::Display for Service {
    // The match arm count is proportional to the number of approved
    // services in the module; there is no natural factoring that
    // would reduce it.
    #[allow(clippy::too_many_lines)]
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let name = match self {
            Self::Sha1 => "SHA-1",
            Self::Sha224 => "SHA-224",
            Self::Sha256 => "SHA-256",
            Self::Sha384 => "SHA-384",
            Self::Sha512 => "SHA-512",
            Self::Sha512_224 => "SHA-512/224",
            Self::Sha512_256 => "SHA-512/256",
            Self::Sha3_224 => "SHA3-224",
            Self::Sha3_256 => "SHA3-256",
            Self::Sha3_384 => "SHA3-384",
            Self::Sha3_512 => "SHA3-512",
            Self::Shake128 => "SHAKE128",
            Self::Shake256 => "SHAKE256",
            Self::CShake128 => "cSHAKE128",
            Self::CShake256 => "cSHAKE256",
            Self::Kmac128 => "KMAC128",
            Self::Kmac256 => "KMAC256",
            Self::KmacXof128 => "KMACXOF128",
            Self::KmacXof256 => "KMACXOF256",
            Self::TupleHash128 => "TupleHash128",
            Self::TupleHash256 => "TupleHash256",
            Self::TupleHashXof128 => "TupleHashXOF128",
            Self::TupleHashXof256 => "TupleHashXOF256",
            Self::ParallelHash128 => "ParallelHash128",
            Self::ParallelHash256 => "ParallelHash256",
            Self::ParallelHashXof128 => "ParallelHashXOF128",
            Self::ParallelHashXof256 => "ParallelHashXOF256",
            Self::HmacSha1 => "HMAC-SHA-1",
            Self::HmacSha224 => "HMAC-SHA-224",
            Self::HmacSha256 => "HMAC-SHA-256",
            Self::HmacSha384 => "HMAC-SHA-384",
            Self::HmacSha512 => "HMAC-SHA-512",
            Self::HmacSha512_224 => "HMAC-SHA-512/224",
            Self::HmacSha512_256 => "HMAC-SHA-512/256",
            Self::HmacSha3_224 => "HMAC-SHA3-224",
            Self::HmacSha3_256 => "HMAC-SHA3-256",
            Self::HmacSha3_384 => "HMAC-SHA3-384",
            Self::HmacSha3_512 => "HMAC-SHA3-512",
            Self::CmacAes128 => "CMAC-AES-128",
            Self::CmacAes192 => "CMAC-AES-192",
            Self::CmacAes256 => "CMAC-AES-256",
            Self::Aes128Ecb => "AES-128-ECB",
            Self::Aes128Cbc => "AES-128-CBC",
            Self::Aes128Ctr => "AES-128-CTR",
            Self::Aes128Gcm => "AES-128-GCM",
            Self::Aes128Ccm => "AES-128-CCM",
            Self::Aes128Kw => "AES-128-KW",
            Self::Aes128Kwp => "AES-128-KWP",
            Self::Aes192Ecb => "AES-192-ECB",
            Self::Aes192Cbc => "AES-192-CBC",
            Self::Aes192Ctr => "AES-192-CTR",
            Self::Aes192Gcm => "AES-192-GCM",
            Self::Aes192Ccm => "AES-192-CCM",
            Self::Aes192Kw => "AES-192-KW",
            Self::Aes192Kwp => "AES-192-KWP",
            Self::Aes256Ecb => "AES-256-ECB",
            Self::Aes256Cbc => "AES-256-CBC",
            Self::Aes256Ctr => "AES-256-CTR",
            Self::Aes256Gcm => "AES-256-GCM",
            Self::Aes256Ccm => "AES-256-CCM",
            Self::Aes256Kw => "AES-256-KW",
            Self::Aes256Kwp => "AES-256-KWP",
            Self::Aes128 => "AES-128",
            Self::Aes192 => "AES-192",
            Self::Aes256 => "AES-256",
            Self::CtrDrbgAes128 => "CTR_DRBG-AES-128",
            Self::CtrDrbgAes192 => "CTR_DRBG-AES-192",
            Self::CtrDrbgAes256 => "CTR_DRBG-AES-256",
            Self::HashDrbgSha256 => "Hash_DRBG-SHA-256",
            Self::HashDrbgSha384 => "Hash_DRBG-SHA-384",
            Self::HashDrbgSha512 => "Hash_DRBG-SHA-512",
            Self::HmacDrbgSha256 => "HMAC_DRBG-SHA-256",
            Self::HmacDrbgSha384 => "HMAC_DRBG-SHA-384",
            Self::HmacDrbgSha512 => "HMAC_DRBG-SHA-512",
            Self::HkdfSha1 => "HKDF-SHA-1",
            Self::HkdfSha256 => "HKDF-SHA-256",
            Self::HkdfSha384 => "HKDF-SHA-384",
            Self::HkdfSha512 => "HKDF-SHA-512",
            Self::KbkdfHmacSha256 => "KBKDF-HMAC-SHA-256",
            Self::KbkdfHmacSha384 => "KBKDF-HMAC-SHA-384",
            Self::KbkdfHmacSha512 => "KBKDF-HMAC-SHA-512",
            Self::KbkdfCmacAes128 => "KBKDF-CMAC-AES-128",
            Self::KbkdfCmacAes192 => "KBKDF-CMAC-AES-192",
            Self::KbkdfCmacAes256 => "KBKDF-CMAC-AES-256",
            Self::Pbkdf2HmacSha1 => "PBKDF2-HMAC-SHA-1",
            Self::Pbkdf2HmacSha224 => "PBKDF2-HMAC-SHA-224",
            Self::Pbkdf2HmacSha256 => "PBKDF2-HMAC-SHA-256",
            Self::Pbkdf2HmacSha384 => "PBKDF2-HMAC-SHA-384",
            Self::Pbkdf2HmacSha512 => "PBKDF2-HMAC-SHA-512",
            Self::RsaKeygen2048 => "RSA-2048 keygen",
            Self::RsaPkcs1v15Sign2048 => "RSA-PKCS1v1.5-Sign-2048",
            Self::RsaPssSign2048 => "RSA-PSS-Sign-2048",
            Self::RsaOaep2048 => "RSA-OAEP-2048",
            Self::RsaPkcs1v15Verify2048 => "RSA-PKCS1v1.5-Verify-2048",
            Self::RsaPssVerify2048 => "RSA-PSS-Verify-2048",
            Self::RsaKeygen3072 => "RSA-3072 keygen",
            Self::RsaPkcs1v15Sign3072 => "RSA-PKCS1v1.5-Sign-3072",
            Self::RsaPssSign3072 => "RSA-PSS-Sign-3072",
            Self::RsaOaep3072 => "RSA-OAEP-3072",
            Self::RsaPkcs1v15Verify3072 => "RSA-PKCS1v1.5-Verify-3072",
            Self::RsaPssVerify3072 => "RSA-PSS-Verify-3072",
            Self::RsaKeygen4096 => "RSA-4096 keygen",
            Self::RsaPkcs1v15Sign4096 => "RSA-PKCS1v1.5-Sign-4096",
            Self::RsaPssSign4096 => "RSA-PSS-Sign-4096",
            Self::RsaOaep4096 => "RSA-OAEP-4096",
            Self::RsaPkcs1v15Verify4096 => "RSA-PKCS1v1.5-Verify-4096",
            Self::RsaPssVerify4096 => "RSA-PSS-Verify-4096",
            Self::EcdsaP256Sign => "ECDSA-P256 sign",
            Self::EcdsaP256Verify => "ECDSA-P256 verify",
            Self::EcdsaP256Keygen => "ECDSA-P256 keygen",
            Self::EcdsaP384Sign => "ECDSA-P384 sign",
            Self::EcdsaP384Verify => "ECDSA-P384 verify",
            Self::EcdsaP384Keygen => "ECDSA-P384 keygen",
            Self::EcdhP256 => "ECDH-P256",
            Self::EcdhP384 => "ECDH-P384",
            Self::Ed25519Sign => "Ed25519 sign",
            Self::Ed25519Verify => "Ed25519 verify",
            Self::Ed25519Keygen => "Ed25519 keygen",
            Self::Tls12Kdf => "TLS 1.2 KDF",
            Self::Tls13Kdf => "TLS 1.3 KDF",
            Self::MlKem1024Encaps => "ML-KEM-1024 encaps",
            Self::MlKem1024Decaps => "ML-KEM-1024 decaps",
            Self::MlKem1024Keygen => "ML-KEM-1024 keygen",
            Self::MlKem512Encaps => "ML-KEM-512 encaps",
            Self::MlKem512Decaps => "ML-KEM-512 decaps",
            Self::MlKem512Keygen => "ML-KEM-512 keygen",
            Self::MlKem768Encaps => "ML-KEM-768 encaps",
            Self::MlKem768Decaps => "ML-KEM-768 decaps",
            Self::MlKem768Keygen => "ML-KEM-768 keygen",
            Self::MlDsa87Sign => "ML-DSA-87 sign",
            Self::MlDsa87Verify => "ML-DSA-87 verify",
            Self::MlDsa87Keygen => "ML-DSA-87 keygen",
            Self::MlDsa44Sign => "ML-DSA-44 sign",
            Self::MlDsa44Verify => "ML-DSA-44 verify",
            Self::MlDsa44Keygen => "ML-DSA-44 keygen",
            Self::MlDsa65Sign => "ML-DSA-65 sign",
            Self::MlDsa65Verify => "ML-DSA-65 verify",
            Self::MlDsa65Keygen => "ML-DSA-65 keygen",
            Self::SlhDsaSha2256sKeygen => "SLH-DSA-SHA2-256s keygen",
            Self::SlhDsaSha2256sSign => "SLH-DSA-SHA2-256s sign",
            Self::SlhDsaSha2256sVerify => "SLH-DSA-SHA2-256s verify",
            Self::SlhDsaSha2128sKeygen => "SLH-DSA-SHA2-128s keygen",
            Self::SlhDsaSha2128sSign => "SLH-DSA-SHA2-128s sign",
            Self::SlhDsaSha2128sVerify => "SLH-DSA-SHA2-128s verify",
            Self::SlhDsaSha2128fKeygen => "SLH-DSA-SHA2-128f keygen",
            Self::SlhDsaSha2128fSign => "SLH-DSA-SHA2-128f sign",
            Self::SlhDsaSha2128fVerify => "SLH-DSA-SHA2-128f verify",
            Self::SlhDsaSha2192sKeygen => "SLH-DSA-SHA2-192s keygen",
            Self::SlhDsaSha2192sSign => "SLH-DSA-SHA2-192s sign",
            Self::SlhDsaSha2192sVerify => "SLH-DSA-SHA2-192s verify",
            Self::SlhDsaSha2192fKeygen => "SLH-DSA-SHA2-192f keygen",
            Self::SlhDsaSha2192fSign => "SLH-DSA-SHA2-192f sign",
            Self::SlhDsaSha2192fVerify => "SLH-DSA-SHA2-192f verify",
            Self::SlhDsaSha2256fKeygen => "SLH-DSA-SHA2-256f keygen",
            Self::SlhDsaSha2256fSign => "SLH-DSA-SHA2-256f sign",
            Self::SlhDsaSha2256fVerify => "SLH-DSA-SHA2-256f verify",
            Self::SlhDsaShake128sKeygen => "SLH-DSA-SHAKE-128s keygen",
            Self::SlhDsaShake128sSign => "SLH-DSA-SHAKE-128s sign",
            Self::SlhDsaShake128sVerify => "SLH-DSA-SHAKE-128s verify",
            Self::SlhDsaShake128fKeygen => "SLH-DSA-SHAKE-128f keygen",
            Self::SlhDsaShake128fSign => "SLH-DSA-SHAKE-128f sign",
            Self::SlhDsaShake128fVerify => "SLH-DSA-SHAKE-128f verify",
            Self::SlhDsaShake192sKeygen => "SLH-DSA-SHAKE-192s keygen",
            Self::SlhDsaShake192sSign => "SLH-DSA-SHAKE-192s sign",
            Self::SlhDsaShake192sVerify => "SLH-DSA-SHAKE-192s verify",
            Self::SlhDsaShake192fKeygen => "SLH-DSA-SHAKE-192f keygen",
            Self::SlhDsaShake192fSign => "SLH-DSA-SHAKE-192f sign",
            Self::SlhDsaShake192fVerify => "SLH-DSA-SHAKE-192f verify",
            Self::SlhDsaShake256sKeygen => "SLH-DSA-SHAKE-256s keygen",
            Self::SlhDsaShake256sSign => "SLH-DSA-SHAKE-256s sign",
            Self::SlhDsaShake256sVerify => "SLH-DSA-SHAKE-256s verify",
            Self::SlhDsaShake256fKeygen => "SLH-DSA-SHAKE-256f keygen",
            Self::SlhDsaShake256fSign => "SLH-DSA-SHAKE-256f sign",
            Self::SlhDsaShake256fVerify => "SLH-DSA-SHAKE-256f verify",
            Self::LmsSha256M32H5W1Sign => "LMS SHA-256 M=32 H=5 W=1 sign",
            Self::LmsSha256M32H5W1Verify => "LMS SHA-256 M=32 H=5 W=1 verify",
            Self::LmsSha256M32H5W2Sign => "LMS SHA-256 M=32 H=5 W=2 sign",
            Self::LmsSha256M32H5W2Verify => "LMS SHA-256 M=32 H=5 W=2 verify",
            Self::LmsSha256M32H5W4Sign => "LMS SHA-256 M=32 H=5 W=4 sign",
            Self::LmsSha256M32H5W4Verify => "LMS SHA-256 M=32 H=5 W=4 verify",
            Self::LmsSha256M32H5W8Sign => "LMS SHA-256 M=32 H=5 W=8 sign",
            Self::LmsSha256M32H5W8Verify => "LMS SHA-256 M=32 H=5 W=8 verify",
            Self::LmsSha256M32H10W1Sign => "LMS SHA-256 M=32 H=10 W=1 sign",
            Self::LmsSha256M32H10W1Verify => "LMS SHA-256 M=32 H=10 W=1 verify",
            Self::LmsSha256M32H10W2Sign => "LMS SHA-256 M=32 H=10 W=2 sign",
            Self::LmsSha256M32H10W2Verify => "LMS SHA-256 M=32 H=10 W=2 verify",
            Self::LmsSha256M32H10W4Sign => "LMS SHA-256 M=32 H=10 W=4 sign",
            Self::LmsSha256M32H10W4Verify => "LMS SHA-256 M=32 H=10 W=4 verify",
            Self::LmsSha256M32H10W8Sign => "LMS SHA-256 M=32 H=10 W=8 sign",
            Self::LmsSha256M32H10W8Verify => "LMS SHA-256 M=32 H=10 W=8 verify",
            Self::LmsSha256M32H15W1Sign => "LMS SHA-256 M=32 H=15 W=1 sign",
            Self::LmsSha256M32H15W1Verify => "LMS SHA-256 M=32 H=15 W=1 verify",
            Self::LmsSha256M32H15W2Sign => "LMS SHA-256 M=32 H=15 W=2 sign",
            Self::LmsSha256M32H15W2Verify => "LMS SHA-256 M=32 H=15 W=2 verify",
            Self::LmsSha256M32H15W4Sign => "LMS SHA-256 M=32 H=15 W=4 sign",
            Self::LmsSha256M32H15W4Verify => "LMS SHA-256 M=32 H=15 W=4 verify",
            Self::LmsSha256M32H15W8Sign => "LMS SHA-256 M=32 H=15 W=8 sign",
            Self::LmsSha256M32H15W8Verify => "LMS SHA-256 M=32 H=15 W=8 verify",
            Self::LmsSha256M32H20W1Sign => "LMS SHA-256 M=32 H=20 W=1 sign",
            Self::LmsSha256M32H20W1Verify => "LMS SHA-256 M=32 H=20 W=1 verify",
            Self::LmsSha256M32H20W2Sign => "LMS SHA-256 M=32 H=20 W=2 sign",
            Self::LmsSha256M32H20W2Verify => "LMS SHA-256 M=32 H=20 W=2 verify",
            Self::LmsSha256M32H20W4Sign => "LMS SHA-256 M=32 H=20 W=4 sign",
            Self::LmsSha256M32H20W4Verify => "LMS SHA-256 M=32 H=20 W=4 verify",
            Self::LmsSha256M32H20W8Sign => "LMS SHA-256 M=32 H=20 W=8 sign",
            Self::LmsSha256M32H20W8Verify => "LMS SHA-256 M=32 H=20 W=8 verify",
            Self::LmsSha256M32H25W1Sign => "LMS SHA-256 M=32 H=25 W=1 sign",
            Self::LmsSha256M32H25W1Verify => "LMS SHA-256 M=32 H=25 W=1 verify",
            Self::LmsSha256M32H25W2Sign => "LMS SHA-256 M=32 H=25 W=2 sign",
            Self::LmsSha256M32H25W2Verify => "LMS SHA-256 M=32 H=25 W=2 verify",
            Self::LmsSha256M32H25W4Sign => "LMS SHA-256 M=32 H=25 W=4 sign",
            Self::LmsSha256M32H25W4Verify => "LMS SHA-256 M=32 H=25 W=4 verify",
            Self::LmsSha256M32H25W8Sign => "LMS SHA-256 M=32 H=25 W=8 sign",
            Self::LmsSha256M32H25W8Verify => "LMS SHA-256 M=32 H=25 W=8 verify",
            Self::LmsSha256M24H5W1Sign => "LMS SHA-256 M=24 H=5 W=1 sign",
            Self::LmsSha256M24H5W1Verify => "LMS SHA-256 M=24 H=5 W=1 verify",
            Self::LmsSha256M24H5W2Sign => "LMS SHA-256 M=24 H=5 W=2 sign",
            Self::LmsSha256M24H5W2Verify => "LMS SHA-256 M=24 H=5 W=2 verify",
            Self::LmsSha256M24H5W4Sign => "LMS SHA-256 M=24 H=5 W=4 sign",
            Self::LmsSha256M24H5W4Verify => "LMS SHA-256 M=24 H=5 W=4 verify",
            Self::LmsSha256M24H5W8Sign => "LMS SHA-256 M=24 H=5 W=8 sign",
            Self::LmsSha256M24H5W8Verify => "LMS SHA-256 M=24 H=5 W=8 verify",
            Self::LmsSha256M24H10W1Sign => "LMS SHA-256 M=24 H=10 W=1 sign",
            Self::LmsSha256M24H10W1Verify => "LMS SHA-256 M=24 H=10 W=1 verify",
            Self::LmsSha256M24H10W2Sign => "LMS SHA-256 M=24 H=10 W=2 sign",
            Self::LmsSha256M24H10W2Verify => "LMS SHA-256 M=24 H=10 W=2 verify",
            Self::LmsSha256M24H10W4Sign => "LMS SHA-256 M=24 H=10 W=4 sign",
            Self::LmsSha256M24H10W4Verify => "LMS SHA-256 M=24 H=10 W=4 verify",
            Self::LmsSha256M24H10W8Sign => "LMS SHA-256 M=24 H=10 W=8 sign",
            Self::LmsSha256M24H10W8Verify => "LMS SHA-256 M=24 H=10 W=8 verify",
            Self::LmsSha256M24H15W1Sign => "LMS SHA-256 M=24 H=15 W=1 sign",
            Self::LmsSha256M24H15W1Verify => "LMS SHA-256 M=24 H=15 W=1 verify",
            Self::LmsSha256M24H15W2Sign => "LMS SHA-256 M=24 H=15 W=2 sign",
            Self::LmsSha256M24H15W2Verify => "LMS SHA-256 M=24 H=15 W=2 verify",
            Self::LmsSha256M24H15W4Sign => "LMS SHA-256 M=24 H=15 W=4 sign",
            Self::LmsSha256M24H15W4Verify => "LMS SHA-256 M=24 H=15 W=4 verify",
            Self::LmsSha256M24H15W8Sign => "LMS SHA-256 M=24 H=15 W=8 sign",
            Self::LmsSha256M24H15W8Verify => "LMS SHA-256 M=24 H=15 W=8 verify",
            Self::LmsSha256M24H20W1Sign => "LMS SHA-256 M=24 H=20 W=1 sign",
            Self::LmsSha256M24H20W1Verify => "LMS SHA-256 M=24 H=20 W=1 verify",
            Self::LmsSha256M24H20W2Sign => "LMS SHA-256 M=24 H=20 W=2 sign",
            Self::LmsSha256M24H20W2Verify => "LMS SHA-256 M=24 H=20 W=2 verify",
            Self::LmsSha256M24H20W4Sign => "LMS SHA-256 M=24 H=20 W=4 sign",
            Self::LmsSha256M24H20W4Verify => "LMS SHA-256 M=24 H=20 W=4 verify",
            Self::LmsSha256M24H20W8Sign => "LMS SHA-256 M=24 H=20 W=8 sign",
            Self::LmsSha256M24H20W8Verify => "LMS SHA-256 M=24 H=20 W=8 verify",
            Self::LmsSha256M24H25W1Sign => "LMS SHA-256 M=24 H=25 W=1 sign",
            Self::LmsSha256M24H25W1Verify => "LMS SHA-256 M=24 H=25 W=1 verify",
            Self::LmsSha256M24H25W2Sign => "LMS SHA-256 M=24 H=25 W=2 sign",
            Self::LmsSha256M24H25W2Verify => "LMS SHA-256 M=24 H=25 W=2 verify",
            Self::LmsSha256M24H25W4Sign => "LMS SHA-256 M=24 H=25 W=4 sign",
            Self::LmsSha256M24H25W4Verify => "LMS SHA-256 M=24 H=25 W=4 verify",
            Self::LmsSha256M24H25W8Sign => "LMS SHA-256 M=24 H=25 W=8 sign",
            Self::LmsSha256M24H25W8Verify => "LMS SHA-256 M=24 H=25 W=8 verify",
            Self::LmsShakeM32H5W1Sign => "LMS SHAKE M=32 H=5 W=1 sign",
            Self::LmsShakeM32H5W1Verify => "LMS SHAKE M=32 H=5 W=1 verify",
            Self::LmsShakeM32H5W2Sign => "LMS SHAKE M=32 H=5 W=2 sign",
            Self::LmsShakeM32H5W2Verify => "LMS SHAKE M=32 H=5 W=2 verify",
            Self::LmsShakeM32H5W4Sign => "LMS SHAKE M=32 H=5 W=4 sign",
            Self::LmsShakeM32H5W4Verify => "LMS SHAKE M=32 H=5 W=4 verify",
            Self::LmsShakeM32H5W8Sign => "LMS SHAKE M=32 H=5 W=8 sign",
            Self::LmsShakeM32H5W8Verify => "LMS SHAKE M=32 H=5 W=8 verify",
            Self::LmsShakeM32H10W1Sign => "LMS SHAKE M=32 H=10 W=1 sign",
            Self::LmsShakeM32H10W1Verify => "LMS SHAKE M=32 H=10 W=1 verify",
            Self::LmsShakeM32H10W2Sign => "LMS SHAKE M=32 H=10 W=2 sign",
            Self::LmsShakeM32H10W2Verify => "LMS SHAKE M=32 H=10 W=2 verify",
            Self::LmsShakeM32H10W4Sign => "LMS SHAKE M=32 H=10 W=4 sign",
            Self::LmsShakeM32H10W4Verify => "LMS SHAKE M=32 H=10 W=4 verify",
            Self::LmsShakeM32H10W8Sign => "LMS SHAKE M=32 H=10 W=8 sign",
            Self::LmsShakeM32H10W8Verify => "LMS SHAKE M=32 H=10 W=8 verify",
            Self::LmsShakeM32H15W1Sign => "LMS SHAKE M=32 H=15 W=1 sign",
            Self::LmsShakeM32H15W1Verify => "LMS SHAKE M=32 H=15 W=1 verify",
            Self::LmsShakeM32H15W2Sign => "LMS SHAKE M=32 H=15 W=2 sign",
            Self::LmsShakeM32H15W2Verify => "LMS SHAKE M=32 H=15 W=2 verify",
            Self::LmsShakeM32H15W4Sign => "LMS SHAKE M=32 H=15 W=4 sign",
            Self::LmsShakeM32H15W4Verify => "LMS SHAKE M=32 H=15 W=4 verify",
            Self::LmsShakeM32H15W8Sign => "LMS SHAKE M=32 H=15 W=8 sign",
            Self::LmsShakeM32H15W8Verify => "LMS SHAKE M=32 H=15 W=8 verify",
            Self::LmsShakeM32H20W1Sign => "LMS SHAKE M=32 H=20 W=1 sign",
            Self::LmsShakeM32H20W1Verify => "LMS SHAKE M=32 H=20 W=1 verify",
            Self::LmsShakeM32H20W2Sign => "LMS SHAKE M=32 H=20 W=2 sign",
            Self::LmsShakeM32H20W2Verify => "LMS SHAKE M=32 H=20 W=2 verify",
            Self::LmsShakeM32H20W4Sign => "LMS SHAKE M=32 H=20 W=4 sign",
            Self::LmsShakeM32H20W4Verify => "LMS SHAKE M=32 H=20 W=4 verify",
            Self::LmsShakeM32H20W8Sign => "LMS SHAKE M=32 H=20 W=8 sign",
            Self::LmsShakeM32H20W8Verify => "LMS SHAKE M=32 H=20 W=8 verify",
            Self::LmsShakeM32H25W1Sign => "LMS SHAKE M=32 H=25 W=1 sign",
            Self::LmsShakeM32H25W1Verify => "LMS SHAKE M=32 H=25 W=1 verify",
            Self::LmsShakeM32H25W2Sign => "LMS SHAKE M=32 H=25 W=2 sign",
            Self::LmsShakeM32H25W2Verify => "LMS SHAKE M=32 H=25 W=2 verify",
            Self::LmsShakeM32H25W4Sign => "LMS SHAKE M=32 H=25 W=4 sign",
            Self::LmsShakeM32H25W4Verify => "LMS SHAKE M=32 H=25 W=4 verify",
            Self::LmsShakeM32H25W8Sign => "LMS SHAKE M=32 H=25 W=8 sign",
            Self::LmsShakeM32H25W8Verify => "LMS SHAKE M=32 H=25 W=8 verify",
            Self::LmsShakeM24H5W1Sign => "LMS SHAKE M=24 H=5 W=1 sign",
            Self::LmsShakeM24H5W1Verify => "LMS SHAKE M=24 H=5 W=1 verify",
            Self::LmsShakeM24H5W2Sign => "LMS SHAKE M=24 H=5 W=2 sign",
            Self::LmsShakeM24H5W2Verify => "LMS SHAKE M=24 H=5 W=2 verify",
            Self::LmsShakeM24H5W4Sign => "LMS SHAKE M=24 H=5 W=4 sign",
            Self::LmsShakeM24H5W4Verify => "LMS SHAKE M=24 H=5 W=4 verify",
            Self::LmsShakeM24H5W8Sign => "LMS SHAKE M=24 H=5 W=8 sign",
            Self::LmsShakeM24H5W8Verify => "LMS SHAKE M=24 H=5 W=8 verify",
            Self::LmsShakeM24H10W1Sign => "LMS SHAKE M=24 H=10 W=1 sign",
            Self::LmsShakeM24H10W1Verify => "LMS SHAKE M=24 H=10 W=1 verify",
            Self::LmsShakeM24H10W2Sign => "LMS SHAKE M=24 H=10 W=2 sign",
            Self::LmsShakeM24H10W2Verify => "LMS SHAKE M=24 H=10 W=2 verify",
            Self::LmsShakeM24H10W4Sign => "LMS SHAKE M=24 H=10 W=4 sign",
            Self::LmsShakeM24H10W4Verify => "LMS SHAKE M=24 H=10 W=4 verify",
            Self::LmsShakeM24H10W8Sign => "LMS SHAKE M=24 H=10 W=8 sign",
            Self::LmsShakeM24H10W8Verify => "LMS SHAKE M=24 H=10 W=8 verify",
            Self::LmsShakeM24H15W1Sign => "LMS SHAKE M=24 H=15 W=1 sign",
            Self::LmsShakeM24H15W1Verify => "LMS SHAKE M=24 H=15 W=1 verify",
            Self::LmsShakeM24H15W2Sign => "LMS SHAKE M=24 H=15 W=2 sign",
            Self::LmsShakeM24H15W2Verify => "LMS SHAKE M=24 H=15 W=2 verify",
            Self::LmsShakeM24H15W4Sign => "LMS SHAKE M=24 H=15 W=4 sign",
            Self::LmsShakeM24H15W4Verify => "LMS SHAKE M=24 H=15 W=4 verify",
            Self::LmsShakeM24H15W8Sign => "LMS SHAKE M=24 H=15 W=8 sign",
            Self::LmsShakeM24H15W8Verify => "LMS SHAKE M=24 H=15 W=8 verify",
            Self::LmsShakeM24H20W1Sign => "LMS SHAKE M=24 H=20 W=1 sign",
            Self::LmsShakeM24H20W1Verify => "LMS SHAKE M=24 H=20 W=1 verify",
            Self::LmsShakeM24H20W2Sign => "LMS SHAKE M=24 H=20 W=2 sign",
            Self::LmsShakeM24H20W2Verify => "LMS SHAKE M=24 H=20 W=2 verify",
            Self::LmsShakeM24H20W4Sign => "LMS SHAKE M=24 H=20 W=4 sign",
            Self::LmsShakeM24H20W4Verify => "LMS SHAKE M=24 H=20 W=4 verify",
            Self::LmsShakeM24H20W8Sign => "LMS SHAKE M=24 H=20 W=8 sign",
            Self::LmsShakeM24H20W8Verify => "LMS SHAKE M=24 H=20 W=8 verify",
            Self::LmsShakeM24H25W1Sign => "LMS SHAKE M=24 H=25 W=1 sign",
            Self::LmsShakeM24H25W1Verify => "LMS SHAKE M=24 H=25 W=1 verify",
            Self::LmsShakeM24H25W2Sign => "LMS SHAKE M=24 H=25 W=2 sign",
            Self::LmsShakeM24H25W2Verify => "LMS SHAKE M=24 H=25 W=2 verify",
            Self::LmsShakeM24H25W4Sign => "LMS SHAKE M=24 H=25 W=4 sign",
            Self::LmsShakeM24H25W4Verify => "LMS SHAKE M=24 H=25 W=4 verify",
            Self::LmsShakeM24H25W8Sign => "LMS SHAKE M=24 H=25 W=8 sign",
            Self::LmsShakeM24H25W8Verify => "LMS SHAKE M=24 H=25 W=8 verify",
            Self::XmssSign => "XMSS sign",
            Self::XmssVerify => "XMSS verify",
            Self::Dh3072 => "DH-3072",
        };
        f.write_str(name)
    }
}

/// Guard used at the entry point of every approved service, after
/// [`require_operational`], to enforce the active algorithm profile.
///
/// Returns `Ok(())` if the service is permitted under the profile
/// selected at initialization, or
/// `Err(Error::AlgorithmRestricted { .. })` otherwise.
///
/// This gate does **not** check [`State`] — callers must call
/// [`require_operational`] first. The two-gate pattern is:
///
/// ```ignore
/// require_operational()?;
/// require_allowed(Service::Sha256)?;
/// // ... perform the operation
/// ```
pub fn require_allowed(service: Service) -> Result<(), Error> {
    let profile = active_profile();
    if is_allowed(profile, service) {
        Ok(())
    } else {
        Err(Error::AlgorithmRestricted { service })
    }
}

/// Single source of truth for which services are allowed in each
/// profile.
const fn is_allowed(profile: AlgorithmProfile, service: Service) -> bool {
    match profile {
        AlgorithmProfile::Unrestricted => true,
        AlgorithmProfile::Cnsa2 => is_cnsa2_allowed(service),
        AlgorithmProfile::Cnsa1 => is_cnsa1_allowed(service),
    }
}

/// True for every LMS service, matching on the contiguous discriminant
/// block the `Service` enum reserves for LMS.
///
/// A range test rather than 160 enumerated arms: the LMS block occupies
/// `500..=659` with nothing else inside it. That is a property of the
/// enum, not of this function, so it is pinned separately by
/// `lms_discriminant_block_is_contiguous_and_pure` — without which a
/// variant inserted into the range would silently become CNSA-permitted.
const fn is_lms_service(service: Service) -> bool {
    lms_block_contains(service as u16)
}

/// The range arithmetic behind [`is_lms_service`], over a raw discriminant.
///
/// Split out so the boundaries can be tested at all. `is_lms_service` takes a
/// `Service`, and the LMS block sits at the very top of the enum — there is no
/// variant above 659 — so a widened *upper* bound admits nothing today and no
/// `Service`-typed probe can detect it. Over a raw `u16`, both edges are
/// reachable: `lms_block_boundaries_are_exact` pins 499/500/659/660, which
/// fails if either bound moves in either direction.
const fn lms_block_contains(discriminant: u16) -> bool {
    discriminant >= Service::LmsSha256M32H5W1Sign as u16
        && discriminant <= Service::LmsShakeM24H25W8Verify as u16
}

/// CNSA 2.0 allowed set. Only quantum-resistant algorithms plus
/// AES-256, SHA-384/512, SHA3-384/512, and 256-bit SP 800-185, and the
/// SP 800-208 stateful hash-based signatures for software and firmware
/// signing.
const fn is_cnsa2_allowed(service: Service) -> bool {
    matches!(
        service,
        // AES-256 all modes
        Service::Aes256Ecb
            | Service::Aes256Cbc
            | Service::Aes256Ctr
            | Service::Aes256Gcm
            | Service::Aes256Ccm
            | Service::Aes256Kw
            | Service::Aes256Kwp
            | Service::Aes256
            // SHA-384, SHA-512
            | Service::Sha384
            | Service::Sha512
            // SHA3-384, SHA3-512 (for internal / hardware — see plan note)
            | Service::Sha3_384
            | Service::Sha3_512
            // SHAKE-256
            | Service::Shake256
            // 256-bit SP 800-185 variants
            | Service::CShake256
            | Service::Kmac256
            | Service::KmacXof256
            | Service::TupleHash256
            | Service::TupleHashXof256
            | Service::ParallelHash256
            | Service::ParallelHashXof256
            // HMAC with allowed hashes
            | Service::HmacSha384
            | Service::HmacSha512
            | Service::HmacSha3_384
            | Service::HmacSha3_512
            // CMAC-AES-256
            | Service::CmacAes256
            // DRBGs backed by AES-256 or SHA-384/512
            | Service::CtrDrbgAes256
            | Service::HashDrbgSha384
            | Service::HashDrbgSha512
            | Service::HmacDrbgSha384
            | Service::HmacDrbgSha512
            // KDFs with allowed backing
            | Service::HkdfSha384
            | Service::HkdfSha512
            | Service::KbkdfHmacSha384
            | Service::KbkdfHmacSha512
            | Service::KbkdfCmacAes256
            | Service::Pbkdf2HmacSha384
            | Service::Pbkdf2HmacSha512
            // TLS 1.3 KDF — mandatory transport in CNSA 2.0
            | Service::Tls13Kdf
            // Post-quantum (CNSA 2.0 core)
            | Service::MlKem1024Encaps
            | Service::MlKem1024Decaps
            | Service::MlKem1024Keygen
            | Service::MlDsa87Sign
            | Service::MlDsa87Verify
            | Service::MlDsa87Keygen
            // XMSS — SP 800-208 stateful HBS for software/firmware signing.
            | Service::XmssSign
            | Service::XmssVerify
    )
        // LMS — SP 800-208 stateful HBS for software/firmware signing.
        // CNSSP-15 Annex B states "all parameters appropriate to protect to
        // TOP SECRET", recommending LMS-SHA-256/192, so every approved
        // parameter set the module implements is permitted rather than a
        // subset. Every non-LMS service outside the arms above still reaches
        // the `matches!` fail-safe default-block.
        || is_lms_service(service)
}

/// CNSA 1.0 allowed set. Classical algorithms for the transition
/// period: AES-256, SHA-384, ECDSA/ECDH P-384, RSA >= 3072,
/// DH >= 3072. Also includes SHA-256 (widely needed for
/// interoperability and certificate verification) and the
/// post-quantum algorithms allowed in CNSA 2.0 (CNSA 1.0 is
/// the transition profile; PQ algorithms are allowed if present).
const fn is_cnsa1_allowed(service: Service) -> bool {
    matches!(
        service,
        // AES-256 all modes
        Service::Aes256Ecb
            | Service::Aes256Cbc
            | Service::Aes256Ctr
            | Service::Aes256Gcm
            | Service::Aes256Ccm
            | Service::Aes256Kw
            | Service::Aes256Kwp
            | Service::Aes256
            // SHA-256, SHA-384, SHA-512 (SHA-256 needed for certs)
            | Service::Sha256
            | Service::Sha384
            | Service::Sha512
            // SHA3-256, SHA3-384, SHA3-512
            | Service::Sha3_256
            | Service::Sha3_384
            | Service::Sha3_512
            // SHAKE-256
            | Service::Shake256
            // 256-bit SP 800-185 variants
            | Service::CShake256
            | Service::Kmac256
            | Service::KmacXof256
            | Service::TupleHash256
            | Service::TupleHashXof256
            | Service::ParallelHash256
            | Service::ParallelHashXof256
            // HMAC with allowed hashes
            | Service::HmacSha256
            | Service::HmacSha384
            | Service::HmacSha512
            | Service::HmacSha3_256
            | Service::HmacSha3_384
            | Service::HmacSha3_512
            // CMAC-AES-256
            | Service::CmacAes256
            // DRBGs backed by AES-256 or allowed hashes
            | Service::CtrDrbgAes256
            | Service::HashDrbgSha256
            | Service::HashDrbgSha384
            | Service::HashDrbgSha512
            | Service::HmacDrbgSha256
            | Service::HmacDrbgSha384
            | Service::HmacDrbgSha512
            // KDFs with allowed backing
            | Service::HkdfSha256
            | Service::HkdfSha384
            | Service::HkdfSha512
            | Service::KbkdfHmacSha256
            | Service::KbkdfHmacSha384
            | Service::KbkdfHmacSha512
            | Service::KbkdfCmacAes256
            | Service::Pbkdf2HmacSha256
            | Service::Pbkdf2HmacSha384
            | Service::Pbkdf2HmacSha512
            // ECDSA/ECDH P-384
            | Service::EcdsaP384Sign
            | Service::EcdsaP384Verify
            | Service::EcdsaP384Keygen
            | Service::EcdhP384
            // RSA >= 3072
            | Service::RsaKeygen3072
            | Service::RsaPkcs1v15Sign3072
            | Service::RsaPssSign3072
            | Service::RsaOaep3072
            | Service::RsaPkcs1v15Verify3072
            | Service::RsaPssVerify3072
            | Service::RsaKeygen4096
            | Service::RsaPkcs1v15Sign4096
            | Service::RsaPssSign4096
            | Service::RsaOaep4096
            | Service::RsaPkcs1v15Verify4096
            | Service::RsaPssVerify4096
            // DH >= 3072
            | Service::Dh3072
            // TLS 1.3 KDF — accepted as the modern transport KDF in the
            // transition profile too (TLS 1.3 with classical-only ciphers
            // is CNSA-1.0-compatible)
            | Service::Tls13Kdf
            // PQ algorithms (allowed during transition for hybrid use)
            | Service::MlKem1024Encaps
            | Service::MlKem1024Decaps
            | Service::MlKem1024Keygen
            | Service::MlDsa87Sign
            | Service::MlDsa87Verify
            | Service::MlDsa87Keygen
            // XMSS — mirrors CNSA 2.0 during the transition.
            | Service::XmssSign
            | Service::XmssVerify
    )
        // LMS — mirrors CNSA 2.0 during the transition. Stateful HBS is
        // permitted under both profiles for software and firmware signing.
        || is_lms_service(service)
}

// -------------------------------------------------------------------------
// Test-only utilities
// -------------------------------------------------------------------------
//
// The global `STATE` makes the normal lifecycle effectively run-once per
// process. Unit tests in this crate need to exercise multiple transitions,
// so we expose a test-only reset helper gated behind `cfg(test)`. It is
// **not** part of the public API and is not compiled into release builds.

/// The test-only state lock, in its own module so the guard cannot be forged.
///
/// The globals are a process singleton and cargo runs unit tests on a parallel
/// thread pool, so without serialisation one test's reset lands inside
/// another's initialize-then-assert window and the second observes a profile it
/// did not install. Measured at 1 failure in 20 runs before this lock, on
/// `require_allowed_returns_restricted_error_under_cnsa2`.
///
/// Serialising is deliberate where per-test isolation would be easier: the
/// singleton is the thing under test, and giving each test private state would
/// stop exercising it.
///
/// **Why a module and not merely a private field.** `MutexGuard` carries no
/// identity tying it to a particular static, so a `StateGuard` wrapping *any*
/// mutex would satisfy the type and still reach `reset`. Confining the type
/// here — field private to this module — means the only way to obtain one is
/// [`reset_for_test`], which takes the real lock; a sibling module cannot build
/// a decoy. Editing this module still defeats it. That is the honest bound, and
/// it is a much smaller one than "every future test must remember".
#[cfg(test)]
pub(crate) mod state_lock {
    use super::{AlgorithmProfile, Ordering, PROFILE, STATE, State};

    static STATE_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());

    /// Proof that the holder owns the state lock. Constructible only by
    /// [`reset_for_test`], because the field is private to this module.
    #[must_use]
    pub(crate) struct StateGuard(
        /// Held for its `Drop`, never read: the lock releases when the guard
        /// leaves the test's scope.
        #[allow(dead_code)]
        std::sync::MutexGuard<'static, ()>,
    );

    impl StateGuard {
        /// Re-reset inside a test that already holds the lock — a loop over
        /// profiles needs fresh state per iteration without releasing it.
        // `&self` is the point: it is the proof the caller holds the lock.
        #[allow(clippy::unused_self)]
        pub(crate) fn reset(&self) {
            STATE.store(State::PowerOff as u8, Ordering::Release);
            PROFILE.store(AlgorithmProfile::Unrestricted as u8, Ordering::Release);
        }
    }

    /// Resets the module to power-off and returns the guard that makes the
    /// reset safe. The lock is not a convention a future test has to remember:
    /// this is the only way to obtain a `StateGuard`, and `StateGuard` is the
    /// only way to reach a reset.
    ///
    /// `StateGuard` is `#[must_use]`, so a bare `reset_for_test();` fails
    /// `clippy -D warnings`, which the workspace gate runs — dropping the guard
    /// early breaks the build rather than reintroducing the race quietly.
    ///
    /// Residual, stated rather than implied: `let _ = reset_for_test();` still
    /// drops the guard immediately and does not warn. That is a deliberate act,
    /// which is the most this construction can bound.
    ///
    /// A panicking test poisons the mutex. The poison carries no meaning here
    /// because the state it protects is unconditionally overwritten on the next
    /// line, so it is recovered rather than propagated.
    pub(crate) fn reset_for_test() -> StateGuard {
        let guard = StateGuard(
            STATE_LOCK
                .lock()
                .unwrap_or_else(std::sync::PoisonError::into_inner),
        );
        guard.reset();
        guard
    }
}

#[cfg(test)]
extern crate alloc;

#[cfg(test)]
extern crate std;

#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::panic)]
mod tests {
    use super::state_lock::reset_for_test;
    use super::{
        AlgorithmProfile, Error, KatEntry, SelfTest, SelfTestFailure, Service, State,
        active_profile, enter_error_state, initialize, initialize_with_profile,
        initialize_with_tests, is_allowed, is_lms_service, is_operational, lms_block_contains,
        require_allowed, require_operational, state,
    };
    use alloc::string::ToString;

    // Both of these helpers are used as `fn() -> Result<(), SelfTestFailure>`
    // function pointers in `KatEntry`, so the `Result` wrapper is mandatory
    // even though clippy sees each body as always returning the same variant.
    #[allow(clippy::unnecessary_wraps)]
    fn always_pass() -> Result<(), SelfTestFailure> {
        Ok(())
    }
    fn always_fail() -> Result<(), SelfTestFailure> {
        Err(SelfTestFailure)
    }

    // All tests in this module share the single global `STATE`, so they
    // cannot run in parallel. `cargo test` in this crate must be invoked
    // with `--test-threads=1` — enforced by the CI workflow.

    #[test]
    fn initial_state_is_power_off() {
        let _guard = reset_for_test();
        assert_eq!(state(), State::PowerOff);
        assert!(!is_operational());
    }

    #[test]
    fn initialize_transitions_to_operational() {
        let _guard = reset_for_test();
        initialize().unwrap();
        assert_eq!(state(), State::Operational);
        assert!(is_operational());
        require_operational().unwrap();
    }

    #[test]
    fn second_initialize_reports_already_initialized() {
        let _guard = reset_for_test();
        initialize().unwrap();
        match initialize() {
            Err(Error::AlreadyInitialized) => {}
            other => panic!("expected AlreadyInitialized, got {other:?}"),
        }
    }

    #[test]
    fn require_operational_rejects_power_off() {
        let _guard = reset_for_test();
        match require_operational() {
            Err(Error::NotOperational {
                current: State::PowerOff,
            }) => {}
            other => panic!("expected NotOperational{{PowerOff}}, got {other:?}"),
        }
    }

    #[test]
    fn enter_error_state_is_terminal_for_require_operational() {
        let _guard = reset_for_test();
        initialize().unwrap();
        assert!(is_operational());
        enter_error_state("unit-test forced failure");
        assert_eq!(state(), State::Error);
        match require_operational() {
            Err(Error::NotOperational {
                current: State::Error,
            }) => {}
            other => panic!("expected NotOperational{{Error}}, got {other:?}"),
        }
    }

    #[test]
    fn state_display_is_stable() {
        // The exact strings appear in log output and audit trails,
        // so pin them.
        assert_eq!(State::PowerOff.to_string(), "PowerOff");
        assert_eq!(State::SelfTest.to_string(), "SelfTest");
        assert_eq!(State::Operational.to_string(), "Operational");
        assert_eq!(State::Error.to_string(), "Error");
    }

    #[test]
    fn registry_runs_passing_tests_and_reaches_operational() {
        let _guard = reset_for_test();
        let tests = [
            KatEntry {
                name: "dummy-pass-a",
                run: always_pass,
            },
            KatEntry {
                name: "dummy-pass-b",
                run: always_pass,
            },
        ];
        initialize_with_tests(&tests).unwrap();
        assert_eq!(state(), State::Operational);
    }

    #[test]
    fn registry_failing_test_latches_error_and_returns_name() {
        let _guard = reset_for_test();
        let tests = [
            KatEntry {
                name: "dummy-pass",
                run: always_pass,
            },
            KatEntry {
                name: "dummy-fail",
                run: always_fail,
            },
            // This third entry must never run.
            KatEntry {
                name: "dummy-unreached",
                run: always_pass,
            },
        ];
        match initialize_with_tests(&tests) {
            Err(Error::SelfTestFailed { test: "dummy-fail" }) => {}
            other => panic!("expected SelfTestFailed{{dummy-fail}}, got {other:?}"),
        }
        assert_eq!(state(), State::Error);
        // Operational-only guard must reject calls from here on.
        assert!(matches!(
            require_operational(),
            Err(Error::NotOperational {
                current: State::Error
            })
        ));
    }

    // Trivial SelfTest implementation exercised outside the registry
    // so the trait stays part of the compiled public surface.
    struct DummyTest;
    impl SelfTest for DummyTest {
        const NAME: &'static str = "dummy-trait-vector";
        fn run() -> Result<(), SelfTestFailure> {
            Ok(())
        }
    }

    #[test]
    fn dummy_self_test_trait_is_usable() {
        assert_eq!(DummyTest::NAME, "dummy-trait-vector");
        DummyTest::run().unwrap();
    }

    // =====================================================================
    // Algorithm profile gating tests
    // =====================================================================

    #[test]
    fn default_profile_is_unrestricted() {
        let _guard = reset_for_test();
        assert_eq!(active_profile(), AlgorithmProfile::Unrestricted);
    }

    #[test]
    fn initialize_with_profile_sets_cnsa2() {
        let _guard = reset_for_test();
        initialize_with_profile(&[], AlgorithmProfile::Cnsa2).unwrap();
        assert_eq!(active_profile(), AlgorithmProfile::Cnsa2);
        assert!(is_operational());
    }

    #[test]
    fn initialize_with_profile_sets_cnsa1() {
        let _guard = reset_for_test();
        initialize_with_profile(&[], AlgorithmProfile::Cnsa1).unwrap();
        assert_eq!(active_profile(), AlgorithmProfile::Cnsa1);
        assert!(is_operational());
    }

    #[test]
    fn unrestricted_allows_all_services() {
        // Spot-check: every category should pass in Unrestricted.
        let spot = [
            Service::Sha1,
            Service::Sha256,
            Service::Aes128Ecb,
            Service::Aes256Gcm,
            Service::CtrDrbgAes128,
            Service::EcdsaP256Sign,
            Service::Ed25519Sign,
            Service::RsaPssSign2048,
            Service::MlKem1024Encaps,
            Service::LmsSha256M32H10W4Sign,
            Service::Dh3072,
            Service::SlhDsaSha2256sSign,
            Service::Tls12Kdf,
            Service::Tls13Kdf,
        ];
        for svc in spot {
            assert!(
                is_allowed(AlgorithmProfile::Unrestricted, svc),
                "{svc} should be allowed in Unrestricted"
            );
        }
    }

    #[test]
    fn cnsa2_allows_aes256_and_pq() {
        let allowed = [
            Service::Aes256Gcm,
            Service::Aes256Cbc,
            Service::Sha384,
            Service::Sha512,
            Service::Sha3_384,
            Service::Sha3_512,
            Service::HmacSha384,
            Service::HmacSha512,
            Service::CtrDrbgAes256,
            Service::HashDrbgSha384,
            Service::HmacDrbgSha512,
            Service::Kmac256,
            Service::KmacXof256,
            Service::MlKem1024Encaps,
            Service::MlKem1024Decaps,
            Service::MlKem1024Keygen,
            Service::MlDsa87Sign,
            Service::MlDsa87Verify,
            Service::MlDsa87Keygen,
            Service::LmsSha256M32H10W4Sign,
            Service::LmsSha256M32H10W4Verify,
            Service::XmssSign,
            Service::XmssVerify,
            Service::Tls13Kdf,
        ];
        for svc in allowed {
            assert!(
                is_allowed(AlgorithmProfile::Cnsa2, svc),
                "{svc} should be allowed in CNSA 2.0"
            );
        }
    }

    #[test]
    fn cnsa2_blocks_classical_and_small_keys() {
        let blocked = [
            Service::Sha1,
            Service::Sha224,
            Service::Sha256,
            Service::Sha512_224,
            Service::Sha512_256,
            Service::Sha3_224,
            Service::Sha3_256,
            Service::Shake128,
            Service::Kmac128,
            Service::Aes128Ecb,
            Service::Aes128Gcm,
            Service::Aes192Cbc,
            Service::CtrDrbgAes128,
            Service::CtrDrbgAes192,
            Service::HashDrbgSha256,
            Service::HmacDrbgSha256,
            Service::HmacSha1,
            Service::HmacSha256,
            Service::CmacAes128,
            Service::CmacAes192,
            Service::EcdsaP256Sign,
            Service::EcdsaP256Verify,
            Service::EcdhP256,
            Service::Ed25519Sign,
            Service::Ed25519Verify,
            Service::RsaPssSign2048,
            Service::RsaOaep2048,
            Service::Tls12Kdf,
            // No SLH-DSA parameter set is in CNSA 2.0 — CNSSP-15 lists none.
            Service::SlhDsaSha2128sSign,
            Service::MlDsa44Sign,
            Service::MlDsa44Verify,
            Service::MlDsa44Keygen,
            Service::MlDsa65Sign,
            Service::MlDsa65Verify,
            Service::MlDsa65Keygen,
        ];
        for svc in blocked {
            assert!(
                !is_allowed(AlgorithmProfile::Cnsa2, svc),
                "{svc} should be BLOCKED in CNSA 2.0"
            );
        }
    }

    #[test]
    fn cnsa1_allows_p384_rsa3072_and_pq() {
        let allowed = [
            Service::Aes256Gcm,
            Service::Sha384,
            Service::Sha256,
            Service::EcdsaP384Sign,
            Service::EcdsaP384Verify,
            Service::EcdsaP384Keygen,
            Service::EcdhP384,
            Service::RsaPssSign3072,
            Service::RsaOaep3072,
            Service::RsaPssSign4096,
            Service::Dh3072,
            Service::MlKem1024Encaps,
            Service::LmsSha256M32H10W4Sign,
            Service::Tls13Kdf,
        ];
        for svc in allowed {
            assert!(
                is_allowed(AlgorithmProfile::Cnsa1, svc),
                "{svc} should be allowed in CNSA 1.0"
            );
        }
    }

    #[test]
    fn cnsa1_blocks_small_keys_and_ed25519() {
        let blocked = [
            Service::Sha1,
            Service::Sha224,
            Service::Aes128Ecb,
            Service::Aes192Gcm,
            Service::CtrDrbgAes128,
            Service::EcdsaP256Sign,
            Service::EcdhP256,
            Service::Ed25519Sign,
            Service::Ed25519Verify,
            Service::RsaPssSign2048,
            Service::RsaOaep2048,
            Service::Tls12Kdf,
        ];
        for svc in blocked {
            assert!(
                !is_allowed(AlgorithmProfile::Cnsa1, svc),
                "{svc} should be BLOCKED in CNSA 1.0"
            );
        }
    }

    // The full LMS service enumeration, shared by the gating test and the
    // discriminant-block test. Explicit rather than generated: CMVP reviewers
    // read this list, and a macro would hide exactly what it must show.
    #[allow(clippy::too_many_lines)]
    fn lms_all_160() -> [Service; 160] {
        [
            // SHA-256 / N=32 family (20 pairs, 40 entries).
            Service::LmsSha256M32H5W1Sign,
            Service::LmsSha256M32H5W1Verify,
            Service::LmsSha256M32H5W2Sign,
            Service::LmsSha256M32H5W2Verify,
            Service::LmsSha256M32H5W4Sign,
            Service::LmsSha256M32H5W4Verify,
            Service::LmsSha256M32H5W8Sign,
            Service::LmsSha256M32H5W8Verify,
            Service::LmsSha256M32H10W1Sign,
            Service::LmsSha256M32H10W1Verify,
            Service::LmsSha256M32H10W2Sign,
            Service::LmsSha256M32H10W2Verify,
            Service::LmsSha256M32H10W4Sign,
            Service::LmsSha256M32H10W4Verify,
            Service::LmsSha256M32H10W8Sign,
            Service::LmsSha256M32H10W8Verify,
            Service::LmsSha256M32H15W1Sign,
            Service::LmsSha256M32H15W1Verify,
            Service::LmsSha256M32H15W2Sign,
            Service::LmsSha256M32H15W2Verify,
            Service::LmsSha256M32H15W4Sign,
            Service::LmsSha256M32H15W4Verify,
            Service::LmsSha256M32H15W8Sign,
            Service::LmsSha256M32H15W8Verify,
            Service::LmsSha256M32H20W1Sign,
            Service::LmsSha256M32H20W1Verify,
            Service::LmsSha256M32H20W2Sign,
            Service::LmsSha256M32H20W2Verify,
            Service::LmsSha256M32H20W4Sign,
            Service::LmsSha256M32H20W4Verify,
            Service::LmsSha256M32H20W8Sign,
            Service::LmsSha256M32H20W8Verify,
            Service::LmsSha256M32H25W1Sign,
            Service::LmsSha256M32H25W1Verify,
            Service::LmsSha256M32H25W2Sign,
            Service::LmsSha256M32H25W2Verify,
            Service::LmsSha256M32H25W4Sign,
            Service::LmsSha256M32H25W4Verify,
            Service::LmsSha256M32H25W8Sign,
            Service::LmsSha256M32H25W8Verify,
            // SHA-256 / N=24 family (20 pairs, 40 entries).
            Service::LmsSha256M24H5W1Sign,
            Service::LmsSha256M24H5W1Verify,
            Service::LmsSha256M24H5W2Sign,
            Service::LmsSha256M24H5W2Verify,
            Service::LmsSha256M24H5W4Sign,
            Service::LmsSha256M24H5W4Verify,
            Service::LmsSha256M24H5W8Sign,
            Service::LmsSha256M24H5W8Verify,
            Service::LmsSha256M24H10W1Sign,
            Service::LmsSha256M24H10W1Verify,
            Service::LmsSha256M24H10W2Sign,
            Service::LmsSha256M24H10W2Verify,
            Service::LmsSha256M24H10W4Sign,
            Service::LmsSha256M24H10W4Verify,
            Service::LmsSha256M24H10W8Sign,
            Service::LmsSha256M24H10W8Verify,
            Service::LmsSha256M24H15W1Sign,
            Service::LmsSha256M24H15W1Verify,
            Service::LmsSha256M24H15W2Sign,
            Service::LmsSha256M24H15W2Verify,
            Service::LmsSha256M24H15W4Sign,
            Service::LmsSha256M24H15W4Verify,
            Service::LmsSha256M24H15W8Sign,
            Service::LmsSha256M24H15W8Verify,
            Service::LmsSha256M24H20W1Sign,
            Service::LmsSha256M24H20W1Verify,
            Service::LmsSha256M24H20W2Sign,
            Service::LmsSha256M24H20W2Verify,
            Service::LmsSha256M24H20W4Sign,
            Service::LmsSha256M24H20W4Verify,
            Service::LmsSha256M24H20W8Sign,
            Service::LmsSha256M24H20W8Verify,
            Service::LmsSha256M24H25W1Sign,
            Service::LmsSha256M24H25W1Verify,
            Service::LmsSha256M24H25W2Sign,
            Service::LmsSha256M24H25W2Verify,
            Service::LmsSha256M24H25W4Sign,
            Service::LmsSha256M24H25W4Verify,
            Service::LmsSha256M24H25W8Sign,
            Service::LmsSha256M24H25W8Verify,
            // SHAKE-256 / N=32 family (20 pairs, 40 entries).
            Service::LmsShakeM32H5W1Sign,
            Service::LmsShakeM32H5W1Verify,
            Service::LmsShakeM32H5W2Sign,
            Service::LmsShakeM32H5W2Verify,
            Service::LmsShakeM32H5W4Sign,
            Service::LmsShakeM32H5W4Verify,
            Service::LmsShakeM32H5W8Sign,
            Service::LmsShakeM32H5W8Verify,
            Service::LmsShakeM32H10W1Sign,
            Service::LmsShakeM32H10W1Verify,
            Service::LmsShakeM32H10W2Sign,
            Service::LmsShakeM32H10W2Verify,
            Service::LmsShakeM32H10W4Sign,
            Service::LmsShakeM32H10W4Verify,
            Service::LmsShakeM32H10W8Sign,
            Service::LmsShakeM32H10W8Verify,
            Service::LmsShakeM32H15W1Sign,
            Service::LmsShakeM32H15W1Verify,
            Service::LmsShakeM32H15W2Sign,
            Service::LmsShakeM32H15W2Verify,
            Service::LmsShakeM32H15W4Sign,
            Service::LmsShakeM32H15W4Verify,
            Service::LmsShakeM32H15W8Sign,
            Service::LmsShakeM32H15W8Verify,
            Service::LmsShakeM32H20W1Sign,
            Service::LmsShakeM32H20W1Verify,
            Service::LmsShakeM32H20W2Sign,
            Service::LmsShakeM32H20W2Verify,
            Service::LmsShakeM32H20W4Sign,
            Service::LmsShakeM32H20W4Verify,
            Service::LmsShakeM32H20W8Sign,
            Service::LmsShakeM32H20W8Verify,
            Service::LmsShakeM32H25W1Sign,
            Service::LmsShakeM32H25W1Verify,
            Service::LmsShakeM32H25W2Sign,
            Service::LmsShakeM32H25W2Verify,
            Service::LmsShakeM32H25W4Sign,
            Service::LmsShakeM32H25W4Verify,
            Service::LmsShakeM32H25W8Sign,
            Service::LmsShakeM32H25W8Verify,
            // SHAKE-256 / N=24 family (20 pairs, 40 entries).
            Service::LmsShakeM24H5W1Sign,
            Service::LmsShakeM24H5W1Verify,
            Service::LmsShakeM24H5W2Sign,
            Service::LmsShakeM24H5W2Verify,
            Service::LmsShakeM24H5W4Sign,
            Service::LmsShakeM24H5W4Verify,
            Service::LmsShakeM24H5W8Sign,
            Service::LmsShakeM24H5W8Verify,
            Service::LmsShakeM24H10W1Sign,
            Service::LmsShakeM24H10W1Verify,
            Service::LmsShakeM24H10W2Sign,
            Service::LmsShakeM24H10W2Verify,
            Service::LmsShakeM24H10W4Sign,
            Service::LmsShakeM24H10W4Verify,
            Service::LmsShakeM24H10W8Sign,
            Service::LmsShakeM24H10W8Verify,
            Service::LmsShakeM24H15W1Sign,
            Service::LmsShakeM24H15W1Verify,
            Service::LmsShakeM24H15W2Sign,
            Service::LmsShakeM24H15W2Verify,
            Service::LmsShakeM24H15W4Sign,
            Service::LmsShakeM24H15W4Verify,
            Service::LmsShakeM24H15W8Sign,
            Service::LmsShakeM24H15W8Verify,
            Service::LmsShakeM24H20W1Sign,
            Service::LmsShakeM24H20W1Verify,
            Service::LmsShakeM24H20W2Sign,
            Service::LmsShakeM24H20W2Verify,
            Service::LmsShakeM24H20W4Sign,
            Service::LmsShakeM24H20W4Verify,
            Service::LmsShakeM24H20W8Sign,
            Service::LmsShakeM24H20W8Verify,
            Service::LmsShakeM24H25W1Sign,
            Service::LmsShakeM24H25W1Verify,
            Service::LmsShakeM24H25W2Sign,
            Service::LmsShakeM24H25W2Verify,
            Service::LmsShakeM24H25W4Sign,
            Service::LmsShakeM24H25W4Verify,
            Service::LmsShakeM24H25W8Sign,
            Service::LmsShakeM24H25W8Verify,
        ]
    }

    #[test]
    fn lms_gating_is_exhaustive_across_all_160_variants() {
        // Enumerates every LMS Service variant (80 (LMS, LM-OTS) pairs ×
        // {Sign, Verify} = 160 entries) and verifies that all 160 are
        // permitted under all three profiles: CNSSP-15 Annex B places no
        // subset restriction on LMS, and Unrestricted permits everything.
        // If a new LMS variant is added without a matching entry in
        // `lms_all_160`, the length assertion catches the drift.
        let all_160 = lms_all_160();
        assert_eq!(all_160.len(), 160, "LMS enumeration drift");

        for svc in all_160 {
            // Unrestricted: every LMS variant is permitted.
            assert!(
                is_allowed(AlgorithmProfile::Unrestricted, svc),
                "{svc} must be allowed in Unrestricted"
            );

            // CNSA 2.0: every approved SP 800-208 parameter set is permitted.
            // CNSSP-15 Annex B places no subset restriction on LMS.
            assert!(
                is_allowed(AlgorithmProfile::Cnsa2, svc),
                "{svc} must be allowed in CNSA 2.0"
            );

            // CNSA 1.0: mirrors CNSA 2.0 on LMS (transition profile).
            assert!(
                is_allowed(AlgorithmProfile::Cnsa1, svc),
                "{svc} must be allowed in CNSA 1.0"
            );
        }
    }

    #[test]
    fn lms_discriminant_block_is_contiguous_and_pure() {
        // `is_lms_service` range-tests `500..=659` instead of enumerating 160
        // arms, which is only sound while that block holds every LMS service
        // and nothing else. Enum discriminants are unique, so proving the 160
        // LMS variants occupy the range exactly also proves no other variant
        // can sit inside it.
        let mut discriminants = [0u16; 160];
        for (slot, svc) in discriminants.iter_mut().zip(lms_all_160()) {
            *slot = svc as u16;
        }
        discriminants.sort_unstable();

        // Strictly increasing after sorting == all 160 are distinct. The
        // comparison count is asserted too, so an empty or short sweep
        // cannot pass this vacuously.
        let mut compared = 0usize;
        for pair in discriminants.windows(2) {
            let [lo, hi] = pair else {
                unreachable!("windows(2) yields two-element slices")
            };
            assert!(hi > lo, "duplicate LMS discriminant {lo}");
            compared += 1;
        }
        assert_eq!(compared, 159, "distinctness sweep did not run in full");

        let (Some(&first), Some(&last)) = (discriminants.first(), discriminants.last()) else {
            unreachable!("the array holds 160 elements")
        };
        assert_eq!(first, 500, "LMS block does not start at 500");
        assert_eq!(last, 659, "LMS block does not end at 659");
        // 160 distinct values spanning an inclusive range of 160 is exactly
        // full occupancy — no gap a non-LMS variant could occupy.
        assert_eq!(last - first + 1, 160, "LMS block is not contiguous");

        for svc in lms_all_160() {
            assert!(is_lms_service(svc), "{svc} must be inside the LMS range");
        }

        // Positive control on the other side of the boundary: the nearest
        // non-LMS neighbours must not be swept in by a widened range.
        for svc in [
            Service::SlhDsaShake256fVerify,
            Service::XmssSign,
            Service::XmssVerify,
            Service::Dh3072,
        ] {
            assert!(!is_lms_service(svc), "{svc} must be outside the LMS range");
        }
    }

    #[test]
    fn slh_dsa_gating_is_exhaustive_across_all_36_variants() {
        // Enumerates every SLH-DSA Service variant (12 parameter sets ×
        // {Keygen, Sign, Verify} = 36 entries) and verifies the
        // fail-safe gating: all 36 are blocked under both CNSA profiles,
        // because CNSSP-15 lists no SLH-DSA parameter set at all; all 36
        // are permitted under Unrestricted.  If a new SLH-DSA variant is
        // added without a matching entry in this array, the assertion at
        // the end catches the gap.
        let all_36 = [
            // SHA-2 family (18 entries).
            Service::SlhDsaSha2256sKeygen,
            Service::SlhDsaSha2256sSign,
            Service::SlhDsaSha2256sVerify,
            Service::SlhDsaSha2128sKeygen,
            Service::SlhDsaSha2128sSign,
            Service::SlhDsaSha2128sVerify,
            Service::SlhDsaSha2128fKeygen,
            Service::SlhDsaSha2128fSign,
            Service::SlhDsaSha2128fVerify,
            Service::SlhDsaSha2192sKeygen,
            Service::SlhDsaSha2192sSign,
            Service::SlhDsaSha2192sVerify,
            Service::SlhDsaSha2192fKeygen,
            Service::SlhDsaSha2192fSign,
            Service::SlhDsaSha2192fVerify,
            Service::SlhDsaSha2256fKeygen,
            Service::SlhDsaSha2256fSign,
            Service::SlhDsaSha2256fVerify,
            // SHAKE family (18 entries).
            Service::SlhDsaShake128sKeygen,
            Service::SlhDsaShake128sSign,
            Service::SlhDsaShake128sVerify,
            Service::SlhDsaShake128fKeygen,
            Service::SlhDsaShake128fSign,
            Service::SlhDsaShake128fVerify,
            Service::SlhDsaShake192sKeygen,
            Service::SlhDsaShake192sSign,
            Service::SlhDsaShake192sVerify,
            Service::SlhDsaShake192fKeygen,
            Service::SlhDsaShake192fSign,
            Service::SlhDsaShake192fVerify,
            Service::SlhDsaShake256sKeygen,
            Service::SlhDsaShake256sSign,
            Service::SlhDsaShake256sVerify,
            Service::SlhDsaShake256fKeygen,
            Service::SlhDsaShake256fSign,
            Service::SlhDsaShake256fVerify,
        ];
        assert_eq!(all_36.len(), 36, "SLH-DSA enumeration drift");

        for svc in all_36 {
            // Unrestricted: every SLH-DSA variant is permitted.
            assert!(
                is_allowed(AlgorithmProfile::Unrestricted, svc),
                "{svc} must be allowed in Unrestricted"
            );

            // CNSA 1.0: no SLH-DSA variant is permitted.
            assert!(
                !is_allowed(AlgorithmProfile::Cnsa1, svc),
                "{svc} must be BLOCKED in CNSA 1.0"
            );

            // CNSA 2.0: no SLH-DSA variant is permitted either. CNSSP-15
            // contains no SLH-DSA entry, so admitting one would be a
            // conformance claim the policy does not support.
            assert!(
                !is_allowed(AlgorithmProfile::Cnsa2, svc),
                "{svc} must be BLOCKED in CNSA 2.0"
            );
        }
    }

    #[test]
    fn require_allowed_returns_restricted_error_under_cnsa2() {
        let _guard = reset_for_test();
        initialize_with_profile(&[], AlgorithmProfile::Cnsa2).unwrap();
        match require_allowed(Service::Aes128Ecb) {
            Err(Error::AlgorithmRestricted {
                service: Service::Aes128Ecb,
            }) => {}
            other => panic!("expected AlgorithmRestricted(Aes128Ecb), got {other:?}"),
        }
    }

    #[test]
    fn require_allowed_passes_permitted_services() {
        let _guard = reset_for_test();
        initialize_with_profile(&[], AlgorithmProfile::Cnsa2).unwrap();
        require_allowed(Service::Aes256Gcm).unwrap();
        require_allowed(Service::Sha384).unwrap();
        require_allowed(Service::MlKem1024Encaps).unwrap();
    }

    #[test]
    fn require_allowed_passes_everything_in_unrestricted() {
        let _guard = reset_for_test();
        initialize().unwrap();
        // Spot-check a few from each end of the spectrum.
        require_allowed(Service::Sha1).unwrap();
        require_allowed(Service::Aes128Ecb).unwrap();
        require_allowed(Service::Ed25519Sign).unwrap();
        require_allowed(Service::MlKem1024Encaps).unwrap();
    }

    /// Entry-point coverage for the SLH-DSA correction. The exhaustive
    /// `slh_dsa_gating_*` test calls the private `is_allowed` helper directly,
    /// so it says nothing about whether the public gate reaches that helper.
    /// This one goes through `require_allowed`, which is what every approved
    /// service actually calls.
    #[test]
    fn require_allowed_refuses_every_slh_dsa_service_under_cnsa2() {
        let guard = reset_for_test();
        for profile in [AlgorithmProfile::Cnsa2, AlgorithmProfile::Cnsa1] {
            guard.reset();
            initialize_with_profile(&[], profile).unwrap();
            for svc in [
                Service::SlhDsaSha2256sKeygen,
                Service::SlhDsaSha2256sSign,
                Service::SlhDsaSha2256sVerify,
                Service::SlhDsaShake128fVerify,
            ] {
                match require_allowed(svc) {
                    Err(Error::AlgorithmRestricted { service }) => assert_eq!(service, svc),
                    other => {
                        panic!("expected AlgorithmRestricted({svc}) under {profile}, got {other:?}")
                    }
                }
            }
        }
    }

    /// Entry-point coverage for the LMS correction, on the `M=24` sets.
    /// CNSSP-15 Annex B recommends LMS-SHA-256/192, which is `M=24`, so these
    /// are the parameter sets the policy names by name.
    #[test]
    fn require_allowed_admits_lms_m24_under_both_cnsa_profiles() {
        let guard = reset_for_test();
        for profile in [AlgorithmProfile::Cnsa2, AlgorithmProfile::Cnsa1] {
            guard.reset();
            initialize_with_profile(&[], profile).unwrap();
            for svc in [
                Service::LmsSha256M24H5W1Sign,
                Service::LmsSha256M24H25W8Verify,
                Service::LmsShakeM24H10W4Sign,
                Service::LmsShakeM24H25W8Verify,
            ] {
                require_allowed(svc)
                    .unwrap_or_else(|e| panic!("{svc} must be allowed under {profile}: {e:?}"));
            }
        }
    }

    /// Both edges of the LMS discriminant block, over raw values.
    ///
    /// The block sits at the very top of the enum, so a widened *upper* bound
    /// admits nothing today and no `Service`-typed probe can see it — moving
    /// the bound to 709 leaves the whole suite green. Testing the arithmetic
    /// directly is what makes the upper edge checkable at all.
    #[test]
    fn lms_block_boundaries_are_exact() {
        assert!(!lms_block_contains(499), "499 is below the LMS block");
        assert!(lms_block_contains(500), "500 is the first LMS discriminant");
        assert!(lms_block_contains(659), "659 is the last LMS discriminant");
        assert!(!lms_block_contains(660), "660 is above the LMS block");

        // ...and the block the arithmetic describes is the one the enum has.
        assert_eq!(Service::LmsSha256M32H5W1Sign as u16, 500);
        assert_eq!(Service::LmsShakeM24H25W8Verify as u16, 659);
    }

    /// The two CNSA profiles must be told apart at the entry point. Every other
    /// `require_allowed` test uses a service the profiles treat alike, so a bug
    /// wiring `Cnsa1` through as `Cnsa2` would survive them all. P-384 ECDSA is
    /// the discriminator: CNSA 1.0 permits it, CNSA 2.0 does not.
    #[test]
    fn require_allowed_distinguishes_cnsa1_from_cnsa2() {
        let guard = reset_for_test();

        initialize_with_profile(&[], AlgorithmProfile::Cnsa1).unwrap();
        require_allowed(Service::EcdsaP384Sign)
            .unwrap_or_else(|e| panic!("P-384 ECDSA must be permitted under CNSA 1.0: {e:?}"));

        guard.reset();
        initialize_with_profile(&[], AlgorithmProfile::Cnsa2).unwrap();
        match require_allowed(Service::EcdsaP384Sign) {
            Err(Error::AlgorithmRestricted {
                service: Service::EcdsaP384Sign,
            }) => {}
            other => panic!("expected P-384 ECDSA refused under CNSA 2.0, got {other:?}"),
        }
    }

    #[test]
    fn algorithm_profile_display() {
        assert_eq!(AlgorithmProfile::Unrestricted.to_string(), "Unrestricted");
        assert_eq!(AlgorithmProfile::Cnsa2.to_string(), "CNSA 2.0");
        assert_eq!(AlgorithmProfile::Cnsa1.to_string(), "CNSA 1.0");
    }

    #[test]
    fn algorithm_restricted_error_display() {
        let _guard = reset_for_test();
        initialize_with_profile(&[], AlgorithmProfile::Cnsa2).unwrap();
        let err = Error::AlgorithmRestricted {
            service: Service::Aes128Ecb,
        };
        let msg = err.to_string();
        assert!(msg.contains("AES-128-ECB"), "got: {msg}");
        assert!(msg.contains("CNSA 2.0"), "got: {msg}");
    }

    #[test]
    fn not_implemented_error_display() {
        let err = Error::NotImplemented;
        assert_eq!(err.to_string(), "algorithm not yet implemented");
    }

    #[test]
    fn service_display_is_stable() {
        // Pin a few representative Display strings.
        assert_eq!(Service::Sha256.to_string(), "SHA-256");
        assert_eq!(Service::Aes256Gcm.to_string(), "AES-256-GCM");
        assert_eq!(Service::MlKem1024Encaps.to_string(), "ML-KEM-1024 encaps");
        assert_eq!(Service::Ed25519Sign.to_string(), "Ed25519 sign");
    }
}