aph-core 0.1.0

APH (Agent per Human) protocol v0.1 — envelope wire types, mandates, notarization flow state machines, and signing helpers
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
//! Structural verification of an envelope's proof block — spec §7.1.11,
//! §7.1.7.1, §7.2.1 and §8.3.1 steps 1a, 1d, 1e.
//!
//! This module answers one question: *is this envelope's proof structure
//! well formed, and does it say what it is?* It is deliberately PURE — no
//! cryptography, no key resolution, no network, no clock. Signature checking
//! belongs to [`crate::crypto`]; key discovery belongs to
//! [`crate::discovery`]; deciding *which* mode a deployment demands belongs
//! to the caller.
//!
//! The reason these checks are separated out and made mandatory is stated
//! in §7.1.11: without them `attestationMode` is a **self-asserted string**.
//! A holder of a notary key could write `PrincipalSigned` above a single
//! notary proof whose `proofPurpose` is `assertionMethod` — indistinguishable
//! from a principal proof by purpose alone — and a verifier that trusted the
//! label would report a forged authorization as the human's own signature.
//! Every rule below exists to make the label unforgeable by a notary.
//!
//! Order of use, mirroring §8.3.1:
//!
//! 1. [`require_mode`] — step 1a. Refuse an envelope weaker than policy
//!    demands BEFORE doing any work on unauthenticated input.
//! 2. [`verify_proof_structure`] — steps 1a (label/structure agreement) and
//!    1e (chain linkage). Returns the mode the envelope is actually in.
//! 3. [`verify_embedded_mandate_binding`] — step 1d, the non-cryptographic
//!    half: is the embedded mandate THIS envelope's parent?
//! 4. [`verify_timestamp_order`] — the issuance order §7.2.1 makes normative.
//!
//! Timestamps are compared by parsing them with `chrono`, which is what
//! [`crate::delegation_mandate::DelegationMandate::is_valid_at`] and
//! [`crate::discovery::dns_txt`] already do, and every comparison fails
//! CLOSED on an unparseable value. No date library is introduced: `chrono`
//! is already a dependency of this crate for exactly this purpose.

/// `proofPurpose` of a principal proof, and of a lone notary proof kept for
/// wire compatibility (§7.1.11).
const ASSERTION_METHOD: &str = "assertionMethod";

/// `proofPurpose` of a notary countersignature — the value that distinguishes
/// the second proof of a chain from the first (§7.1.11).
const AUTHENTICATION: &str = "authentication";

/// Verifies the structural rules of §7.1.11 and returns the mode the
/// envelope is actually in.
///
/// This is the check that makes `attestationMode` mean something. It
/// enforces, each with its own `APH_E013` reason string:
///
/// - a chain carries EXACTLY two proofs;
/// - position 1 has `proofPurpose == "assertionMethod"`, position 2
///   `"authentication"`;
/// - both chain proofs carry an `id`, and the two ids differ;
/// - the notary proof's `previousProof` equals the principal proof's `id` —
///   array position is a hint, this linkage is the binding (§7.1.11);
/// - the principal proof carries NO `previousProof`; it is the chain head;
/// - label and structure agree in BOTH directions: a chain MUST be labelled
///   `PrincipalSigned`, and `PrincipalSigned` MUST be a chain. A mismatch is
///   a rejection, never a silent coercion;
/// - the principal proof's `verificationMethod` is a DID URL under
///   `credentialSubject.humanPrincipal.id`. This is the rule that closes the
///   forgery §7.1.11 describes, because the notary does not hold that key.
///
/// A single-object `proof` is a notary proof and yields `NotaryAttested` —
/// the shape all eight published example envelopes carry.
///
/// A successful return says the structure is sound; it says NOTHING about
/// whether either signature verifies. A caller that reports "the human
/// signed this" on the strength of this function alone is reporting a claim
/// no key has backed.
pub fn verify_proof_structure(
  envelope: &crate::envelope::NotarizationEnvelope,
) -> std::result::Result<crate::envelope::AttestationMode, crate::errors::AphError> {
  let declared = envelope.credential_subject.policy.attestation_mode;

  // ── The single-object form ────────────────────────────────────────────
  if !envelope.proof.is_chain() {
    if declared
      == std::option::Option::Some(crate::envelope::AttestationMode::PrincipalSigned)
    {
      return std::result::Result::Err(crate::errors::AphError::proof_chain_invalid(
        "`attestationMode` is `PrincipalSigned` but `proof` is a single object; \
         the label MUST accompany a two-element chain (§7.1.11)",
      ));
    }
    // A lone proof links to nothing, so chain members on it are not merely
    // redundant — they are a claim about a chain that does not exist. §7.1.11
    // says `id` is "omitted for a single-object `proof`" and a
    // `previousProof` here is dangling by construction. Accepting either
    // would let a stripped chain keep the vocabulary of one, and a verifier
    // reading loosely could conclude a principal proof had been present.
    let lone = match envelope.proof.notary() {
      std::option::Option::Some(proof) => proof,
      // A non-chain always has exactly one proof; refuse rather than index.
      std::option::Option::None => {
        return std::result::Result::Err(crate::errors::AphError::proof_chain_invalid(
          "`proof` is neither a chain nor a single proof",
        ));
      }
    };
    if lone.id.is_some() || lone.previous_proof.is_some() {
      return std::result::Result::Err(crate::errors::AphError::proof_chain_invalid(
        "a single-object `proof` MUST carry neither `id` nor `previousProof`; \
         both belong to a chain, and a lone proof links to nothing (§7.1.11)",
      ));
    }
    // §7.1.11: a lone proof "uses `assertionMethod` for wire compatibility".
    // `authentication` is the countersignature purpose and means a chain was
    // intended, so a lone proof carrying it is a chain missing its head.
    if lone.proof_purpose != ASSERTION_METHOD {
      return std::result::Result::Err(crate::errors::AphError::proof_chain_invalid(
        std::format!(
          "a single-object `proof` MUST use `proofPurpose` `{}`; found `{}` (§7.1.11)",
          ASSERTION_METHOD, lone.proof_purpose
        ),
      ));
    }
    return std::result::Result::Ok(crate::envelope::AttestationMode::NotaryAttested);
  }

  // ── The array form ────────────────────────────────────────────────────
  let proofs = envelope.proof.all();
  if proofs.len() != 2 {
    return std::result::Result::Err(crate::errors::AphError::proof_chain_invalid(
      std::format!(
        "a proof chain MUST carry exactly two proofs (principal, then notary); found {}",
        proofs.len()
      ),
    ));
  }

  // Label/structure agreement, the other direction: an array MUST be
  // labelled `PrincipalSigned`. Left unenforced, an intermediary could
  // present a chain as a notary attestation and the human's own proof would
  // be read as the notary's.
  match declared {
    std::option::Option::None => {
      return std::result::Result::Err(crate::errors::AphError::proof_chain_invalid(
        "`proof` is a chain but `attestationMode` is absent; absent means \
         `NotaryAttested`, and a chain MUST declare `PrincipalSigned` (§7.1.11)",
      ));
    }
    std::option::Option::Some(crate::envelope::AttestationMode::NotaryAttested) => {
      return std::result::Result::Err(crate::errors::AphError::proof_chain_invalid(
        "`proof` is a chain but `attestationMode` is `NotaryAttested`; \
         a chain MUST declare `PrincipalSigned` (§7.1.11)",
      ));
    }
    std::option::Option::Some(crate::envelope::AttestationMode::PrincipalSigned) => {}
  }

  let (principal, notary) = match (envelope.proof.principal(), envelope.proof.notary()) {
    (std::option::Option::Some(p), std::option::Option::Some(n)) => (p, n),
    // Unreachable while the accessors key off the same length just checked.
    // Written as a refusal rather than an `unwrap` so that a future change
    // to `EnvelopeProofs` fails closed instead of panicking on hostile input.
    _ => {
      return std::result::Result::Err(crate::errors::AphError::proof_chain_invalid(
        "`proof` is not a well-formed two-element chain",
      ));
    }
  };

  // ── Proof purposes, per position (§7.1.11) ────────────────────────────
  if principal.proof_purpose != ASSERTION_METHOD {
    return std::result::Result::Err(crate::errors::AphError::proof_chain_invalid(
      std::format!(
        "chain position 1 (principal proof) MUST have `proofPurpose` \
         `assertionMethod`; found `{}`",
        principal.proof_purpose
      ),
    ));
  }
  if notary.proof_purpose != AUTHENTICATION {
    return std::result::Result::Err(crate::errors::AphError::proof_chain_invalid(
      std::format!(
        "chain position 2 (notary proof) MUST have `proofPurpose` \
         `authentication`; found `{}`",
        notary.proof_purpose
      ),
    ));
  }

  // ── Proof ids ─────────────────────────────────────────────────────────
  let principal_id = match principal.id.as_deref() {
    std::option::Option::Some(id) => id,
    std::option::Option::None => {
      return std::result::Result::Err(crate::errors::AphError::proof_chain_invalid(
        "chain position 1 (principal proof) MUST carry an `id`; \
         without one the notary proof has nothing to name (§7.1.11)",
      ));
    }
  };
  let notary_id = match notary.id.as_deref() {
    std::option::Option::Some(id) => id,
    std::option::Option::None => {
      return std::result::Result::Err(crate::errors::AphError::proof_chain_invalid(
        "chain position 2 (notary proof) MUST carry an `id`; \
         every proof in a chain is identified (§7.1.11)",
      ));
    }
  };
  if principal_id == notary_id {
    return std::result::Result::Err(crate::errors::AphError::proof_chain_invalid(
      std::format!(
        "the two proofs in a chain MUST carry distinct `id`s; both are `{}`",
        principal_id
      ),
    ));
  }

  // ── Chain linkage (§8.3.1 step 1e) ────────────────────────────────────
  // Array position is a hint. `previousProof` is the binding, because a
  // verifier that trusted order alone would accept a chain an intermediary
  // reordered.
  if principal.previous_proof.is_some() {
    return std::result::Result::Err(crate::errors::AphError::proof_chain_invalid(
      "the principal proof is the HEAD of the chain and MUST NOT carry \
       `previousProof` (§7.1.11)",
    ));
  }
  let previous = match notary.previous_proof.as_deref() {
    std::option::Option::Some(previous) => previous,
    std::option::Option::None => {
      return std::result::Result::Err(crate::errors::AphError::proof_chain_invalid(
        "the notary proof MUST carry `previousProof` naming the principal \
         proof's `id`; a chain without linkage is only an ordered array",
      ));
    }
  };
  if previous == notary_id {
    return std::result::Result::Err(crate::errors::AphError::proof_chain_invalid(
      std::format!(
        "`previousProof` is self-referential: the notary proof names its own \
         `id` `{}`",
        notary_id
      ),
    ));
  }
  if previous != principal_id {
    return std::result::Result::Err(crate::errors::AphError::proof_chain_invalid(
      std::format!(
        "`previousProof` `{}` is dangling: it does not name a proof in this \
         chain (the principal proof's `id` is `{}`)",
        previous, principal_id
      ),
    ));
  }

  // ── The binding that makes the label unforgeable (§7.1.11) ────────────
  // A DID URL is `<did>#<fragment>`. The principal proof MUST be made under
  // the human principal's own DID; a proof made by any other key is not the
  // principal's proof, whatever its `proofPurpose` says (§8.3.1 step 1c).
  // Only this check stops a notary — who does not hold the human's key —
  // from writing `PrincipalSigned` above a proof of its own.
  let human_did = envelope.credential_subject.human_principal.id.as_str();
  let principal_did = match principal.verification_method.split_once('#') {
    std::option::Option::Some((did, _fragment)) => did,
    std::option::Option::None => principal.verification_method.as_str(),
  };
  if principal_did != human_did {
    return std::result::Result::Err(crate::errors::AphError::proof_chain_invalid(
      std::format!(
        "the principal proof's `verificationMethod` `{}` is not a DID URL under \
         the human principal `{}`; a proof made by any other key is not the \
         principal's proof (§7.1.11)",
        principal.verification_method, human_did
      ),
    ));
  }

  std::result::Result::Ok(crate::envelope::AttestationMode::PrincipalSigned)
}

/// Refuses an envelope weaker than the caller's policy demands (§8.3.1 1a).
///
/// `PrincipalSigned` required against a `NotaryAttested` envelope is
/// `APH_E012`. The reverse is fine: a stronger envelope satisfies a weaker
/// policy. There is no silent downgrade, for the same reason §8.4.6 forbids
/// downgrading key discovery — an attacker who can defeat the weak path will
/// always present the weak path.
///
/// This reads the DECLARED label, and deliberately so: §8.3.1 puts it at
/// step 1a precisely so a verifier refuses a too-weak claim *before* doing
/// work on unauthenticated input. The label is not evidence on its own, so a
/// caller MUST also call [`verify_proof_structure`], which is what rejects a
/// `PrincipalSigned` label written above a structure that does not support
/// it. Calling this function alone accepts a forged label.
pub fn require_mode(
  envelope: &crate::envelope::NotarizationEnvelope,
  required: crate::envelope::AttestationMode,
) -> std::result::Result<(), crate::errors::AphError> {
  let actual = envelope
    .credential_subject
    .policy
    .effective_attestation_mode();
  match (required, actual) {
    (
      crate::envelope::AttestationMode::PrincipalSigned,
      crate::envelope::AttestationMode::NotaryAttested,
    ) => std::result::Result::Err(crate::errors::AphError::attestation_mode_refused(
      required.label(),
      actual.label(),
    )),
    _ => std::result::Result::Ok(()),
  }
}

/// Binds an embedded Delegation Mandate to the envelope carrying it
/// (§7.1.7.1 steps 3-4, §8.3.1 step 1d). Signature checking is NOT here.
///
/// **Why this exists.** `policy.delegationMandateId` names the parent
/// mandate by id only, and an id is not verifiable. Embedding the whole
/// mandate lets a recipient check the human's `principalSignature` offline —
/// but a valid signature over *some* grant proves nothing about *this*
/// message. Without the equalities below, an attacker could staple any
/// validly-signed mandate to any envelope and the notary's word would again
/// be the only thing linking the human to the send. Closing that hole is the
/// entire purpose of §7.1.7.1.
///
/// Checks, in order:
///
/// - `mandate.humanPrincipalDid == credentialSubject.humanPrincipal.id`
/// - `mandate.agentDid == credentialSubject.agent.id`
/// - `mandate.id == policy.delegationMandateId`, when that field is present
/// - `channel.kind` is in `mandate.allowedChannels` (else `APH_E005`)
/// - the envelope's `validFrom` AND `validUntil` fall inside the mandate's
///   window (else `APH_E003`)
///
/// The three identity equalities report `APH_E011`: the embedded mandate
/// carries a principal signature that does not authorize THIS envelope, so
/// no valid signature by this human covers this authorization. §11 scopes
/// `APH_E011` to step 1d, and the two codes §8.3.1 names explicitly for that
/// step (`APH_E005`, `APH_E003`) cover only the scope and window failures.
///
/// A `None` embedded mandate is `Ok(())`. Absence is not a structural
/// defect: §7.1.7.1 makes embedding SHOULD, not MUST, because an embedded
/// mandate discloses the human's entire standing grant to every recipient.
/// What absence means is that the human's authorization is NOT verifiable by
/// this recipient, who should then treat the credential as the notary's
/// assertion alone — a policy decision, and the caller's to make.
///
/// **Bounds.** The embedded mandate parses under the same
/// `deny_unknown_fields` rule as the envelope (§7.1.7.1), so a mandate
/// carrying an unknown field never reaches this function. Rejecting an
/// oversized envelope before canonicalizing it remains the caller's job.
pub fn verify_embedded_mandate_binding(
  envelope: &crate::envelope::NotarizationEnvelope,
) -> std::result::Result<(), crate::errors::AphError> {
  let subject = &envelope.credential_subject;
  let mandate = match subject.policy.delegation_mandate.as_ref() {
    std::option::Option::Some(mandate) => mandate,
    std::option::Option::None => return std::result::Result::Ok(()),
  };

  if mandate.human_principal_did != subject.human_principal.id {
    return std::result::Result::Err(crate::errors::AphError::PrincipalSignatureInvalid);
  }
  if mandate.agent_did != subject.agent.id {
    return std::result::Result::Err(crate::errors::AphError::PrincipalSignatureInvalid);
  }
  if let std::option::Option::Some(declared_id) = subject.policy.delegation_mandate_id.as_deref() {
    if mandate.id != declared_id {
      return std::result::Result::Err(crate::errors::AphError::PrincipalSignatureInvalid);
    }
  }

  // §7.1.7.1 is explicit that a Delegation Mandate constrains channel, rate
  // and time and NOTHING else — it cannot express a recipient allow-list or
  // a content class, so this is a channel-and-window check and must not be
  // described as more.
  if !mandate.allows_channel(subject.channel.kind) {
    return std::result::Result::Err(crate::errors::AphError::channel_not_allowed(
      subject.channel.kind.label(),
    ));
  }

  // RFC 0005: when the grant constrains WHO consumes, the envelope must
  // declare a class inside it. An envelope declaring NOTHING under a
  // constrained grant refuses too — a constraint escapable by omission
  // would teach every over-broad agent the same trick, and the party this
  // check disciplines is the principal's own honest agent.
  if let std::option::Option::Some(allowed) = &mandate.allowed_recipient_classes {
    let allowed_labels = allowed
      .iter()
      .map(crate::envelope::RecipientClass::label)
      .collect::<std::vec::Vec<&'static str>>()
      .join(", ");
    match envelope.credential_subject.channel.recipient_class {
      std::option::Option::Some(declared) if allowed.contains(&declared) => {}
      std::option::Option::Some(declared) => {
        return std::result::Result::Err(crate::errors::AphError::recipient_class_not_allowed(
          declared.label(),
          &mandate.id,
          allowed_labels,
        ));
      }
      std::option::Option::None => {
        return std::result::Result::Err(crate::errors::AphError::recipient_class_not_allowed(
          "nothing",
          &mandate.id,
          allowed_labels,
        ));
      }
    }
  }

  // `is_valid_at` is the crate's existing RFC 3339 window comparison: it
  // parses with `chrono` and returns false on anything unparseable, so a
  // garbage timestamp denies rather than defaulting to valid. Both endpoints
  // are checked because §8.3.1 step 1d requires the envelope's WINDOW to
  // fall inside the mandate's, not merely its start.
  if !mandate.is_valid_at(&envelope.valid_from) || !mandate.is_valid_at(&envelope.valid_until) {
    return std::result::Result::Err(crate::errors::AphError::mandate_expired(
      mandate.id.as_str(),
    ));
  }

  std::result::Result::Ok(())
}

/// Pins the issuance order §7.2.1 makes normative:
/// `decisionTimestamp <= principal.created <= notary.created`.
///
/// The order is not stylistic. The notary prepares the complete envelope
/// (including its own `notarization` metadata), THEN the principal signs
/// what was prepared, THEN the notary countersigns the result. Reverse the
/// first two and the principal would have to sign notary-produced fields
/// that do not yet exist — the circularity the §7.2.1 canonicalization bases
/// are written to avoid. §7.2.1 states outright that a verifier MUST NOT
/// accept a chain whose notary proof is dated before the principal proof it
/// claims to have observed.
///
/// Out-of-order is `APH_E013`. Applies only to a chain: a single proof has
/// nothing to order, so a single-proof envelope returns `Ok(())`.
///
/// Timestamps are parsed with `chrono`, matching
/// [`crate::delegation_mandate::DelegationMandate::is_valid_at`] rather than
/// comparing strings, so an offset written as `+00:00` orders correctly
/// against the same instant written as `Z`. An unparseable timestamp is
/// `APH_E013`: this fails CLOSED, because an unordered chain and an
/// unreadable one are equally unacceptable as evidence.
pub fn verify_timestamp_order(
  envelope: &crate::envelope::NotarizationEnvelope,
) -> std::result::Result<(), crate::errors::AphError> {
  if !envelope.proof.is_chain() {
    return std::result::Result::Ok(());
  }
  let (principal, notary) = match (envelope.proof.principal(), envelope.proof.notary()) {
    (std::option::Option::Some(p), std::option::Option::Some(n)) => (p, n),
    _ => {
      return std::result::Result::Err(crate::errors::AphError::proof_chain_invalid(
        "cannot check issuance order: `proof` is not a well-formed two-element chain",
      ));
    }
  };

  let decision = parse_rfc3339(
    &envelope.credential_subject.notarization.decision_timestamp,
    "credentialSubject.notarization.decisionTimestamp",
  )?;
  let principal_created = parse_rfc3339(&principal.created, "the principal proof's `created`")?;
  let notary_created = parse_rfc3339(&notary.created, "the notary proof's `created`")?;

  if principal_created < decision {
    return std::result::Result::Err(crate::errors::AphError::proof_chain_invalid(
      std::format!(
        "the principal proof's `created` `{}` precedes \
         `notarization.decisionTimestamp` `{}`; the principal signs the \
         envelope the notary has already prepared (§7.2.1)",
        principal.created, envelope.credential_subject.notarization.decision_timestamp
      ),
    ));
  }
  if notary_created < principal_created {
    return std::result::Result::Err(crate::errors::AphError::proof_chain_invalid(
      std::format!(
        "the notary proof's `created` `{}` precedes the principal proof's \
         `created` `{}`; a countersignature cannot predate the proof it \
         claims to have observed (§7.2.1)",
        notary.created, principal.created
      ),
    ));
  }

  std::result::Result::Ok(())
}

/// §8.3's audience step (RFC 0003 §1): when the envelope names who may
/// accept it, this verifier had better be that party.
///
/// The semantics, in refusal order:
/// - `audience` ABSENT: admitted. Absence is the producer's decision to
///   issue a bearer credential, never an oversight the verifier forgives.
/// - `audience.id` differs from `verifier_id`: refused, `APH_E017`.
/// - `audience.channelBinding` present: every member it names — `kind`
///   included — must equal the same-named member of `act_coordinates`, the
///   delivery coordinates of the act this verifier is being asked to
///   perform. A member the act's coordinates lack is a MISMATCH, and
///   `act_coordinates` being `None` entirely is a mismatch too: a verifier
///   that cannot determine the act's coordinates MUST reject rather than
///   skip, exactly as one that cannot determine its own identity must —
///   which is why `verifier_id` is a required parameter rather than an
///   `Option` this function politely tolerates.
///
/// A comparison, not cryptography: nothing here checks a signature, and
/// the caller runs this AFTER signature verification (§8.3 ordering).
pub fn verify_audience(
  envelope: &crate::envelope::NotarizationEnvelope,
  verifier_id: &str,
  act_coordinates: std::option::Option<&crate::envelope::AudienceChannelBinding>,
) -> std::result::Result<(), crate::errors::AphError> {
  let audience = match &envelope.credential_subject.audience {
    std::option::Option::None => return std::result::Result::Ok(()),
    std::option::Option::Some(audience) => audience,
  };
  if audience.id != verifier_id {
    return std::result::Result::Err(crate::errors::AphError::audience_mismatch(
      &audience.id,
      verifier_id,
    ));
  }
  let binding = match &audience.channel_binding {
    std::option::Option::None => return std::result::Result::Ok(()),
    std::option::Option::Some(binding) => binding,
  };
  let act = match act_coordinates {
    std::option::Option::Some(act) => act,
    std::option::Option::None => {
      // Reject-rather-than-skip: a binding nobody can check is refused,
      // not waved through, because a check a verifier may skip when
      // inconvenient is not a check.
      return std::result::Result::Err(crate::errors::AphError::audience_mismatch(
        std::format!("{} (channelBinding present)", audience.id),
        std::format!("{} (act delivery coordinates unknown to this verifier)", verifier_id),
      ));
    }
  };
  if binding.kind != act.kind {
    return std::result::Result::Err(crate::errors::AphError::audience_mismatch(
      std::format!("{} (channelBinding kind: `{}`)", audience.id, binding.kind.label()),
      std::format!("{} (act kind: `{}`)", verifier_id, act.kind.label()),
    ));
  }
  for (member, bound) in &binding.coordinates {
    match act.coordinates.get(member) {
      std::option::Option::Some(actual) if actual == bound => {}
      std::option::Option::Some(actual) => {
        return std::result::Result::Err(crate::errors::AphError::audience_mismatch(
          std::format!("{} (channelBinding {}: `{}`)", audience.id, member, bound),
          std::format!("{} (act {}: `{}`)", verifier_id, member, actual),
        ));
      }
      std::option::Option::None => {
        return std::result::Result::Err(crate::errors::AphError::audience_mismatch(
          std::format!("{} (channelBinding {}: `{}`)", audience.id, member, bound),
          std::format!("{} (act has no `{}` coordinate)", verifier_id, member),
        ));
      }
    }
  }
  std::result::Result::Ok(())
}

/// The envelope's OWN validity window, judged against a clock the CALLER
/// supplies (RFC 0003's `APH_E019`; §8.3 step 2's check finally has its
/// code). Distinct from anything mandate-shaped: `APH_E003` is a mandate
/// consulted past its expiry, and routing an expired ENVELOPE there was
/// the miscite this crate's own tests nearly shipped.
///
/// `observed_at` is a parameter, not a syscall: verification stays a pure
/// function of its inputs, testable at any instant, and the caller states
/// which clock it trusts. Every timestamp unparseable → refused with the
/// same code, fail-closed — an unreadable window never admits.
pub fn verify_envelope_window(
  envelope: &crate::envelope::NotarizationEnvelope,
  observed_at: &str,
) -> std::result::Result<(), crate::errors::AphError> {
  let window_invalid = || {
    crate::errors::AphError::envelope_window_invalid(
      &envelope.valid_from,
      &envelope.valid_until,
      observed_at,
    )
  };
  let from = chrono::DateTime::parse_from_rfc3339(&envelope.valid_from)
    .map_err(|_| window_invalid())?;
  let until = chrono::DateTime::parse_from_rfc3339(&envelope.valid_until)
    .map_err(|_| window_invalid())?;
  let at = chrono::DateTime::parse_from_rfc3339(observed_at).map_err(|_| window_invalid())?;
  if at < from || at > until {
    return std::result::Result::Err(window_invalid());
  }
  std::result::Result::Ok(())
}

/// The single-use record §8.3's final step obliges a recipient to keep
/// (RFC 0003 §2): `id` is spent at ACCEPTANCE, and a second presentation
/// of the same `id` is refused with `APH_E018`.
///
/// The protocol states the obligation, not the storage — a single process
/// may use a set, a cluster needs shared state, a notary MAY offer a burn
/// endpoint — so this is a port. The retention contract an implementor
/// signs up for: an id MUST be retained at least until its envelope's
/// `validUntil` has passed, and MAY be discarded thereafter, because
/// [`verify_envelope_window`] already refuses the envelope from then on.
pub trait ConsumedEnvelopeLedger {
  /// Records `envelope_id` as spent iff it was not already; returns whether
  /// this call was the one that spent it. One atomic question, because
  /// separate "seen?" and "record!" calls are a check-then-act race — two
  /// concurrent presentations would both pass the check.
  fn spend(&mut self, envelope_id: &str) -> bool;
}

/// The single-process ledger: a set. Exists so the obligation is testable
/// here and so a caller with one process needs no ceremony; anything
/// distributed brings its own implementation of the trait.
#[derive(std::fmt::Debug, std::default::Default)]
pub struct InMemoryConsumedEnvelopeLedger {
  spent: std::collections::BTreeSet<String>,
}

impl ConsumedEnvelopeLedger for InMemoryConsumedEnvelopeLedger {
  fn spend(&mut self, envelope_id: &str) -> bool {
    self.spent.insert(envelope_id.to_string())
  }
}

/// §8.3's single-use step: call this AT THE MOMENT the verifier commits to
/// the act — acceptance, in the RFC 5321 §6.1 sense — after every other
/// check has passed. Calling it earlier consumes the envelope on a
/// presentation that some LATER check may still refuse, and RFC 0003 is
/// explicit that a verifier which rejects for any other reason has not
/// consumed the id.
pub fn enforce_single_use(
  ledger: &mut dyn ConsumedEnvelopeLedger,
  envelope: &crate::envelope::NotarizationEnvelope,
) -> std::result::Result<(), crate::errors::AphError> {
  if ledger.spend(&envelope.id) {
    std::result::Result::Ok(())
  } else {
    std::result::Result::Err(crate::errors::AphError::envelope_already_spent(&envelope.id))
  }
}

/// Parses an RFC 3339 timestamp, reporting `APH_E013` and naming the field
/// when it cannot be read. Fails closed: an unreadable timestamp is never
/// treated as satisfying an ordering constraint.
fn parse_rfc3339(
  value: &str,
  field: &str,
) -> std::result::Result<
  chrono::DateTime<chrono::FixedOffset>,
  crate::errors::AphError,
> {
  chrono::DateTime::parse_from_rfc3339(value).map_err(|_| {
    crate::errors::AphError::proof_chain_invalid(std::format!(
      "{} `{}` is not a parseable RFC 3339 timestamp, so issuance order \
       cannot be checked",
      field, value
    ))
  })
}

#[cfg(test)]
mod tests {
  // ── Fixtures ──────────────────────────────────────────────────────────
  //
  // The human principal's DID appears in three places that MUST agree —
  // `humanPrincipal.id`, the principal proof's `verificationMethod`, and an
  // embedded mandate's `humanPrincipalDid` — so it is named once here. A
  // test that needs them to disagree changes one copy explicitly, which is
  // what makes the disagreement visible in the test body.
  const HUMAN_DID: &str = "did:key:z6MkfAkfRZ3v9zJWh9LM2YQbWLh6hqGYDVxxC7ueoVcd5dGy";
  const NOTARY_DID: &str = "did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSdoVTwBaPaeT1KhFmkV";
  const AGENT_DID: &str = "did:web:agent.squillo.com";
  const PRINCIPAL_PROOF_ID: &str = "urn:uuid:00000000-0000-4000-8000-0000000000a1";
  const NOTARY_PROOF_ID: &str = "urn:uuid:00000000-0000-4000-8000-0000000000b1";
  const MANDATE_ID: &str = "urn:uuid:00000000-0000-4000-8000-0000000000c1";

  fn notary_proof() -> crate::envelope::EnvelopeProof {
    crate::envelope::EnvelopeProof {
      r#type: std::string::String::from("DataIntegrityProof"),
      cryptosuite: std::option::Option::Some(std::string::String::from("eddsa-jcs-2022")),
      verification_method: std::format!("{}#{}", NOTARY_DID, "z6MkpTHR8VNsBxYAAWHut2Geadd9jSdoVTwBaPaeT1KhFmkV"),
      created: std::string::String::from("2026-05-21T00:00:01Z"),
      proof_purpose: std::string::String::from("assertionMethod"),
      proof_value: std::string::String::from("z-illustrative-lone-notary-proof"),
      id: std::option::Option::None,
      previous_proof: std::option::Option::None,
    }
  }

  fn principal_proof() -> crate::envelope::EnvelopeProof {
    crate::envelope::EnvelopeProof {
      r#type: std::string::String::from("DataIntegrityProof"),
      cryptosuite: std::option::Option::Some(std::string::String::from("eddsa-jcs-2022")),
      verification_method: std::format!("{}#{}", HUMAN_DID, "z6MkfAkfRZ3v9zJWh9LM2YQbWLh6hqGYDVxxC7ueoVcd5dGy"),
      created: std::string::String::from("2026-05-21T00:00:02Z"),
      proof_purpose: std::string::String::from("assertionMethod"),
      proof_value: std::string::String::from("z-illustrative-principal-proof"),
      id: std::option::Option::Some(std::string::String::from(PRINCIPAL_PROOF_ID)),
      previous_proof: std::option::Option::None,
    }
  }

  fn countersignature() -> crate::envelope::EnvelopeProof {
    crate::envelope::EnvelopeProof {
      r#type: std::string::String::from("DataIntegrityProof"),
      cryptosuite: std::option::Option::Some(std::string::String::from("eddsa-jcs-2022")),
      verification_method: std::format!("{}#{}", NOTARY_DID, "z6MkpTHR8VNsBxYAAWHut2Geadd9jSdoVTwBaPaeT1KhFmkV"),
      created: std::string::String::from("2026-05-21T00:00:03Z"),
      proof_purpose: std::string::String::from("authentication"),
      proof_value: std::string::String::from("z-illustrative-countersignature"),
      id: std::option::Option::Some(std::string::String::from(NOTARY_PROOF_ID)),
      previous_proof: std::option::Option::Some(std::string::String::from(PRINCIPAL_PROOF_ID)),
    }
  }

  fn mandate() -> crate::delegation_mandate::DelegationMandate {
    crate::delegation_mandate::DelegationMandate {
      id: std::string::String::from(MANDATE_ID),
      human_principal_did: std::string::String::from(HUMAN_DID),
      agent_did: std::string::String::from(AGENT_DID),
      allowed_channels: std::vec![crate::envelope::ChannelKind::Slack, crate::envelope::ChannelKind::Email],
      allowed_recipient_classes: std::option::Option::None,
      rate_limit_per_hour: std::option::Option::Some(12),
      valid_from: std::string::String::from("2026-05-01T00:00:00Z"),
      valid_until: std::string::String::from("2026-06-01T00:00:00Z"),
      principal_signature: std::string::String::from("z-illustrative-principal-signature"),
      notary_signature: std::string::String::from("z-illustrative-notary-signature"),
    }
  }

  /// A `NotaryAttested` envelope in exactly the shape all eight published
  /// examples carry: one proof object, no `attestationMode`.
  fn single_proof_envelope() -> crate::envelope::NotarizationEnvelope {
    crate::envelope::NotarizationEnvelope {
      aph_version: std::string::String::from("0.1"),
      context: std::vec![
        std::string::String::from("https://www.w3.org/ns/credentials/v2"),
        std::string::String::from("https://w3id.org/aph/v1"),
      ],
      r#type: std::vec![
        std::string::String::from("VerifiableCredential"),
        std::string::String::from("AgentSendAuthorizationCredential"),
      ],
      id: std::string::String::from("urn:uuid:00000000-0000-4000-8000-000000000001"),
      issuer: std::string::String::from(NOTARY_DID),
      valid_from: std::string::String::from("2026-05-21T00:00:00Z"),
      valid_until: std::string::String::from("2026-05-22T00:00:00Z"),
      credential_subject: crate::envelope::CredentialSubject {
        human_principal: crate::envelope::HumanPrincipalRef {
          id: std::string::String::from(HUMAN_DID),
          display_name: std::string::String::from("Scott Wyatt"),
        },
        agent: crate::envelope::AgentRef {
          id: std::string::String::from(AGENT_DID),
          agent_card_uri: std::option::Option::None,
          display_name: std::string::String::from("Squillo Concierge"),
          version: std::string::String::from("1.0"),
        },
        channel: crate::envelope::ChannelDescriptor {
          kind: crate::envelope::ChannelKind::Slack,
          recipient_addressing: serde_json::json!({"teamId": "T01234567"}),
          recipient_class: std::option::Option::None,
        },
        communication: crate::envelope::CommunicationDescriptor {
          content_class: crate::envelope::ContentClass::Reply,
          body_sha256: std::string::String::from(
            "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
          ),
          body_size: 1842,
          preview_lines: 3,
          preview: std::string::String::from("hello world"),
        },
        policy: crate::envelope::PolicyDescriptor {
          decision: crate::envelope::PolicyDecision::AskEveryTime,
          matched_scope: std::string::String::from("per-channel"),
          delegation_mandate_id: std::option::Option::None,
          act_chain: std::vec::Vec::new(),
          attestation_mode: std::option::Option::None,
          delegation_mandate: std::option::Option::None,
        },
        notarization: crate::envelope::NotarizationMetadata {
          notary_service: crate::envelope::NotaryServiceRef {
            id: std::string::String::from("did:web:notary.squillo.com"),
            name: std::string::String::from("Squillo Notary Service"),
            version: std::string::String::from("0.1.0"),
            attested_digest: std::option::Option::None,
            attestation_uri: std::option::Option::None,
          },
          decision_timestamp: std::string::String::from("2026-05-21T00:00:01Z"),
          decision_latency_ms: 1834,
        },
        apple_aur_acceptance: std::option::Option::None,
        audience: std::option::Option::None,
        act_classification: std::option::Option::None,
      },
      linked_mandate: std::option::Option::None,
      // Pattern A (§7.1.1): absent here means NO `credentialStatus` key on
      // the wire, which is what keeps this fixture byte-identical to the
      // pre-revocation shape its signatures were made over.
      credential_status: std::option::Option::None,
      proof: crate::envelope::EnvelopeProofs::Single(notary_proof()),
    }
  }

  /// A well-formed `PrincipalSigned` envelope: two linked proofs, the label
  /// that matches, and the principal proof made under the human's own DID.
  fn chain_envelope() -> crate::envelope::NotarizationEnvelope {
    let mut envelope = single_proof_envelope();
    envelope.credential_subject.policy.attestation_mode =
      std::option::Option::Some(crate::envelope::AttestationMode::PrincipalSigned);
    envelope.proof =
      crate::envelope::EnvelopeProofs::Chain(std::vec![principal_proof(), countersignature()]);
    envelope
  }

  /// Builds a chain envelope after letting the caller mutate either proof —
  /// the shape every rejection test below needs.
  fn chain_envelope_with(
    mutate: impl std::ops::FnOnce(
      &mut crate::envelope::EnvelopeProof,
      &mut crate::envelope::EnvelopeProof,
    ),
  ) -> crate::envelope::NotarizationEnvelope {
    let mut principal = principal_proof();
    let mut notary = countersignature();
    mutate(&mut principal, &mut notary);
    let mut envelope = chain_envelope();
    envelope.proof = crate::envelope::EnvelopeProofs::Chain(std::vec![principal, notary]);
    envelope
  }

  // ── verify_proof_structure: positive cases ────────────────────────────

  #[test]
  fn single_proof_without_a_label_verifies_as_notary_attested() {
    // This is the shape of ALL EIGHT published example envelopes and every
    // envelope any notary signed before `attestationMode` existed. If it
    // failed, or resolved to `PrincipalSigned`, the implementation would
    // either reject the entire deployed corpus or silently promote it to a
    // claim no human ever made.
    let mode = super::verify_proof_structure(&single_proof_envelope())
      .expect("a lone notary proof is a well-formed NotaryAttested envelope");
    std::assert_eq!(mode, crate::envelope::AttestationMode::NotaryAttested);
  }

  #[test]
  fn a_lone_proof_carrying_chain_members_is_rejected() {
    // `id` and `previousProof` are chain vocabulary. On a lone proof they
    // describe a chain that is not there, which is exactly what a stripped
    // chain looks like: an attacker removes the countersignature and the
    // remainder still speaks as though a principal proof were present. A
    // verifier reading loosely could conclude the human had signed.
    let mut envelope = single_proof_envelope();
    if let crate::envelope::EnvelopeProofs::Single(proof) = &mut envelope.proof {
      proof.previous_proof =
        std::option::Option::Some(std::string::String::from("urn:uuid:00000000-0000-4000-8000-0000000000f1"));
    }
    let err = super::verify_proof_structure(&envelope)
      .expect_err("a lone proof naming a previous proof is not well formed");
    std::assert_eq!(err.code(), "APH_E013");
  }

  #[test]
  fn a_lone_proof_using_the_countersignature_purpose_is_rejected() {
    // `authentication` is the purpose of the SECOND proof of a chain. A lone
    // proof carrying it is a chain missing its head — the principal proof
    // that was the entire authorization. Accepting it would admit an
    // envelope whose own proof block says a countersignature is all that
    // remains.
    let mut envelope = single_proof_envelope();
    if let crate::envelope::EnvelopeProofs::Single(proof) = &mut envelope.proof {
      proof.proof_purpose = std::string::String::from("authentication");
    }
    let err = super::verify_proof_structure(&envelope)
      .expect_err("a lone proof may not use the countersignature purpose");
    std::assert_eq!(err.code(), "APH_E013");
  }

  #[test]
  fn single_proof_labelled_notary_attested_verifies() {
    // A producer MAY state the weaker mode explicitly rather than relying on
    // absence. Rejecting the explicit spelling would punish the more honest
    // producer and push everyone toward the ambiguous form.
    let mut envelope = single_proof_envelope();
    envelope.credential_subject.policy.attestation_mode =
      std::option::Option::Some(crate::envelope::AttestationMode::NotaryAttested);
    let mode = super::verify_proof_structure(&envelope)
      .expect("an explicitly-labelled NotaryAttested envelope is well formed");
    std::assert_eq!(mode, crate::envelope::AttestationMode::NotaryAttested);
  }

  #[test]
  fn well_formed_chain_verifies_as_principal_signed() {
    // The positive control for the whole §7.1.11 rule set. Every rejection
    // test below mutates exactly one field of this envelope, so if this case
    // failed they would all pass for the wrong reason and the suite would
    // prove nothing.
    let mode = super::verify_proof_structure(&chain_envelope())
      .expect("a correctly linked and labelled chain must verify");
    std::assert_eq!(mode, crate::envelope::AttestationMode::PrincipalSigned);
  }

  #[test]
  fn principal_verification_method_without_a_fragment_is_accepted() {
    // A DID URL is `<did>#<fragment>`, but the fragment is what selects a
    // key, not what identifies the principal. A bare DID equal to
    // `humanPrincipal.id` still binds the proof to the right human, so
    // rejecting it would refuse a conformant producer on a formatting
    // preference rather than a security property.
    let envelope = chain_envelope_with(|principal, _notary| {
      principal.verification_method = std::string::String::from(HUMAN_DID);
    });
    std::assert!(super::verify_proof_structure(&envelope).is_ok());
  }

  // ── verify_proof_structure: chain length ──────────────────────────────

  #[test]
  fn a_one_element_chain_is_rejected() {
    // §7.2.1's stripped-proof attack lands here. An intermediary that
    // removes the notary proof from a PrincipalSigned envelope leaves a
    // one-element ARRAY, and because the principal's signature covers the
    // array form it would still verify. Rejecting length 1 is what stops the
    // human's own proof being re-presented as a notary attestation.
    let mut envelope = chain_envelope();
    envelope.proof = crate::envelope::EnvelopeProofs::Chain(std::vec![principal_proof()]);
    let err = super::verify_proof_structure(&envelope)
      .expect_err("a one-element chain must be rejected");
    std::assert_eq!(err.code(), "APH_E013");
  }

  #[test]
  fn a_three_element_chain_is_rejected() {
    // §7.1.11 constrains the chain to exactly two ROLES. A third proof has
    // no defined role, no canonicalization base (§7.2.1 defines two), and no
    // verification order — accepting it would mean verifying a signature
    // whose covered bytes are undefined.
    let mut envelope = chain_envelope();
    envelope.proof = crate::envelope::EnvelopeProofs::Chain(std::vec![
      principal_proof(),
      countersignature(),
      countersignature(),
    ]);
    let err = super::verify_proof_structure(&envelope)
      .expect_err("a three-element chain must be rejected");
    std::assert_eq!(err.code(), "APH_E013");
  }

  #[test]
  fn an_empty_chain_is_rejected() {
    // An empty array is a `proof` member that proves nothing while still
    // satisfying "the proof field is present". Without this rule an envelope
    // with no signatures at all would reach the signature-verification step
    // and pass it vacuously, having verified zero proofs.
    let mut envelope = chain_envelope();
    envelope.proof = crate::envelope::EnvelopeProofs::Chain(std::vec::Vec::new());
    let err =
      super::verify_proof_structure(&envelope).expect_err("an empty chain must be rejected");
    std::assert_eq!(err.code(), "APH_E013");
  }

  // ── verify_proof_structure: label/structure agreement ─────────────────

  #[test]
  fn a_chain_with_no_attestation_mode_is_rejected() {
    // Absence means `NotaryAttested` (§7.1.7), and a NotaryAttested envelope
    // has no principal proof. An unlabelled chain therefore contradicts
    // itself, and §7.1.11 requires rejection rather than inferring the
    // stronger mode from the structure — the label must be asserted, so that
    // its absence can never be read as a claim.
    let mut envelope = chain_envelope();
    envelope.credential_subject.policy.attestation_mode = std::option::Option::None;
    let err = super::verify_proof_structure(&envelope)
      .expect_err("an unlabelled chain must be rejected");
    std::assert_eq!(err.code(), "APH_E013");
  }

  #[test]
  fn a_chain_labelled_notary_attested_is_rejected() {
    // The mismatch must be a rejection, never a silent coercion to the
    // weaker mode: an envelope carrying the human's own proof but read as
    // the notary's assertion would understate the evidence, and a verifier
    // that quietly rewrote the label would hide a producer bug that could
    // just as easily have gone the other way.
    let mut envelope = chain_envelope();
    envelope.credential_subject.policy.attestation_mode =
      std::option::Option::Some(crate::envelope::AttestationMode::NotaryAttested);
    let err = super::verify_proof_structure(&envelope)
      .expect_err("a chain labelled NotaryAttested must be rejected");
    std::assert_eq!(err.code(), "APH_E013");
  }

  #[test]
  fn a_single_proof_labelled_principal_signed_is_rejected() {
    // THE attack §7.1.11 names outright. A holder of a notary key can write
    // `PrincipalSigned` above a single notary proof whose `proofPurpose` is
    // `assertionMethod` — indistinguishable from a principal proof by purpose
    // alone. A verifier that trusted the label would report a forged
    // authorization as the human's own signature.
    let mut envelope = single_proof_envelope();
    envelope.credential_subject.policy.attestation_mode =
      std::option::Option::Some(crate::envelope::AttestationMode::PrincipalSigned);
    let err = super::verify_proof_structure(&envelope)
      .expect_err("a PrincipalSigned label over a single proof must be rejected");
    std::assert_eq!(err.code(), "APH_E013");
  }

  // ── verify_proof_structure: proof purposes ────────────────────────────

  #[test]
  fn a_principal_proof_with_the_wrong_purpose_is_rejected() {
    // Position 1 asserts the authorization, so its purpose is
    // `assertionMethod` (§7.1.11). A head proof carrying `authentication`
    // describes a countersignature, and a chain of two countersignatures
    // contains no authorization at all.
    let envelope = chain_envelope_with(|principal, _notary| {
      principal.proof_purpose = std::string::String::from("authentication");
    });
    let err = super::verify_proof_structure(&envelope)
      .expect_err("principal proof must carry assertionMethod");
    std::assert_eq!(err.code(), "APH_E013");
  }

  #[test]
  fn a_notary_proof_with_the_wrong_purpose_is_rejected() {
    // Position 2 is a COUNTERSIGNATURE — `authentication`, not
    // `assertionMethod`. A notary proof claiming `assertionMethod` claims to
    // be making the assertion rather than witnessing it, which is precisely
    // the substitution the chain exists to prevent.
    let envelope = chain_envelope_with(|_principal, notary| {
      notary.proof_purpose = std::string::String::from("assertionMethod");
    });
    let err = super::verify_proof_structure(&envelope)
      .expect_err("notary proof must carry authentication");
    std::assert_eq!(err.code(), "APH_E013");
  }

  // ── verify_proof_structure: proof ids ─────────────────────────────────

  #[test]
  fn a_principal_proof_without_an_id_is_rejected() {
    // The `id` is what `previousProof` names. Without it the chain can only
    // be read positionally, and §7.1.11 says a verifier that trusted order
    // alone would accept a chain an intermediary reordered.
    let envelope = chain_envelope_with(|principal, notary| {
      principal.id = std::option::Option::None;
      notary.previous_proof = std::option::Option::None;
    });
    let err = super::verify_proof_structure(&envelope)
      .expect_err("a chain proof without an id must be rejected");
    std::assert_eq!(err.code(), "APH_E013");
  }

  #[test]
  fn a_notary_proof_without_an_id_is_rejected() {
    // §7.1.11 requires an `id` on EVERY proof in a chain, not only on the
    // one that is named. Enforcing it on the head alone would leave the
    // requirement half-implemented and let a producer ship a chain no other
    // implementation accepts.
    let envelope = chain_envelope_with(|_principal, notary| {
      notary.id = std::option::Option::None;
    });
    let err = super::verify_proof_structure(&envelope)
      .expect_err("the notary proof must carry an id");
    std::assert_eq!(err.code(), "APH_E013");
  }

  #[test]
  fn two_proofs_sharing_one_id_are_rejected() {
    // Duplicate ids make `previousProof` ambiguous: the link would resolve
    // to either proof, so a self-reference and a real countersignature
    // become indistinguishable and the linkage stops carrying information.
    let envelope = chain_envelope_with(|_principal, notary| {
      notary.id = std::option::Option::Some(std::string::String::from(PRINCIPAL_PROOF_ID));
      notary.previous_proof = std::option::Option::Some(std::string::String::from(PRINCIPAL_PROOF_ID));
    });
    let err = super::verify_proof_structure(&envelope)
      .expect_err("duplicate proof ids must be rejected");
    std::assert_eq!(err.code(), "APH_E013");
  }

  // ── verify_proof_structure: linkage ───────────────────────────────────

  #[test]
  fn a_notary_proof_without_previous_proof_is_rejected() {
    // Array position is a HINT; `previousProof` is the binding (§8.3.1 1e).
    // An unlinked pair of proofs is just two signatures that happen to share
    // an envelope — nothing states that the notary observed THIS principal
    // proof, which is the one thing the countersignature is for.
    let envelope = chain_envelope_with(|_principal, notary| {
      notary.previous_proof = std::option::Option::None;
    });
    let err = super::verify_proof_structure(&envelope)
      .expect_err("missing previousProof must be rejected");
    std::assert_eq!(err.code(), "APH_E013");
    std::assert!(
      std::string::ToString::to_string(&err).contains("previousProof"),
      "the reason must name the missing member: {}",
      err
    );
  }

  #[test]
  fn a_dangling_previous_proof_is_rejected() {
    // §7.1.11: a verifier MUST reject a chain whose `previousProof` does not
    // resolve to a proof present in the same chain. A link pointing outside
    // this envelope could name a proof from a DIFFERENT envelope, which is
    // how a countersignature would get detached and reattached.
    let envelope = chain_envelope_with(|_principal, notary| {
      notary.previous_proof =
        std::option::Option::Some(std::string::String::from("urn:uuid:not-in-this-chain"));
    });
    let err = super::verify_proof_structure(&envelope)
      .expect_err("a dangling previousProof must be rejected");
    std::assert_eq!(err.code(), "APH_E013");
    std::assert!(
      std::string::ToString::to_string(&err).contains("dangling"),
      "the reason must distinguish dangling from missing: {}",
      err
    );
  }

  #[test]
  fn a_self_referential_previous_proof_is_rejected() {
    // A proof that countersigns itself is a cycle: it claims to cover bytes
    // that include its own signature, which is unconstructible. Reported
    // distinctly from "dangling" so an implementer sees which mistake they
    // made rather than hunting for a proof id that was never missing.
    let envelope = chain_envelope_with(|_principal, notary| {
      notary.previous_proof = std::option::Option::Some(std::string::String::from(NOTARY_PROOF_ID));
    });
    let err = super::verify_proof_structure(&envelope)
      .expect_err("a self-referential previousProof must be rejected");
    std::assert_eq!(err.code(), "APH_E013");
    std::assert!(
      std::string::ToString::to_string(&err).contains("self-referential"),
      "the reason must distinguish a cycle from a dangling link: {}",
      err
    );
  }

  #[test]
  fn a_principal_proof_carrying_previous_proof_is_rejected() {
    // The principal proof is the HEAD of the chain and countersigns nothing
    // (§7.1.11). A `previousProof` on it either names a proof outside this
    // envelope or forms a cycle with the notary proof; both make the chain's
    // direction — and therefore the verification order — undefined.
    let envelope = chain_envelope_with(|principal, _notary| {
      principal.previous_proof = std::option::Option::Some(std::string::String::from(NOTARY_PROOF_ID));
    });
    let err = super::verify_proof_structure(&envelope)
      .expect_err("the chain head must not carry previousProof");
    std::assert_eq!(err.code(), "APH_E013");
  }

  // ── verify_proof_structure: the principal-key binding ─────────────────

  #[test]
  fn a_principal_proof_made_by_another_key_is_rejected() {
    // The check that makes `PrincipalSigned` unforgeable by a notary. Here
    // the chain is perfectly formed and correctly labelled, but the head
    // proof is made under the NOTARY's DID — so the "principal proof" is the
    // notary signing twice. §7.1.11 says a proof made by any other key is
    // not the principal's proof, whatever its `proofPurpose` says, and the
    // notary does not hold the human's key.
    let envelope = chain_envelope_with(|principal, _notary| {
      principal.verification_method = std::format!("{}#key-1", NOTARY_DID);
    });
    let err = super::verify_proof_structure(&envelope)
      .expect_err("a principal proof under a foreign DID must be rejected");
    std::assert_eq!(err.code(), "APH_E013");
  }

  #[test]
  fn a_principal_verification_method_that_only_prefixes_the_human_did_is_rejected() {
    // Prefix matching on the RAW string would accept
    // `did:key:zHuman-evil#k` for the principal `did:key:zHuman`. The rule
    // is equality of the DID part BEFORE the `#`, not string containment, or
    // an attacker could register a DID whose text extends the victim's.
    let envelope = chain_envelope_with(|principal, _notary| {
      principal.verification_method = std::format!("{}-attacker#key-1", HUMAN_DID);
    });
    let err = super::verify_proof_structure(&envelope)
      .expect_err("a DID that merely starts with the human's DID must be rejected");
    std::assert_eq!(err.code(), "APH_E013");
  }

  // ── require_mode ──────────────────────────────────────────────────────

  #[test]
  fn require_principal_signed_refuses_a_notary_attested_envelope() {
    // §8.3.1 step 1a: there is no silent downgrade from a stronger
    // attestation to a weaker one, because an attacker who can defeat the
    // weak path will always present the weak path. APH_E012 specifically —
    // this is a policy refusal, not a malformed envelope, and an operator
    // reading a log must be able to tell those apart.
    let err = super::require_mode(
      &single_proof_envelope(),
      crate::envelope::AttestationMode::PrincipalSigned,
    )
    .expect_err("a NotaryAttested envelope must not satisfy a PrincipalSigned policy");
    std::assert_eq!(err.code(), "APH_E012");
  }

  #[test]
  fn require_notary_attested_accepts_a_principal_signed_envelope() {
    // A STRONGER envelope satisfies a weaker policy. Rejecting it would
    // punish the producer who did more work and give every deployment a
    // reason to stay on the weaker mode.
    std::assert!(
      super::require_mode(
        &chain_envelope(),
        crate::envelope::AttestationMode::NotaryAttested,
      )
      .is_ok()
    );
  }

  #[test]
  fn require_mode_accepts_an_exact_match() {
    // The ordinary case, pinned so a future rewrite of the comparison cannot
    // make the common path fail while the two interesting cases still pass.
    std::assert!(
      super::require_mode(
        &single_proof_envelope(),
        crate::envelope::AttestationMode::NotaryAttested,
      )
      .is_ok()
    );
  }

  #[test]
  fn require_mode_reads_absence_as_the_weaker_mode() {
    // The eight published envelopes carry no `attestationMode`, so a
    // deployment that demands `PrincipalSigned` must refuse them rather than
    // treat the missing field as unconstrained. §7.1.7 fixes absent as
    // `NotaryAttested`; this is that default doing security work.
    let mut envelope = single_proof_envelope();
    envelope.credential_subject.policy.attestation_mode = std::option::Option::None;
    let err = super::require_mode(&envelope, crate::envelope::AttestationMode::PrincipalSigned)
      .expect_err("an unlabelled envelope must not satisfy a PrincipalSigned policy");
    std::assert_eq!(err.code(), "APH_E012");
  }

  // ── verify_embedded_mandate_binding ───────────────────────────────────

  #[test]
  fn an_absent_embedded_mandate_is_not_a_structural_error() {
    // §7.1.7.1 makes embedding SHOULD, not MUST, because an embedded mandate
    // discloses the human's entire standing grant to every recipient.
    // Absence means the human's authorization is not VERIFIABLE here — a
    // policy conclusion for the caller, not a malformed envelope.
    std::assert!(super::verify_embedded_mandate_binding(&single_proof_envelope()).is_ok());
  }

  #[test]
  fn a_matching_embedded_mandate_binds() {
    // Positive control for all five bindings at once. Every rejection test
    // below changes exactly one field of this envelope, so if this failed
    // they would all pass without proving the specific rule they name.
    let mut envelope = single_proof_envelope();
    envelope.credential_subject.policy.delegation_mandate_id =
      std::option::Option::Some(std::string::String::from(MANDATE_ID));
    envelope.credential_subject.policy.delegation_mandate = std::option::Option::Some(mandate());
    std::assert!(super::verify_embedded_mandate_binding(&envelope).is_ok());
  }

  #[test]
  fn an_embedded_mandate_for_another_human_is_rejected() {
    // The staple attack §7.1.7.1 exists to close: a validly-signed mandate
    // belonging to a DIFFERENT human, attached to this envelope. Its
    // principalSignature verifies perfectly; it just does not authorize this
    // send. Without this equality the embedded mandate proves only that
    // SOME human granted SOME agent SOMETHING.
    let mut envelope = single_proof_envelope();
    let mut stapled = mandate();
    stapled.human_principal_did = std::string::String::from("did:key:z6MkSomeoneElse");
    envelope.credential_subject.policy.delegation_mandate = std::option::Option::Some(stapled);
    let err = super::verify_embedded_mandate_binding(&envelope)
      .expect_err("a mandate for another human must not bind");
    std::assert_eq!(err.code(), "APH_E011");
  }

  #[test]
  fn an_embedded_mandate_for_another_agent_is_rejected() {
    // Same staple, one hop over: the human is right but the grant was made
    // to a different agent. Accepting it would let any agent borrow another
    // agent's delegation from the same human, defeating the per-agent scope
    // the mandate exists to express.
    let mut envelope = single_proof_envelope();
    let mut stapled = mandate();
    stapled.agent_did = std::string::String::from("did:web:other-agent.example");
    envelope.credential_subject.policy.delegation_mandate = std::option::Option::Some(stapled);
    let err = super::verify_embedded_mandate_binding(&envelope)
      .expect_err("a mandate for another agent must not bind");
    std::assert_eq!(err.code(), "APH_E011");
  }

  #[test]
  fn an_embedded_mandate_disagreeing_with_the_declared_id_is_rejected() {
    // When `delegationMandateId` is present the envelope has NAMED its
    // parent. A different mandate in the `delegationMandate` member means
    // the notary's own record and the embedded evidence disagree, and a
    // verifier cannot know which one policy was actually evaluated against.
    let mut envelope = single_proof_envelope();
    envelope.credential_subject.policy.delegation_mandate_id =
      std::option::Option::Some(std::string::String::from("urn:uuid:some-other-mandate"));
    envelope.credential_subject.policy.delegation_mandate = std::option::Option::Some(mandate());
    let err = super::verify_embedded_mandate_binding(&envelope)
      .expect_err("an embedded mandate must be the one the envelope names");
    std::assert_eq!(err.code(), "APH_E011");
  }

  #[test]
  fn an_embedded_mandate_not_covering_the_channel_is_rejected() {
    // APH_E005 specifically, because this is the one §8.3.1 step 1d names
    // for it: the grant is genuine and belongs to this pair, but this send
    // is outside the channel scope the human approved. A verifier reporting
    // E011 here would tell an operator the signature was bad when the
    // signature was fine and the SCOPE was wrong.
    let mut envelope = single_proof_envelope();
    envelope.credential_subject.channel.kind = crate::envelope::ChannelKind::Discord;
    envelope.credential_subject.policy.delegation_mandate = std::option::Option::Some(mandate());
    let err = super::verify_embedded_mandate_binding(&envelope)
      .expect_err("a channel outside allowedChannels must not bind");
    std::assert_eq!(err.code(), "APH_E005");
  }

  #[test]
  fn an_envelope_outside_the_mandate_window_is_rejected() {
    // APH_E003, the other code §8.3.1 step 1d names. Expiry is the primary
    // revocation mechanism in v0.1 (§6.3.1 defers on-wire revocation), so an
    // envelope dated outside the mandate's window that still bound would
    // leave a human with no way to withdraw authority at all.
    let mut envelope = single_proof_envelope();
    envelope.valid_from = std::string::String::from("2026-07-01T00:00:00Z");
    envelope.valid_until = std::string::String::from("2026-07-02T00:00:00Z");
    envelope.credential_subject.policy.delegation_mandate = std::option::Option::Some(mandate());
    let err = super::verify_embedded_mandate_binding(&envelope)
      .expect_err("an envelope outside the mandate window must not bind");
    std::assert_eq!(err.code(), "APH_E003");
  }

  #[test]
  fn an_envelope_expiring_after_the_mandate_is_rejected() {
    // §8.3.1 step 1d requires the envelope's WINDOW to fall inside the
    // mandate's, not merely its start. Checking `validFrom` alone would let
    // a notary issue an envelope that starts inside the grant and stays
    // valid for a year after the human's authority ended.
    let mut envelope = single_proof_envelope();
    envelope.valid_from = std::string::String::from("2026-05-31T00:00:00Z");
    envelope.valid_until = std::string::String::from("2026-06-30T00:00:00Z");
    envelope.credential_subject.policy.delegation_mandate = std::option::Option::Some(mandate());
    let err = super::verify_embedded_mandate_binding(&envelope)
      .expect_err("an envelope outliving the mandate must not bind");
    std::assert_eq!(err.code(), "APH_E003");
  }

  #[test]
  fn an_unparseable_envelope_window_fails_closed() {
    // Hostile input must deny, never default to valid. `is_valid_at`
    // returns false on anything it cannot parse, and this pins that the
    // caller inherits that fail-closed behavior instead of a panic or a
    // silent pass.
    let mut envelope = single_proof_envelope();
    envelope.valid_from = std::string::String::from("not-a-timestamp");
    envelope.credential_subject.policy.delegation_mandate = std::option::Option::Some(mandate());
    let err = super::verify_embedded_mandate_binding(&envelope)
      .expect_err("an unparseable window must not bind");
    std::assert_eq!(err.code(), "APH_E003");
  }

  // ── verify_timestamp_order ────────────────────────────────────────────

  #[test]
  fn a_single_proof_envelope_has_nothing_to_order() {
    // §7.2.1's ordering constrains a CHAIN. A lone notary proof has no
    // principal proof to precede it, so the check must be vacuous rather
    // than inventing a constraint that would reject the published corpus.
    std::assert!(super::verify_timestamp_order(&single_proof_envelope()).is_ok());
  }

  #[test]
  fn issuance_order_decision_then_principal_then_notary_is_accepted() {
    // The order §7.2.1 makes normative: the notary prepares the envelope,
    // the principal signs what was prepared, the notary countersigns. This
    // is the positive control for the two rejections below.
    std::assert!(super::verify_timestamp_order(&chain_envelope()).is_ok());
  }

  #[test]
  fn equal_timestamps_are_accepted() {
    // The constraint is `<=`, not `<`. Three signatures produced inside the
    // same second are ordinary — the notary is typically co-located with the
    // principal (§4) — and a strict comparison would reject correct
    // envelopes for having a fast device.
    let mut envelope = chain_envelope_with(|principal, notary| {
      principal.created = std::string::String::from("2026-05-21T00:00:01Z");
      notary.created = std::string::String::from("2026-05-21T00:00:01Z");
    });
    envelope.credential_subject.notarization.decision_timestamp =
      std::string::String::from("2026-05-21T00:00:01Z");
    std::assert!(super::verify_timestamp_order(&envelope).is_ok());
  }

  #[test]
  fn a_principal_proof_predating_the_decision_is_rejected() {
    // Reverse §7.2.1 steps 1 and 2 and the principal would have to sign
    // notary-produced fields — the decision timestamp, the latency — that do
    // not exist yet. A proof dated before the decision it authorizes is
    // evidence that the envelope was assembled out of order, or that its
    // timestamps were written to tell a story.
    let envelope = chain_envelope_with(|principal, _notary| {
      principal.created = std::string::String::from("2026-05-21T00:00:00Z");
    });
    let err = super::verify_timestamp_order(&envelope)
      .expect_err("a principal proof predating the decision must be rejected");
    std::assert_eq!(err.code(), "APH_E013");
  }

  #[test]
  fn a_countersignature_predating_the_principal_proof_is_rejected() {
    // §7.2.1 states it outright: a verifier MUST NOT accept a chain whose
    // notary proof is dated before the principal proof it claims to have
    // observed. The notary proof covers the principal's `proofValue`, so a
    // notary that signed first signed bytes that did not yet exist.
    let envelope = chain_envelope_with(|_principal, notary| {
      notary.created = std::string::String::from("2026-05-21T00:00:01Z");
    });
    let err = super::verify_timestamp_order(&envelope)
      .expect_err("a countersignature predating its principal proof must be rejected");
    std::assert_eq!(err.code(), "APH_E013");
  }

  #[test]
  fn an_unparseable_proof_timestamp_is_rejected() {
    // Fail closed. An unreadable timestamp cannot be shown to satisfy the
    // ordering, and treating "cannot check" as "checked" would make the
    // whole rule optional for any producer willing to emit garbage.
    let envelope = chain_envelope_with(|principal, _notary| {
      principal.created = std::string::String::from("yesterday");
    });
    let err = super::verify_timestamp_order(&envelope)
      .expect_err("an unparseable timestamp must be rejected");
    std::assert_eq!(err.code(), "APH_E013");
  }

  #[test]
  fn timestamps_are_compared_as_instants_not_strings() {
    // `2026-05-21T00:00:03Z` and `2026-05-20T20:00:03-04:00` are the SAME
    // instant, but the second sorts earlier lexicographically. Comparing the
    // text would reject this correct chain, so the crate parses with
    // `chrono` — the same thing `DelegationMandate::is_valid_at` does.
    let envelope = chain_envelope_with(|_principal, notary| {
      notary.created = std::string::String::from("2026-05-20T20:00:03-04:00");
    });
    std::assert!(
      super::verify_timestamp_order(&envelope).is_ok(),
      "an equivalent instant written with an offset must order correctly"
    );
  }

  #[test]
  fn a_malformed_chain_cannot_have_its_order_checked() {
    // Fail closed on a structure this function cannot interpret. Returning
    // Ok would let a caller that runs the ordering check but skips
    // `verify_proof_structure` conclude a malformed chain was fine.
    let mut envelope = chain_envelope();
    envelope.proof = crate::envelope::EnvelopeProofs::Chain(std::vec![principal_proof()]);
    let err = super::verify_timestamp_order(&envelope)
      .expect_err("a one-element chain has no checkable order");
    std::assert_eq!(err.code(), "APH_E013");
  }
  // ── RFC 0003: audience, window, single-use ──────────────────────

  fn audience(id: &str) -> crate::envelope::Audience {
    crate::envelope::Audience {
      id: id.to_string(),
      channel_binding: std::option::Option::None,
    }
  }

  fn binding(
    kind: crate::envelope::ChannelKind,
    coordinates: &[(&str, &str)],
  ) -> crate::envelope::AudienceChannelBinding {
    crate::envelope::AudienceChannelBinding {
      kind,
      coordinates: coordinates
        .iter()
        .map(|(k, v)| (k.to_string(), serde_json::json!(v)))
        .collect(),
    }
  }

  #[test]
  fn an_envelope_with_no_audience_is_a_bearer_credential_by_choice() {
    // RFC 0003 makes bearer semantics the producer's EXPLICIT decision:
    // absence admits, and turning absence into a refusal would break every
    // envelope minted before the field existed.
    let envelope = single_proof_envelope();
    super::verify_audience(&envelope, "did:web:anyone.example.com", std::option::Option::None)
      .expect("absence is not a constraint");
  }

  #[test]
  fn an_audience_naming_another_verifier_is_refused() {
    // The core of the replay fix: a captured envelope stops being valid in
    // anyone's hands. The refusal must be E017 and must name both parties,
    // or the operator reading the log cannot tell WHICH side is misrouted.
    let mut envelope = single_proof_envelope();
    envelope.credential_subject.audience =
      std::option::Option::Some(audience("did:web:intended.example.com"));
    let err =
      super::verify_audience(&envelope, "did:web:other.example.com", std::option::Option::None)
        .expect_err("a mismatched audience must refuse");
    std::assert_eq!(err.code(), "APH_E017");
    let text = std::format!("{}", err);
    std::assert!(text.contains("did:web:intended.example.com"));
    std::assert!(text.contains("did:web:other.example.com"));
  }

  #[test]
  fn the_named_verifier_is_admitted() {
    // The positive half of the same coin, pinned so the check cannot rot
    // into refusing everyone and passing its negative tests forever.
    let mut envelope = single_proof_envelope();
    envelope.credential_subject.audience =
      std::option::Option::Some(audience("did:web:intended.example.com"));
    super::verify_audience(&envelope, "did:web:intended.example.com", std::option::Option::None)
      .expect("the named audience is exactly who may accept");
  }

  #[test]
  fn a_channel_binding_with_no_act_coordinates_is_refused_not_skipped() {
    // RFC 0003's sharpest sentence: a verifier that cannot determine the
    // coordinates MUST reject rather than skip. Skipping here would turn
    // the binding into decoration precisely when an attacker relays the
    // envelope somewhere its coordinates are unknown.
    let mut envelope = single_proof_envelope();
    let mut aud = audience("did:web:intended.example.com");
    aud.channel_binding = std::option::Option::Some(binding(
      crate::envelope::ChannelKind::Slack,
      &[("teamId", "T01234567")],
    ));
    envelope.credential_subject.audience = std::option::Option::Some(aud);
    let err = super::verify_audience(
      &envelope,
      "did:web:intended.example.com",
      std::option::Option::None,
    )
    .expect_err("unknown act coordinates must refuse, not skip");
    std::assert_eq!(err.code(), "APH_E017");
  }

  #[test]
  fn a_channel_binding_member_mismatch_is_refused_naming_the_member() {
    // The member-by-member comparison is what stops an envelope for one
    // channel being spent on another; the refusal names the member so the
    // mismatch is diagnosable without diffing JSON by eye.
    let mut envelope = single_proof_envelope();
    let mut aud = audience("did:web:intended.example.com");
    aud.channel_binding = std::option::Option::Some(binding(
      crate::envelope::ChannelKind::Slack,
      &[("teamId", "T01234567"), ("channelId", "C01234567")],
    ));
    envelope.credential_subject.audience = std::option::Option::Some(aud);
    let act = binding(
      crate::envelope::ChannelKind::Slack,
      &[("teamId", "T01234567"), ("channelId", "C_ELSEWHERE")],
    );
    let err = super::verify_audience(
      &envelope,
      "did:web:intended.example.com",
      std::option::Option::Some(&act),
    )
    .expect_err("a differing coordinate must refuse");
    std::assert_eq!(err.code(), "APH_E017");
    std::assert!(std::format!("{}", err).contains("channelId"));
  }

  #[test]
  fn a_matching_channel_binding_is_admitted_and_extra_act_members_are_fine() {
    // The binding constrains what it NAMES: coordinates the act carries
    // beyond the bound ones are the channel's business, not a mismatch —
    // otherwise no binding could be written without enumerating every
    // incidental member a transport adds.
    let mut envelope = single_proof_envelope();
    let mut aud = audience("did:web:intended.example.com");
    aud.channel_binding = std::option::Option::Some(binding(
      crate::envelope::ChannelKind::Slack,
      &[("teamId", "T01234567")],
    ));
    envelope.credential_subject.audience = std::option::Option::Some(aud);
    let act = binding(
      crate::envelope::ChannelKind::Slack,
      &[("teamId", "T01234567"), ("threadTs", "1727000000.000100")],
    );
    super::verify_audience(
      &envelope,
      "did:web:intended.example.com",
      std::option::Option::Some(&act),
    )
    .expect("every bound member matches");
  }

  #[test]
  fn a_kind_mismatch_in_the_binding_is_refused() {
    // Cross-channel spend is the attack the binding exists for: an envelope
    // bound to slack coordinates presented as an email act must refuse even
    // when no other member overlaps.
    let mut envelope = single_proof_envelope();
    let mut aud = audience("did:web:intended.example.com");
    aud.channel_binding =
      std::option::Option::Some(binding(crate::envelope::ChannelKind::Slack, &[]));
    envelope.credential_subject.audience = std::option::Option::Some(aud);
    let act = binding(crate::envelope::ChannelKind::Email, &[]);
    let err = super::verify_audience(
      &envelope,
      "did:web:intended.example.com",
      std::option::Option::Some(&act),
    )
    .expect_err("kind is a bound member like any other");
    std::assert_eq!(err.code(), "APH_E017");
  }

  #[test]
  fn the_envelope_window_admits_inside_and_refuses_both_sides_with_e019() {
    // APH_E019 exists because refusing an expired ENVELOPE used to have no
    // code of its own and E003 (a mandate consulted past expiry) was the
    // standing miscite. The fixture window is 2026-05-21T00:00:00Z ..
    // 2026-05-22T00:00:00Z; both refusals must carry E019 and the verbatim
    // window so the operator sees WHAT was judged, not only that it failed.
    let envelope = single_proof_envelope();
    super::verify_envelope_window(&envelope, "2026-05-21T12:00:00Z").expect("inside the window");
    let early = super::verify_envelope_window(&envelope, "2026-05-20T23:59:59Z")
      .expect_err("not yet valid");
    std::assert_eq!(early.code(), "APH_E019");
    let late = super::verify_envelope_window(&envelope, "2026-05-22T00:00:01Z")
      .expect_err("expired");
    std::assert_eq!(late.code(), "APH_E019");
    std::assert!(std::format!("{}", late).contains("2026-05-22T00:00:00Z"));
  }

  #[test]
  fn an_unparseable_window_fails_closed_to_e019() {
    // An unreadable window never admits — same fail-closed rule as proof
    // timestamps, but routed to the envelope-window code, because "your
    // window is garbage" and "your proof chain is garbage" are different
    // repairs.
    let mut envelope = single_proof_envelope();
    envelope.valid_until = std::string::String::from("not-a-timestamp");
    let err = super::verify_envelope_window(&envelope, "2026-05-21T12:00:00Z")
      .expect_err("garbage must refuse");
    std::assert_eq!(err.code(), "APH_E019");
  }

  #[test]
  fn an_envelope_is_spent_by_acceptance_and_a_second_presentation_refuses() {
    // RFC 0003 §2 measured the defect: 100 presentations of one golden
    // envelope, 100 admissions. The ledger turns presentation two into
    // APH_E018 naming the id, and refusing is not accepting — the refusal
    // consumes nothing further.
    let mut ledger = super::InMemoryConsumedEnvelopeLedger::default();
    let envelope = single_proof_envelope();
    super::enforce_single_use(&mut ledger, &envelope).expect("first acceptance spends");
    let err = super::enforce_single_use(&mut ledger, &envelope)
      .expect_err("second presentation is a replay");
    std::assert_eq!(err.code(), "APH_E018");
    std::assert!(std::format!("{}", err).contains(&envelope.id));
    let again =
      super::enforce_single_use(&mut ledger, &envelope).expect_err("and it stays spent");
    std::assert_eq!(again.code(), "APH_E018");
  }

  #[test]
  fn distinct_envelope_ids_spend_independently() {
    // The ledger is keyed on `id` alone — two different envelopes never
    // contend, which is what makes the retention bound implementable: the
    // store grows with ids inside live windows, not with traffic.
    let mut ledger = super::InMemoryConsumedEnvelopeLedger::default();
    let first = single_proof_envelope();
    let mut second = single_proof_envelope();
    second.id = std::string::String::from("urn:uuid:00000000-0000-4000-8000-000000000002");
    super::enforce_single_use(&mut ledger, &first).expect("first id is fresh");
    super::enforce_single_use(&mut ledger, &second).expect("second id is fresh too");
  }
  // ── RFC 0005: the recipient-class constraint ─────────────────────

  #[test]
  fn an_unconstrained_mandate_ignores_recipient_class_entirely() {
    // None means the grant says nothing about consumers — which every
    // mandate signed before RFC 0005 is, so this arm is also the
    // signature-compatibility arm.
    let mut envelope = single_proof_envelope();
    envelope.credential_subject.policy.delegation_mandate = std::option::Option::Some(mandate());
    super::verify_embedded_mandate_binding(&envelope)
      .expect("no constraint, nothing to check");
  }

  #[test]
  fn a_declared_class_inside_the_grant_is_admitted() {
    let mut envelope = single_proof_envelope();
    envelope.credential_subject.channel.recipient_class =
      std::option::Option::Some(crate::envelope::RecipientClass::Human);
    let mut constrained = mandate();
    constrained.allowed_recipient_classes =
      std::option::Option::Some(std::vec![crate::envelope::RecipientClass::Human]);
    envelope.credential_subject.policy.delegation_mandate =
      std::option::Option::Some(constrained);
    super::verify_embedded_mandate_binding(&envelope)
      .expect("human is exactly what the grant covers");
  }

  #[test]
  fn a_declared_class_outside_the_grant_is_refused_with_e020() {
    // The motivating case verbatim: the grant says "email to PEOPLE", the
    // act is unattended machine-to-machine traffic on the same medium.
    let mut envelope = single_proof_envelope();
    envelope.credential_subject.channel.recipient_class =
      std::option::Option::Some(crate::envelope::RecipientClass::Agent);
    let mut constrained = mandate();
    constrained.allowed_recipient_classes =
      std::option::Option::Some(std::vec![crate::envelope::RecipientClass::Human]);
    envelope.credential_subject.policy.delegation_mandate =
      std::option::Option::Some(constrained);
    let err = super::verify_embedded_mandate_binding(&envelope)
      .expect_err("agent traffic under a human-only grant must refuse");
    std::assert_eq!(err.code(), "APH_E020");
    std::assert!(std::format!("{}", err).contains("agent"));
  }

  #[test]
  fn declaring_nothing_under_a_constrained_grant_is_refused_not_waved() {
    // The omission arm: were absence admitted, the constraint would be
    // escapable by simply not declaring — and the refusal message says
    // `nothing` so the operator sees WHICH failure this was.
    let mut envelope = single_proof_envelope();
    let mut constrained = mandate();
    constrained.allowed_recipient_classes =
      std::option::Option::Some(std::vec![crate::envelope::RecipientClass::Human]);
    envelope.credential_subject.policy.delegation_mandate =
      std::option::Option::Some(constrained);
    let err = super::verify_embedded_mandate_binding(&envelope)
      .expect_err("an unverifiable constraint refuses");
    std::assert_eq!(err.code(), "APH_E020");
    std::assert!(std::format!("{}", err).contains("nothing"));
  }
}