enclavia-protocol 0.1.0

Shared wire types and Noise+CBOR responder helpers used by the Enclavia server, router, and SDK.
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
//! Nitro NSM attestation verification.
//!
//! Two entry points share the same parse-and-verify core:
//!
//! - [`verify_against`] — "is this document from the enclave I expected?"
//!   The SDK's call path: a client knows what PCRs the target enclave is
//!   supposed to have and wants the document to confirm it.
//!
//! - [`verify_and_extract`] — "what enclave produced this document?" The
//!   synchronizer's call path: it does not pre-commit to a specific
//!   identity; the document's verified PCRs *are* the identity, and the
//!   caller hashes them into its own key. The doc must also carry the
//!   enclave's raw 32-byte Ed25519 control pubkey in `user_data` — the
//!   synchronizer registers it alongside the key and uses it to verify
//!   `Transition` signatures later.
//!
//! Both check that the doc's nonce equals `base64(handshake_hash)`,
//! binding the document to the live Noise session. In `debug_mode` the
//! COSE_Sign1 certificate chain is skipped (the in-enclave NSM
//! self-signs when run under QEMU) — production validates the full
//! chain.

use attestation_doc_validation::{
    PCRProvider, attestation_doc::decode_attestation_document,
    attestation_doc::get_pcrs as att_get_pcrs, validate_and_parse_attestation_doc,
    validate_expected_nonce, validate_expected_pcrs,
};
use aws_nitro_enclaves_nsm_api::api::AttestationDoc;
use base64::Engine;
use sha2::{Digest, Sha256};

/// PCR (Platform Configuration Register) measurements that identify a
/// specific enclave image and configuration:
///
/// - `pcr0` — Enclave Image File (EIF) measurement.
/// - `pcr1` — Enclave OS measurement.
/// - `pcr2` — Application configuration measurement.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Pcrs {
    /// EIF measurement.
    pub pcr0: Vec<u8>,
    /// Enclave OS measurement.
    pub pcr1: Vec<u8>,
    /// Application configuration measurement.
    pub pcr2: Vec<u8>,
}

impl Pcrs {
    /// Build [`Pcrs`] from the three hex-encoded measurements, exactly as
    /// printed by `enclavia enclave status` / `enclavia reproduce` and
    /// shown on the dashboard, so they can be copy/pasted verbatim.
    /// Accepts upper- or lower-case hex and surrounding whitespace. Each
    /// value must decode to 32, 48, or 64 bytes (real Nitro PCRs are
    /// 48-byte SHA-384, 96 hex characters).
    ///
    /// ```
    /// let pcrs = enclavia_protocol::attestation::Pcrs::from_hex(
    ///     &"ab".repeat(48),
    ///     &"cd".repeat(48),
    ///     &"ef".repeat(48),
    /// )
    /// .unwrap();
    /// assert_eq!(pcrs.pcr0.len(), 48);
    /// ```
    pub fn from_hex(pcr0: &str, pcr1: &str, pcr2: &str) -> Result<Self, AttestationError> {
        fn decode(idx: usize, s: &str) -> Result<Vec<u8>, AttestationError> {
            let bytes =
                hex::decode(s.trim()).map_err(|_| AttestationError::InvalidPcrHex(idx))?;
            if !matches!(bytes.len(), 32 | 48 | 64) {
                return Err(AttestationError::InvalidPcrLength { idx, len: bytes.len() });
            }
            Ok(bytes)
        }
        Ok(Pcrs {
            pcr0: decode(0, pcr0)?,
            pcr1: decode(1, pcr1)?,
            pcr2: decode(2, pcr2)?,
        })
    }

    /// SHA-256 over `PCR0 || PCR1 || PCR2`. The synchronizer uses this
    /// 32-byte digest as the per-enclave session key.
    pub fn digest(&self) -> [u8; 32] {
        let mut hasher = Sha256::new();
        hasher.update(&self.pcr0);
        hasher.update(&self.pcr1);
        hasher.update(&self.pcr2);
        hasher.finalize().into()
    }
}

/// Errors from attestation verification.
#[derive(Debug, thiserror::Error)]
pub enum AttestationError {
    /// Parse/structure/signature/PCR/nonce validation failed in the
    /// upstream `attestation-doc-validation` crate. Carries the original
    /// error rendered to a string — the upstream type is non-exhaustive
    /// and not worth re-exporting.
    #[error("attestation document validation failed: {0}")]
    Validation(String),
    /// A PCR value coming out of the validated document hex-decoded to
    /// something other than 32/48/64 bytes, which would break PcrKey
    /// derivation. Should be unreachable for real Nitro docs.
    #[error("attestation document PCR {idx} has unexpected length {len}")]
    InvalidPcrLength {
        /// The PCR index (0, 1, or 2).
        idx: usize,
        /// The decoded length in bytes.
        len: usize,
    },
    /// A PCR slot was not hex-encoded. Should be unreachable: the
    /// upstream crate is the one that hex-encodes them on the way out.
    #[error("attestation document PCR {0} is not valid hex")]
    InvalidPcrHex(usize),
    /// The doc's `user_data` field is missing or not a 65-byte
    /// uncompressed SEC1 ECDSA P-256 verifying key. Required by
    /// [`verify_and_extract`] — the synchronizer needs the control
    /// pubkey to verify `Transition` signatures later in the session.
    #[error(
        "attestation document user_data is missing or not a 65-byte uncompressed SEC1 P-256 pubkey"
    )]
    InvalidControlPubkey,
    /// The doc's `user_data` field is missing or not the 32-byte
    /// SHA-256 hash of the chain link's `payload`. Required by
    /// [`verify_chain_attestation`] — every chain link binds its
    /// `attestation.user_data` to `sha256(payload)`, so any mismatch
    /// means either the payload or the attestation has been swapped.
    #[error("attestation document user_data does not match sha256(payload)")]
    PayloadBindingMismatch,
    /// The doc's `user_data` field is missing or not exactly 32 bytes
    /// where a control nonce was expected. Returned by
    /// [`verify_control_nonce_attestation`]: the in-enclave server's
    /// `RequestAttestation` reply always embeds the current 32-byte
    /// control nonce as `user_data`, so any other shape means the
    /// document was produced for a different purpose (or tampered with).
    #[error("attestation document user_data is not a 32-byte control nonce")]
    InvalidControlNonce,
    /// The document verified (structure, signature, nonce binding) but
    /// its PCR0/1/2 equal NONE of the caller's expected triples.
    /// Returned by [`verify_and_extract_pcrs`]: the presenting enclave
    /// is genuine but is not the identity the caller trusts
    /// (server authentication).
    #[error("attestation document PCRs match none of the expected values")]
    PcrsNotExpected,
}

/// Length of an ECDSA P-256 verifying key in uncompressed SEC1 form
/// (`0x04 || X(32) || Y(32)`). Locked at the protocol layer because
/// every caller — synchronizer node, in-enclave server, attestation
/// emitter — needs to agree on the shape carried in
/// `AttestationDoc::user_data`.
pub const CONTROL_PUBKEY_LEN: usize = 65;

/// Domain-separation string the canonical non-upgradable control key is
/// derived from. Public and fixed: it is the audit anchor that lets
/// anyone reproduce [`NON_UPGRADABLE_CONTROL_KEY`] and confirm the
/// construction.
pub const NON_UPGRADABLE_CONTROL_KEY_DST: &[u8] =
    b"enclavia/synchronizer/non-upgradable-control-key/v1";

/// The canonical "provably un-signable" control key for enclaves that
/// have no upgrade path at all (non-upgradable enclaves).
///
/// ## What it is for
///
/// The synchronizer freezes a key's control pubkey at first pin and uses
/// it for exactly one thing: verifying the ECDSA signature on a future
/// `Transition` (the PCR re-key that an upgrade performs). An enclave
/// with no upgrade chain has no control key, so the storage-pinning
/// client registers with THIS value instead. Because no private key for
/// it is known to anyone, no `Transition` signature can ever verify, so
/// the pinned storage history is permanently bound to that one image,
/// which is exactly the correct semantic for a non-upgradable enclave.
/// It only disables `Transition`; `Pin`/`Get` are gated by the attested
/// PCR key, not by this pubkey, so storage pinning works normally.
///
/// ## Why it is provably un-signable (nothing-up-my-sleeve)
///
/// The point's x-coordinate is a SHA-256 hash output over the public
/// [`NON_UPGRADABLE_CONTROL_KEY_DST`] (try-and-increment to the first
/// valid curve point). Recovering a private key would mean solving the
/// discrete log for a point whose x nobody chose, so by construction no
/// party knows (or could have arranged to know) the scalar. This is
/// strictly safer than minting a throwaway real key and trusting that
/// its private half was destroyed: here no usable private half ever
/// existed.
///
/// Baked as a compile-time constant (uncompressed SEC1, `0x04 || X || Y`)
/// so it costs nothing at runtime and is usable in const contexts. The
/// bytes are the output of try-and-increment over
/// [`NON_UPGRADABLE_CONTROL_KEY_DST`] (hash the DST with a 1-byte
/// counter to a candidate x-coordinate, take the first that decompresses
/// to a valid P-256 point). `derive_non_upgradable_control_key` in the
/// tests re-runs that derivation and asserts it equals this constant, so
/// the literal can never silently drift from its construction.
pub const NON_UPGRADABLE_CONTROL_KEY: [u8; CONTROL_PUBKEY_LEN] = [
    0x04, 0x22, 0x18, 0xad, 0x29, 0x17, 0x7d, 0x9a, 0x5c, 0xb3, 0x52, 0xc4, 0x78, 0x64, 0x06, 0xfa,
    0x76, 0x57, 0xaa, 0xc1, 0x6c, 0xe4, 0xb2, 0xe8, 0x19, 0xcd, 0xbd, 0x7f, 0x6e, 0xbd, 0xfa, 0x5a,
    0x8e, 0xb1, 0x1a, 0xf7, 0x68, 0x69, 0x3a, 0xd6, 0x5f, 0xc5, 0xb2, 0x21, 0x10, 0x3f, 0x10, 0x8a,
    0xe9, 0x50, 0x87, 0xb3, 0x1d, 0x68, 0x54, 0xe8, 0x13, 0x51, 0x60, 0x6d, 0xc4, 0xe2, 0xd4, 0xf7,
    0xda,
];

/// Verified enclave identity extracted from an NSM attestation document.
///
/// Returned by [`verify_and_extract`] when the document validates and the
/// caller wants both the PCRs (for deriving a session key) and the
/// enclave's ECDSA P-256 control pubkey (for verifying future
/// `Transition` signatures from this key).
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AttestedIdentity {
    /// PCR0/1/2 from the validated document.
    pub pcrs: Pcrs,
    /// 65-byte uncompressed SEC1 ECDSA P-256 verifying key extracted
    /// from the doc's `user_data` field. The synchronizer registers
    /// this alongside the [`Pcrs::digest`]-derived key on first
    /// attestation, and uses it to verify raw r||s signatures on
    /// subsequent `Transition` RPCs.
    pub control_pubkey: [u8; CONTROL_PUBKEY_LEN],
}

/// Verify an attestation document against expected PCRs.
///
/// SDK entry point. The caller has pinned the enclave's identity at
/// configure-time and wants `Ok(())` on a match or an error otherwise.
///
/// Checks performed (in order, in both `debug_mode` and production):
///
/// 1. Parse + structural validation of the COSE_Sign1 wrapper.
/// 2. Nonce equals `base64(handshake_hash)`.
/// 3. PCR0/1/2 in the doc equal the caller-supplied `expected_pcrs`.
///
/// Additionally, in production mode (`debug_mode = false`), the AWS
/// Nitro CA chain is validated and the COSE signature is verified.
pub fn verify_against(
    attestation_data: &[u8],
    handshake_hash: &[u8],
    expected_pcrs: &Pcrs,
    debug_mode: bool,
) -> Result<(), AttestationError> {
    let pcrs_hex = PcrsHex::from_pcrs(expected_pcrs);
    let doc = parse_and_validate(attestation_data, debug_mode)?;

    check_nonce(&doc, handshake_hash)?;

    validate_expected_pcrs(&doc, &pcrs_hex)
        .map_err(|e| AttestationError::Validation(e.to_string()))?;

    Ok(())
}

/// Verify a control-nonce attestation and return the attested nonce.
///
/// Backend control-dispatch entry point (upgrade-chain hardening). Before signing
/// and sending a control command, the dispatcher requests an attestation
/// over the control channel; the in-enclave server's reply binds the
/// live Noise session (doc `nonce` = `base64(handshake_hash)`) and
/// carries the current 32-byte control nonce in `user_data`. Verifying
/// the document before dispatch gives the caller two guarantees a bare
/// `GetControlNonce` round-trip cannot:
///
/// 1. The Noise session terminates inside the enclave whose PCRs the
///    caller expected, with no host in the middle, so the eventual
///    `ControlResult` is authentic rather than the relay's word.
/// 2. The nonce embedded in the signed command was minted by that
///    enclave, not substituted on the way through the host.
///
/// Verification is [`verify_against`] (COSE chain in production mode,
/// session-nonce binding, PCR equality) plus a requirement that
/// `user_data` is exactly 32 bytes, returned as the attested control
/// nonce.
pub fn verify_control_nonce_attestation(
    attestation_data: &[u8],
    handshake_hash: &[u8],
    expected_pcrs: &Pcrs,
    debug_mode: bool,
) -> Result<[u8; 32], AttestationError> {
    let pcrs_hex = PcrsHex::from_pcrs(expected_pcrs);
    let doc = parse_and_validate(attestation_data, debug_mode)?;

    check_nonce(&doc, handshake_hash)?;

    validate_expected_pcrs(&doc, &pcrs_hex)
        .map_err(|e| AttestationError::Validation(e.to_string()))?;

    let user_data = doc
        .user_data
        .as_ref()
        .ok_or(AttestationError::InvalidControlNonce)?;
    user_data
        .as_slice()
        .try_into()
        .map_err(|_| AttestationError::InvalidControlNonce)
}

/// Verify an attestation document and return the enclave identity it
/// embeds.
///
/// Synchronizer entry point. The caller does not know in advance which
/// enclave is connecting — the verified document's PCRs *are* the
/// identity, and the doc's `user_data` carries the enclave's Ed25519
/// control pubkey. The caller typically passes the returned
/// [`AttestedIdentity::pcrs`] through [`Pcrs::digest`] to derive a stable
/// session key, and registers
/// [`AttestedIdentity::control_pubkey`] for verifying future
/// `Transition` RPCs from this key.
///
/// Verification is identical to [`verify_against`] minus the
/// `expected_pcrs` equality check (there are no expected PCRs at this
/// layer — the doc's nonce binding to the handshake hash is what
/// authenticates the document's origin to the live session), plus a
/// requirement that `user_data` is exactly [`CONTROL_PUBKEY_LEN`]
/// bytes — the uncompressed SEC1 ECDSA P-256 verifying key.
pub fn verify_and_extract(
    attestation_data: &[u8],
    handshake_hash: &[u8],
    debug_mode: bool,
) -> Result<AttestedIdentity, AttestationError> {
    let doc = parse_and_validate(attestation_data, debug_mode)?;

    check_nonce(&doc, handshake_hash)?;

    let hex_pcrs = att_get_pcrs(&doc).map_err(|e| AttestationError::Validation(e.to_string()))?;

    let pcrs = Pcrs {
        pcr0: decode_pcr(&hex_pcrs.pcr_0, 0)?,
        pcr1: decode_pcr(&hex_pcrs.pcr_1, 1)?,
        pcr2: decode_pcr(&hex_pcrs.pcr_2, 2)?,
    };

    let user_data = doc
        .user_data
        .as_ref()
        .ok_or(AttestationError::InvalidControlPubkey)?;
    let control_pubkey: [u8; CONTROL_PUBKEY_LEN] = user_data
        .as_slice()
        .try_into()
        .map_err(|_| AttestationError::InvalidControlPubkey)?;
    // SEC1 uncompressed-form prefix must be 0x04. Anything else (0x02 /
    // 0x03 compressed, or random bytes that happen to fit) is rejected
    // here so the in-enclave verifier doesn't have to handle the
    // compressed-form decompression path.
    if control_pubkey[0] != 0x04 {
        return Err(AttestationError::InvalidControlPubkey);
    }

    Ok(AttestedIdentity {
        pcrs,
        control_pubkey,
    })
}

/// Verify an attestation document's session binding AND that its PCRs
/// equal one of the caller's `expected` triples, with no `user_data`
/// requirement. Returns the verified PCRs (which of the expected set
/// matched).
///
/// Server-authentication entry point.
/// The synchronizer's CUSTOMER client uses this to authenticate the
/// ORACLE back to itself: the synchronizer sends its own NSM document
/// bound to the live Noise session and the client validates it here
/// against the synchronizer measurements it trusts.
///
/// Checks performed:
///
/// 1. Parse + structural validation of the COSE_Sign1 wrapper; in
///    production mode (`debug_mode = false`) the AWS Nitro CA chain is
///    validated and the COSE signature verified, exactly like
///    [`verify_against`] / [`verify_and_extract`].
/// 2. Nonce equals `base64(handshake_hash)`: the document is bound to
///    *this* Noise session, so a document captured from any other
///    session (including the mesh and other customers' sessions) is
///    rejected.
/// 3. The document's PCR0/1/2 equal one of `expected` EXACTLY. An empty
///    `expected` admits nothing. The comparison is mandatory and lives
///    here (not at the caller) so no public API exists that verifies a
///    document without committing to an identity; a bare
///    verify-and-return-PCRs form would be passable by ANY enclave,
///    including a reflection of the caller's own document.
///
/// Differs from [`verify_against`] in accepting a SET of valid triples
/// (a deployment may roll between two cluster images), and from
/// [`verify_and_extract`] in not requiring (or reading) `user_data`:
/// the server side of the customer protocol carries no control pubkey,
/// so demanding one would force the server to stuff a meaningless value
/// into the document.
pub fn verify_and_extract_pcrs(
    attestation_data: &[u8],
    handshake_hash: &[u8],
    expected: &[Pcrs],
    debug_mode: bool,
) -> Result<Pcrs, AttestationError> {
    let doc = parse_and_validate(attestation_data, debug_mode)?;

    check_nonce(&doc, handshake_hash)?;

    let hex_pcrs = att_get_pcrs(&doc).map_err(|e| AttestationError::Validation(e.to_string()))?;

    let pcrs = Pcrs {
        pcr0: decode_pcr(&hex_pcrs.pcr_0, 0)?,
        pcr1: decode_pcr(&hex_pcrs.pcr_1, 1)?,
        pcr2: decode_pcr(&hex_pcrs.pcr_2, 2)?,
    };
    if !expected.iter().any(|e| e == &pcrs) {
        return Err(AttestationError::PcrsNotExpected);
    }
    Ok(pcrs)
}

/// Extract PCR0/1/2 from an attestation document the caller JUST obtained from
/// its OWN `/dev/nsm`, WITHOUT verifying the certificate chain or the nonce.
///
/// # This is NOT a verification function. Read before using.
///
/// Every other entry point in this module (`verify_against`,
/// `verify_and_extract`, `verify_control_nonce_attestation`,
/// `verify_chain_attestation`) authenticates a document that came from SOMEONE
/// ELSE: in production it validates the AWS Nitro CA chain and the COSE
/// signature, and it binds the document to a live Noise session via the nonce.
/// This function does NONE of that. It only structurally decodes the COSE_Sign1
/// envelope and pulls out the PCRs. A document fed to it could be a forgery and
/// it would happily return whatever PCRs the forgery claims.
///
/// That is acceptable for, and ONLY for, one caller: a node deriving its OWN
/// self-PCR digest from a document it just requested from its OWN local
/// `/dev/nsm`. The local NSM device is inside the node's trusted computing base
/// (on real Nitro it is the hardware module measuring this very VM; under
/// QEMU's nitro-enclave machine it is the emulated module measuring the same),
/// so there is no cert chain to trust (the node is reading its own hardware
/// measurements, not authenticating a remote party) and there is no Noise
/// session to bind to (the node generated the request itself, with an arbitrary
/// nonce). This replaces a host-supplied PCR allowlist, which the host (the
/// adversary) could otherwise choose to admit a rogue image into the mesh.
///
/// Do NOT use this on a document received over the network, ever: use
/// [`verify_and_extract`] (peer attestation) or [`verify_against`] (pinned
/// identity) for that.
pub fn extract_own_pcrs(attestation_data: &[u8]) -> Result<Pcrs, AttestationError> {
    // Structural decode only: no cert chain, no signature, no nonce. The
    // `debug_mode = true` arm of `parse_and_validate` is exactly this
    // (decode_attestation_document), and it is correct here on BOTH QEMU and
    // real Nitro because the caller is reading its own local device, not
    // authenticating a remote party.
    let doc = parse_and_validate(attestation_data, true)?;
    let hex_pcrs = att_get_pcrs(&doc).map_err(|e| AttestationError::Validation(e.to_string()))?;
    Ok(Pcrs {
        pcr0: decode_pcr(&hex_pcrs.pcr_0, 0)?,
        pcr1: decode_pcr(&hex_pcrs.pcr_1, 1)?,
        pcr2: decode_pcr(&hex_pcrs.pcr_2, 2)?,
    })
}

/// Verify a chain-link attestation document.
///
/// Used by the backend's `POST /enclaves/{id}/chain-links` ingest
/// path: each chain link (`boot`, `upgrade`, `revocation`) carries a
/// hardware-signed `attestation` whose `user_data` field commits to the
/// link's `payload` via `sha256(payload)`. This function performs the
/// minimum-trust check required at ingest:
///
/// 1. Parse + structural validation of the COSE_Sign1 wrapper (same as
///    [`verify_against`] / [`verify_and_extract`]).
/// 2. `attestation.user_data == sha256(payload)` — the binding that
///    makes the chain entry tamper-evident.
/// 3. PCR0/1/2 in the doc equal `expected_pcrs` (the backend's recorded
///    PCRs for this enclave, post-build).
///
/// In production mode (`debug_mode = false`), the AWS Nitro CA chain is
/// validated and the COSE signature is verified by the upstream
/// `attestation-doc-validation` crate, same as the existing entry
/// points. In `debug_mode`, only structural validity is required —
/// matching QEMU's emulated NSM device, which signs documents with its
/// own key instead of the AWS CA (and the `test-utils` doc builders,
/// which carry placeholder signatures).
///
/// The doc's `nonce` field is **not** checked here. The chain-link
/// attestations are not produced in the context of a Noise session, so
/// there is no handshake hash to bind against; the binding lives in
/// `user_data` instead. Any value in `nonce` is accepted.
pub fn verify_chain_attestation(
    attestation_data: &[u8],
    payload: &[u8],
    expected_pcrs: &Pcrs,
    debug_mode: bool,
) -> Result<(), AttestationError> {
    let pcrs_hex = PcrsHex::from_pcrs(expected_pcrs);
    let doc = parse_and_validate(attestation_data, debug_mode)?;

    let user_data = doc
        .user_data
        .as_ref()
        .ok_or(AttestationError::PayloadBindingMismatch)?;
    let expected: [u8; 32] = {
        let mut hasher = Sha256::new();
        hasher.update(payload);
        hasher.finalize().into()
    };
    if user_data.as_slice() != expected {
        return Err(AttestationError::PayloadBindingMismatch);
    }

    validate_expected_pcrs(&doc, &pcrs_hex)
        .map_err(|e| AttestationError::Validation(e.to_string()))?;

    Ok(())
}

fn parse_and_validate(
    attestation_data: &[u8],
    debug_mode: bool,
) -> Result<AttestationDoc, AttestationError> {
    if debug_mode {
        let (_, doc) = decode_attestation_document(attestation_data)
            .map_err(|e| AttestationError::Validation(e.to_string()))?;
        Ok(doc)
    } else {
        validate_and_parse_attestation_doc(attestation_data)
            .map_err(|e| AttestationError::Validation(e.to_string()))
    }
}

fn check_nonce(doc: &AttestationDoc, handshake_hash: &[u8]) -> Result<(), AttestationError> {
    let nonce_b64 = base64::engine::general_purpose::STANDARD.encode(handshake_hash);
    validate_expected_nonce(doc, &nonce_b64)
        .map_err(|e| AttestationError::Validation(e.to_string()))
}

fn decode_pcr(hex_str: &str, idx: usize) -> Result<Vec<u8>, AttestationError> {
    let bytes = hex::decode(hex_str).map_err(|_| AttestationError::InvalidPcrHex(idx))?;
    if ![32usize, 48, 64].contains(&bytes.len()) {
        return Err(AttestationError::InvalidPcrLength {
            idx,
            len: bytes.len(),
        });
    }
    Ok(bytes)
}

/// Internal hex-encoded view of a [`Pcrs`] for the `PCRProvider` trait.
/// The upstream crate compares PCRs by string equality on hex
/// representations, so we encode once at the entry point.
struct PcrsHex {
    pcr0: String,
    pcr1: String,
    pcr2: String,
}

impl PcrsHex {
    fn from_pcrs(pcrs: &Pcrs) -> Self {
        Self {
            pcr0: hex::encode(&pcrs.pcr0),
            pcr1: hex::encode(&pcrs.pcr1),
            pcr2: hex::encode(&pcrs.pcr2),
        }
    }
}

impl PCRProvider for PcrsHex {
    fn pcr_0(&self) -> Option<&str> {
        Some(&self.pcr0)
    }
    fn pcr_1(&self) -> Option<&str> {
        Some(&self.pcr1)
    }
    fn pcr_2(&self) -> Option<&str> {
        Some(&self.pcr2)
    }
    fn pcr_8(&self) -> Option<&str> {
        None
    }
}

/// Test-only helpers for constructing attestation documents with known
/// PCRs and nonces. Behind the `test-utils` feature so downstream test
/// suites can build doc fixtures without spinning up real Nitro
/// hardware. Production builds cannot reach this module.
#[cfg(any(test, feature = "test-utils"))]
pub mod test_utils {
    use std::collections::BTreeMap;

    use aws_nitro_enclaves_nsm_api::api::{AttestationDoc, Digest};
    use ciborium::value::Value as CborValue;

    /// Builder for synthetic attestation documents accepted by
    /// [`verify_against`](super::verify_against) /
    /// [`verify_and_extract`](super::verify_and_extract) in debug mode.
    ///
    /// In debug mode the COSE signature is not validated, so any
    /// well-formed COSE_Sign1 envelope around a well-formed
    /// [`AttestationDoc`] is accepted. PCR0/1/2 are 48-byte SHA-384
    /// values (matches what real Nitro hardware emits).
    pub struct FakeAttestation {
        pub pcr0: Vec<u8>,
        pub pcr1: Vec<u8>,
        pub pcr2: Vec<u8>,
        /// Raw Noise handshake hash. The encoded doc's `nonce` field is
        /// set to these bytes verbatim — the verifier base64-encodes
        /// before comparing, so it works out.
        pub handshake_hash: Vec<u8>,
        /// 65-byte uncompressed SEC1 ECDSA P-256 verifying key. Encoded
        /// into the doc's `user_data` field — [`super::verify_and_extract`]
        /// requires this to be a 65-byte pubkey with the SEC1 prefix
        /// `0x04`.
        pub control_pubkey: [u8; super::CONTROL_PUBKEY_LEN],
    }

    impl FakeAttestation {
        /// Build a fixture with all three PCRs derived from `seed` and a
        /// synthetic but structurally-valid SEC1 control pubkey (prefix
        /// `0x04`, the remaining 64 bytes filled with `seed | 0x80`).
        /// The synthetic pubkey will NOT decode as a valid P-256 point,
        /// so tests that only need to exercise the verifier's
        /// length-and-prefix check can use this directly; tests that
        /// need a *real* P-256 keypair (to actually sign) should use
        /// [`Self::with_seed_and_pubkey`] with bytes from a
        /// `p256::ecdsa::SigningKey`.
        pub fn with_seed(seed: u8, handshake_hash: Vec<u8>) -> Self {
            let mut control_pubkey = [seed.wrapping_add(0x80); super::CONTROL_PUBKEY_LEN];
            control_pubkey[0] = 0x04;
            Self {
                pcr0: vec![seed; 48],
                pcr1: vec![seed.wrapping_add(1); 48],
                pcr2: vec![seed.wrapping_add(2); 48],
                handshake_hash,
                control_pubkey,
            }
        }

        /// Like [`Self::with_seed`] but with a caller-supplied control
        /// pubkey (typically `VerifyingKey::to_encoded_point(false)` from
        /// a real `p256::ecdsa::SigningKey` the test holds for signing).
        pub fn with_seed_and_pubkey(
            seed: u8,
            handshake_hash: Vec<u8>,
            control_pubkey: [u8; super::CONTROL_PUBKEY_LEN],
        ) -> Self {
            let mut fake = Self::with_seed(seed, handshake_hash);
            fake.control_pubkey = control_pubkey;
            fake
        }

        /// CBOR-encoded COSE_Sign1 bytes ready to pass through the
        /// `debug_mode` verify path.
        pub fn encode(&self) -> Vec<u8> {
            assert_eq!(self.pcr0.len(), 48, "test PCRs must be 48 bytes (SHA-384)");
            assert_eq!(self.pcr1.len(), 48, "test PCRs must be 48 bytes (SHA-384)");
            assert_eq!(self.pcr2.len(), 48, "test PCRs must be 48 bytes (SHA-384)");

            let mut pcrs = BTreeMap::new();
            pcrs.insert(0usize, self.pcr0.clone());
            pcrs.insert(1usize, self.pcr1.clone());
            pcrs.insert(2usize, self.pcr2.clone());
            // The upstream `get_pcrs` is hard-coded to require PCR8
            // (signing-cert measurement). Synchronizer doesn't use it,
            // but the doc has to include it to deserialize.
            pcrs.insert(8usize, vec![0u8; 48]);

            let doc = AttestationDoc::new(
                "test-module".to_string(),
                Digest::SHA384,
                0,
                pcrs,
                // certificate / cabundle: not validated in debug mode,
                // but `validate_attestation_document_structure` does
                // require each cert byte slice to be 1..=1024 bytes.
                vec![0u8; 64],
                vec![vec![0u8; 64]],
                Some(self.control_pubkey.to_vec()),
                Some(self.handshake_hash.clone()),
                None,
            );

            let mut payload = Vec::new();
            ciborium::into_writer(&doc, &mut payload).expect("ciborium encode AttestationDoc");

            // COSE_Sign1, untagged: [protected: bstr, unprotected: map, payload: bstr, signature: bstr].
            // - protected is a *byte string* whose contents are a serialized HeaderMap.
            //   An empty CBOR map is one byte: 0xa0.
            let cose = CborValue::Array(vec![
                CborValue::Bytes(vec![0xa0]),
                CborValue::Map(Vec::new()),
                CborValue::Bytes(payload),
                // Signature: junk. The debug-mode verify path does not
                // touch it (and even production verify only fails if the
                // cert chain is wrong, which it always will be for
                // synthetic docs).
                CborValue::Bytes(vec![0u8; 96]),
            ]);

            let mut out = Vec::new();
            ciborium::into_writer(&cose, &mut out).expect("ciborium encode COSE_Sign1");
            out
        }
    }

    /// Builder for synthetic control-nonce attestation documents
    /// accepted by
    /// [`verify_control_nonce_attestation`](super::verify_control_nonce_attestation)
    /// in debug mode. Mirrors the in-enclave server's
    /// `RequestAttestation` reply shape: `nonce` carries the Noise
    /// handshake hash, `user_data` carries the 32-byte control nonce.
    pub struct FakeControlNonceAttestation {
        pub pcr0: Vec<u8>,
        pub pcr1: Vec<u8>,
        pub pcr2: Vec<u8>,
        /// Raw Noise handshake hash, encoded verbatim into the doc's
        /// `nonce` field (the verifier base64-encodes before comparing).
        pub handshake_hash: Vec<u8>,
        /// Encoded into the doc's `user_data` field. 32 bytes on the
        /// happy path; tests exercising the length check can override.
        pub control_nonce: Vec<u8>,
    }

    impl FakeControlNonceAttestation {
        /// Build a fixture with all three PCRs derived from `seed`.
        pub fn with_seed(seed: u8, handshake_hash: Vec<u8>, control_nonce: [u8; 32]) -> Self {
            Self {
                pcr0: vec![seed; 48],
                pcr1: vec![seed.wrapping_add(1); 48],
                pcr2: vec![seed.wrapping_add(2); 48],
                handshake_hash,
                control_nonce: control_nonce.to_vec(),
            }
        }

        /// CBOR-encoded COSE_Sign1 bytes ready to pass through the
        /// `debug_mode` verify path.
        pub fn encode(&self) -> Vec<u8> {
            assert_eq!(self.pcr0.len(), 48, "test PCRs must be 48 bytes (SHA-384)");
            assert_eq!(self.pcr1.len(), 48, "test PCRs must be 48 bytes (SHA-384)");
            assert_eq!(self.pcr2.len(), 48, "test PCRs must be 48 bytes (SHA-384)");

            let mut pcrs = BTreeMap::new();
            pcrs.insert(0usize, self.pcr0.clone());
            pcrs.insert(1usize, self.pcr1.clone());
            pcrs.insert(2usize, self.pcr2.clone());
            pcrs.insert(8usize, vec![0u8; 48]);

            let doc = AttestationDoc::new(
                "test-module".to_string(),
                Digest::SHA384,
                0,
                pcrs,
                vec![0u8; 64],
                vec![vec![0u8; 64]],
                Some(self.control_nonce.clone()),
                Some(self.handshake_hash.clone()),
                None,
            );

            let mut payload = Vec::new();
            ciborium::into_writer(&doc, &mut payload).expect("ciborium encode AttestationDoc");

            let cose = CborValue::Array(vec![
                CborValue::Bytes(vec![0xa0]),
                CborValue::Map(Vec::new()),
                CborValue::Bytes(payload),
                CborValue::Bytes(vec![0u8; 96]),
            ]);

            let mut out = Vec::new();
            ciborium::into_writer(&cose, &mut out).expect("ciborium encode COSE_Sign1");
            out
        }
    }

    /// Builder for synthetic chain-link attestation documents accepted
    /// by [`verify_chain_attestation`](super::verify_chain_attestation)
    /// in debug mode. Differs from [`FakeAttestation`] in two ways:
    ///   * `user_data` carries the SHA-256 of a caller-supplied
    ///     `payload` (not the control pubkey, which the chain ingest
    ///     path doesn't read).
    ///   * `nonce` is irrelevant to the chain ingest verifier and is
    ///     populated with a fixed zero-padded value so the doc still
    ///     serialises.
    pub struct FakeChainAttestation {
        pub pcr0: Vec<u8>,
        pub pcr1: Vec<u8>,
        pub pcr2: Vec<u8>,
        /// 32-byte SHA-256 of the chain link's payload. Set by
        /// [`Self::for_payload`]; tests that want to exercise a
        /// `user_data` mismatch can override after construction.
        pub user_data: Vec<u8>,
    }

    impl FakeChainAttestation {
        /// Build a fixture with all three PCRs derived from `seed` and
        /// `user_data` set to `sha256(payload)`. Drop-in for the chain
        /// ingest verifier's happy path.
        pub fn for_payload(seed: u8, payload: &[u8]) -> Self {
            use sha2::Digest as _;
            let mut hasher = sha2::Sha256::new();
            hasher.update(payload);
            let user_data: Vec<u8> = hasher.finalize().to_vec();
            Self {
                pcr0: vec![seed; 48],
                pcr1: vec![seed.wrapping_add(1); 48],
                pcr2: vec![seed.wrapping_add(2); 48],
                user_data,
            }
        }

        /// CBOR-encoded COSE_Sign1 bytes ready to pass through the
        /// `debug_mode` chain-attestation verify path.
        pub fn encode(&self) -> Vec<u8> {
            assert_eq!(self.pcr0.len(), 48, "test PCRs must be 48 bytes (SHA-384)");
            assert_eq!(self.pcr1.len(), 48, "test PCRs must be 48 bytes (SHA-384)");
            assert_eq!(self.pcr2.len(), 48, "test PCRs must be 48 bytes (SHA-384)");

            let mut pcrs = BTreeMap::new();
            pcrs.insert(0usize, self.pcr0.clone());
            pcrs.insert(1usize, self.pcr1.clone());
            pcrs.insert(2usize, self.pcr2.clone());
            pcrs.insert(8usize, vec![0u8; 48]);

            let doc = AttestationDoc::new(
                "test-module".to_string(),
                Digest::SHA384,
                0,
                pcrs,
                vec![0u8; 64],
                vec![vec![0u8; 64]],
                Some(self.user_data.clone()),
                // Nonce is not consulted by `verify_chain_attestation`,
                // but the doc has to carry one to serialise. Zero-padded
                // to a length the structure-validator accepts.
                Some(vec![0u8; 32]),
                None,
            );

            let mut payload = Vec::new();
            ciborium::into_writer(&doc, &mut payload).expect("ciborium encode AttestationDoc");

            let cose = CborValue::Array(vec![
                CborValue::Bytes(vec![0xa0]),
                CborValue::Map(Vec::new()),
                CborValue::Bytes(payload),
                CborValue::Bytes(vec![0u8; 96]),
            ]);

            let mut out = Vec::new();
            ciborium::into_writer(&cose, &mut out).expect("ciborium encode COSE_Sign1");
            out
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn hh() -> Vec<u8> {
        // 32-byte BLAKE2s-shaped handshake hash for tests.
        (0u8..32).collect()
    }

    #[test]
    fn verify_and_extract_returns_doc_identity_in_debug_mode() {
        let fake = test_utils::FakeAttestation::with_seed(0x11, hh());
        let bytes = fake.encode();

        let identity = verify_and_extract(&bytes, &hh(), true).expect("verify");
        assert_eq!(identity.pcrs.pcr0, fake.pcr0);
        assert_eq!(identity.pcrs.pcr1, fake.pcr1);
        assert_eq!(identity.pcrs.pcr2, fake.pcr2);
        assert_eq!(identity.control_pubkey, fake.control_pubkey);
    }

    #[test]
    fn extract_own_pcrs_returns_doc_pcrs_without_nonce_or_chain() {
        // A document the node "just got from its own /dev/nsm" (here a
        // FakeAttestation fixture). extract_own_pcrs must return its PCR0/1/2
        // verbatim with no nonce/cert-chain check, so the node can derive its
        // own self-PCR digest regardless of the throwaway/self-signed key.
        let fake = test_utils::FakeAttestation::with_seed(0x5a, hh());
        let bytes = fake.encode();

        let pcrs = extract_own_pcrs(&bytes).expect("extract own pcrs");
        assert_eq!(pcrs.pcr0, fake.pcr0);
        assert_eq!(pcrs.pcr1, fake.pcr1);
        assert_eq!(pcrs.pcr2, fake.pcr2);

        // The digest matches what verify_and_extract derives for the same doc,
        // i.e. it is the SAME identity a peer would compute, just without the
        // verification a peer document requires.
        let verified = verify_and_extract(&bytes, &hh(), true).expect("verify");
        assert_eq!(pcrs.digest(), verified.pcrs.digest());
    }

    #[test]
    fn extract_own_pcrs_ignores_the_nonce_entirely() {
        // Unlike verify_and_extract, extract_own_pcrs takes no handshake hash
        // and never inspects the nonce: a doc minted with one nonce still
        // yields its PCRs. (The node mints the request itself with an arbitrary
        // nonce; there is no session to bind to.)
        let fake = test_utils::FakeAttestation::with_seed(0x77, vec![0xde; 32]);
        let pcrs = extract_own_pcrs(&fake.encode()).expect("extract own pcrs");
        assert_eq!(pcrs.pcr0, fake.pcr0);
    }

    #[test]
    fn extract_own_pcrs_rejects_garbage_bytes() {
        let err = extract_own_pcrs(b"not a cose document").unwrap_err();
        assert!(
            matches!(err, AttestationError::Validation(_)),
            "expected Validation, got {err:?}"
        );
    }

    #[test]
    fn verify_control_nonce_attestation_returns_attested_nonce() {
        let nonce = [0xab; 32];
        let fake = test_utils::FakeControlNonceAttestation::with_seed(0x21, hh(), nonce);
        let expected = Pcrs {
            pcr0: fake.pcr0.clone(),
            pcr1: fake.pcr1.clone(),
            pcr2: fake.pcr2.clone(),
        };

        let got = verify_control_nonce_attestation(&fake.encode(), &hh(), &expected, true)
            .expect("verify");
        assert_eq!(got, nonce);
    }

    #[test]
    fn verify_control_nonce_attestation_rejects_wrong_pcrs() {
        let fake = test_utils::FakeControlNonceAttestation::with_seed(0x21, hh(), [0xab; 32]);
        let wrong = Pcrs {
            pcr0: vec![0xff; 48],
            pcr1: fake.pcr1.clone(),
            pcr2: fake.pcr2.clone(),
        };

        let err =
            verify_control_nonce_attestation(&fake.encode(), &hh(), &wrong, true).unwrap_err();
        assert!(matches!(err, AttestationError::Validation(_)), "{err}");
    }

    #[test]
    fn verify_control_nonce_attestation_rejects_wrong_handshake_hash() {
        let fake = test_utils::FakeControlNonceAttestation::with_seed(0x21, hh(), [0xab; 32]);
        let expected = Pcrs {
            pcr0: fake.pcr0.clone(),
            pcr1: fake.pcr1.clone(),
            pcr2: fake.pcr2.clone(),
        };
        let other_hh: Vec<u8> = (100u8..132).collect();

        let err = verify_control_nonce_attestation(&fake.encode(), &other_hh, &expected, true)
            .unwrap_err();
        assert!(matches!(err, AttestationError::Validation(_)), "{err}");
    }

    #[test]
    fn verify_control_nonce_attestation_rejects_non_32_byte_user_data() {
        let mut fake = test_utils::FakeControlNonceAttestation::with_seed(0x21, hh(), [0xab; 32]);
        fake.control_nonce = vec![0xab; 16]; // wrong length
        let expected = Pcrs {
            pcr0: fake.pcr0.clone(),
            pcr1: fake.pcr1.clone(),
            pcr2: fake.pcr2.clone(),
        };

        let err =
            verify_control_nonce_attestation(&fake.encode(), &hh(), &expected, true).unwrap_err();
        assert!(
            matches!(err, AttestationError::InvalidControlNonce),
            "{err}"
        );
    }

    #[test]
    fn verify_and_extract_rejects_doc_without_user_data() {
        // Build a doc with `user_data: None` by constructing it directly,
        // since `FakeAttestation::encode` always populates user_data.
        use aws_nitro_enclaves_nsm_api::api::{AttestationDoc, Digest};
        use ciborium::value::Value as CborValue;
        use std::collections::BTreeMap;

        let mut pcrs = BTreeMap::new();
        pcrs.insert(0usize, vec![0x11u8; 48]);
        pcrs.insert(1usize, vec![0x12u8; 48]);
        pcrs.insert(2usize, vec![0x13u8; 48]);
        pcrs.insert(8usize, vec![0u8; 48]);

        let doc = AttestationDoc::new(
            "test-module".to_string(),
            Digest::SHA384,
            0,
            pcrs,
            vec![0u8; 64],
            vec![vec![0u8; 64]],
            None, // user_data missing — the case under test.
            Some(hh()),
            None,
        );

        let mut payload = Vec::new();
        ciborium::into_writer(&doc, &mut payload).unwrap();
        let cose = CborValue::Array(vec![
            CborValue::Bytes(vec![0xa0]),
            CborValue::Map(Vec::new()),
            CborValue::Bytes(payload),
            CborValue::Bytes(vec![0u8; 96]),
        ]);
        let mut bytes = Vec::new();
        ciborium::into_writer(&cose, &mut bytes).unwrap();

        let err = verify_and_extract(&bytes, &hh(), true).unwrap_err();
        assert!(
            matches!(err, AttestationError::InvalidControlPubkey),
            "expected InvalidControlPubkey, got {err:?}"
        );
    }

    #[test]
    fn verify_and_extract_rejects_doc_with_wrong_size_user_data() {
        let mut fake = test_utils::FakeAttestation::with_seed(0x22, hh());
        // Override user_data via the `control_pubkey` field by encoding
        // a longer payload — done by reaching directly into the struct
        // and re-encoding manually. Easier: build the doc inline with a
        // 16-byte user_data.
        use aws_nitro_enclaves_nsm_api::api::{AttestationDoc, Digest};
        use ciborium::value::Value as CborValue;
        use std::collections::BTreeMap;
        let _ = &mut fake;

        let mut pcrs = BTreeMap::new();
        pcrs.insert(0usize, vec![0x22u8; 48]);
        pcrs.insert(1usize, vec![0x23u8; 48]);
        pcrs.insert(2usize, vec![0x24u8; 48]);
        pcrs.insert(8usize, vec![0u8; 48]);

        let doc = AttestationDoc::new(
            "test-module".to_string(),
            Digest::SHA384,
            0,
            pcrs,
            vec![0u8; 64],
            vec![vec![0u8; 64]],
            Some(vec![0u8; 16]), // 16 bytes is the wrong size.
            Some(hh()),
            None,
        );

        let mut payload = Vec::new();
        ciborium::into_writer(&doc, &mut payload).unwrap();
        let cose = CborValue::Array(vec![
            CborValue::Bytes(vec![0xa0]),
            CborValue::Map(Vec::new()),
            CborValue::Bytes(payload),
            CborValue::Bytes(vec![0u8; 96]),
        ]);
        let mut bytes = Vec::new();
        ciborium::into_writer(&cose, &mut bytes).unwrap();

        let err = verify_and_extract(&bytes, &hh(), true).unwrap_err();
        assert!(
            matches!(err, AttestationError::InvalidControlPubkey),
            "expected InvalidControlPubkey, got {err:?}"
        );
    }

    /// The expected-PCR triple matching `FakeAttestation::with_seed(seed)`.
    fn seed_pcrs(seed: u8) -> Pcrs {
        Pcrs {
            pcr0: vec![seed; 48],
            pcr1: vec![seed.wrapping_add(1); 48],
            pcr2: vec![seed.wrapping_add(2); 48],
        }
    }

    #[test]
    fn verify_and_extract_pcrs_returns_doc_pcrs_in_debug_mode() {
        let fake = test_utils::FakeAttestation::with_seed(0x66, hh());
        let pcrs = verify_and_extract_pcrs(&fake.encode(), &hh(), &[seed_pcrs(0x66)], true)
            .expect("verify");
        assert_eq!(pcrs.pcr0, fake.pcr0);
        assert_eq!(pcrs.pcr1, fake.pcr1);
        assert_eq!(pcrs.pcr2, fake.pcr2);
    }

    #[test]
    fn verify_and_extract_pcrs_rejects_unexpected_pcrs() {
        let fake = test_utils::FakeAttestation::with_seed(0x66, hh());
        let err =
            verify_and_extract_pcrs(&fake.encode(), &hh(), &[seed_pcrs(0x99)], true).unwrap_err();
        assert!(
            matches!(err, AttestationError::PcrsNotExpected),
            "expected PcrsNotExpected, got {err:?}"
        );
        // An empty expected set admits nothing.
        let err = verify_and_extract_pcrs(&fake.encode(), &hh(), &[], true).unwrap_err();
        assert!(
            matches!(err, AttestationError::PcrsNotExpected),
            "expected PcrsNotExpected, got {err:?}"
        );
    }

    #[test]
    fn verify_and_extract_pcrs_rejects_wrong_handshake_hash() {
        let fake = test_utils::FakeAttestation::with_seed(0x67, hh());
        let wrong: Vec<u8> = vec![0xab; 32];
        let err =
            verify_and_extract_pcrs(&fake.encode(), &wrong, &[seed_pcrs(0x67)], true).unwrap_err();
        assert!(
            matches!(err, AttestationError::Validation(_)),
            "expected Validation, got {err:?}"
        );
    }

    #[test]
    fn verify_and_extract_pcrs_rejects_garbage_bytes() {
        let err = verify_and_extract_pcrs(b"not a cose document", &hh(), &[seed_pcrs(0x66)], true)
            .unwrap_err();
        assert!(
            matches!(err, AttestationError::Validation(_)),
            "expected Validation, got {err:?}"
        );
    }

    #[test]
    fn verify_and_extract_pcrs_accepts_doc_without_user_data() {
        // The server side of the customer protocol carries no control
        // pubkey, so the doc may legitimately omit user_data (a real
        // /dev/nsm request with user_data = None). Build one inline,
        // since FakeAttestation always populates user_data.
        use aws_nitro_enclaves_nsm_api::api::{AttestationDoc, Digest};
        use ciborium::value::Value as CborValue;
        use std::collections::BTreeMap;

        let mut pcrs = BTreeMap::new();
        pcrs.insert(0usize, vec![0x68u8; 48]);
        pcrs.insert(1usize, vec![0x69u8; 48]);
        pcrs.insert(2usize, vec![0x6au8; 48]);
        pcrs.insert(8usize, vec![0u8; 48]);

        let doc = AttestationDoc::new(
            "test-module".to_string(),
            Digest::SHA384,
            0,
            pcrs,
            vec![0u8; 64],
            vec![vec![0u8; 64]],
            None, // no user_data: must still verify.
            Some(hh()),
            None,
        );

        let mut payload = Vec::new();
        ciborium::into_writer(&doc, &mut payload).unwrap();
        let cose = CborValue::Array(vec![
            CborValue::Bytes(vec![0xa0]),
            CborValue::Map(Vec::new()),
            CborValue::Bytes(payload),
            CborValue::Bytes(vec![0u8; 96]),
        ]);
        let mut bytes = Vec::new();
        ciborium::into_writer(&cose, &mut bytes).unwrap();

        let pcrs =
            verify_and_extract_pcrs(&bytes, &hh(), &[seed_pcrs(0x68)], true).expect("verify");
        assert_eq!(pcrs.pcr0, vec![0x68u8; 48]);
    }

    #[test]
    fn verify_against_accepts_matching_pcrs_in_debug_mode() {
        let fake = test_utils::FakeAttestation::with_seed(0x22, hh());
        let bytes = fake.encode();
        let expected = Pcrs {
            pcr0: fake.pcr0.clone(),
            pcr1: fake.pcr1.clone(),
            pcr2: fake.pcr2.clone(),
        };
        verify_against(&bytes, &hh(), &expected, true).expect("verify");
    }

    #[test]
    fn verify_against_rejects_mismatched_pcrs() {
        let fake = test_utils::FakeAttestation::with_seed(0x33, hh());
        let bytes = fake.encode();
        let expected = Pcrs {
            pcr0: vec![0xff; 48],
            pcr1: fake.pcr1.clone(),
            pcr2: fake.pcr2.clone(),
        };
        let err = verify_against(&bytes, &hh(), &expected, true).unwrap_err();
        assert!(
            matches!(err, AttestationError::Validation(_)),
            "expected Validation, got {err:?}"
        );
    }

    #[test]
    fn verify_rejects_wrong_handshake_hash() {
        let fake = test_utils::FakeAttestation::with_seed(0x44, hh());
        let bytes = fake.encode();
        let wrong: Vec<u8> = vec![0xab; 32];
        let err = verify_and_extract(&bytes, &wrong, true).unwrap_err();
        assert!(
            matches!(err, AttestationError::Validation(_)),
            "expected Validation, got {err:?}"
        );
    }

    #[test]
    fn digest_is_sha256_of_concatenated_pcrs() {
        let pcrs = Pcrs {
            pcr0: vec![0x01; 48],
            pcr1: vec![0x02; 48],
            pcr2: vec![0x03; 48],
        };
        let mut hasher = Sha256::new();
        hasher.update(&pcrs.pcr0);
        hasher.update(&pcrs.pcr1);
        hasher.update(&pcrs.pcr2);
        let expected: [u8; 32] = hasher.finalize().into();
        assert_eq!(pcrs.digest(), expected);
    }

    fn pcrs_from_seed(seed: u8) -> Pcrs {
        Pcrs {
            pcr0: vec![seed; 48],
            pcr1: vec![seed.wrapping_add(1); 48],
            pcr2: vec![seed.wrapping_add(2); 48],
        }
    }

    #[test]
    fn verify_chain_attestation_accepts_well_formed_link_in_debug_mode() {
        let payload = b"chain-link-payload-canary".to_vec();
        let fake = test_utils::FakeChainAttestation::for_payload(0x33, &payload);
        let bytes = fake.encode();
        let expected_pcrs = pcrs_from_seed(0x33);

        verify_chain_attestation(&bytes, &payload, &expected_pcrs, true)
            .expect("valid chain attestation must pass");
    }

    #[test]
    fn verify_chain_attestation_rejects_mismatched_payload_binding() {
        let payload = b"chain-link-payload-canary".to_vec();
        let fake = test_utils::FakeChainAttestation::for_payload(0x44, &payload);
        let bytes = fake.encode();
        let expected_pcrs = pcrs_from_seed(0x44);

        // Same attestation, different payload — user_data binds to the
        // original, so the verifier must reject the substitution.
        let err = verify_chain_attestation(&bytes, b"DIFFERENT", &expected_pcrs, true)
            .expect_err("payload swap must fail the binding check");
        assert!(
            matches!(err, AttestationError::PayloadBindingMismatch),
            "expected PayloadBindingMismatch, got {err:?}"
        );
    }

    #[test]
    fn verify_chain_attestation_rejects_pcr_mismatch() {
        let payload = b"chain-link-payload-canary".to_vec();
        let fake = test_utils::FakeChainAttestation::for_payload(0x55, &payload);
        let bytes = fake.encode();
        // Wrong expected PCRs — the caller's recorded PCRs disagree with
        // what the doc carries. Verifier must reject.
        let mismatched_pcrs = pcrs_from_seed(0x99);

        let err = verify_chain_attestation(&bytes, &payload, &mismatched_pcrs, true)
            .expect_err("PCR mismatch must fail");
        assert!(
            matches!(err, AttestationError::Validation(_)),
            "expected Validation error, got {err:?}"
        );
    }

    #[test]
    fn verify_chain_attestation_rejects_doc_without_user_data() {
        use aws_nitro_enclaves_nsm_api::api::{AttestationDoc, Digest};
        use ciborium::value::Value as CborValue;
        use std::collections::BTreeMap;

        let payload = b"any-payload".to_vec();
        let pcrs = pcrs_from_seed(0x77);

        let mut pcr_map = BTreeMap::new();
        pcr_map.insert(0usize, pcrs.pcr0.clone());
        pcr_map.insert(1usize, pcrs.pcr1.clone());
        pcr_map.insert(2usize, pcrs.pcr2.clone());
        pcr_map.insert(8usize, vec![0u8; 48]);

        let doc = AttestationDoc::new(
            "test-module".to_string(),
            Digest::SHA384,
            0,
            pcr_map,
            vec![0u8; 64],
            vec![vec![0u8; 64]],
            None, // user_data missing — the case under test.
            Some(vec![0u8; 32]),
            None,
        );

        let mut doc_bytes = Vec::new();
        ciborium::into_writer(&doc, &mut doc_bytes).unwrap();
        let cose = CborValue::Array(vec![
            CborValue::Bytes(vec![0xa0]),
            CborValue::Map(Vec::new()),
            CborValue::Bytes(doc_bytes),
            CborValue::Bytes(vec![0u8; 96]),
        ]);
        let mut bytes = Vec::new();
        ciborium::into_writer(&cose, &mut bytes).unwrap();

        let err = verify_chain_attestation(&bytes, &payload, &pcrs, true)
            .expect_err("missing user_data must be rejected");
        assert!(
            matches!(err, AttestationError::PayloadBindingMismatch),
            "expected PayloadBindingMismatch, got {err:?}"
        );
    }
}

#[cfg(test)]
mod non_upgradable_control_key_tests {
    use super::{CONTROL_PUBKEY_LEN, NON_UPGRADABLE_CONTROL_KEY, NON_UPGRADABLE_CONTROL_KEY_DST};
    use sha2::{Digest, Sha256};

    /// Re-run the try-and-increment derivation the baked constant came
    /// from: hash the DST with a 1-byte counter to a candidate
    /// x-coordinate and take the first that decompresses to a valid
    /// P-256 point. Test-only; the production value is the
    /// [`NON_UPGRADABLE_CONTROL_KEY`] constant, this just proves the
    /// constant equals its construction so the literal cannot drift.
    fn derive_non_upgradable_control_key() -> [u8; CONTROL_PUBKEY_LEN] {
        use p256::EncodedPoint;
        use p256::elliptic_curve::sec1::{FromEncodedPoint, ToEncodedPoint};

        for counter in 0u16..=255 {
            let mut hasher = Sha256::new();
            hasher.update(NON_UPGRADABLE_CONTROL_KEY_DST);
            hasher.update([counter as u8]);
            let x = hasher.finalize();
            let mut compressed = [0u8; 33];
            compressed[0] = 0x02; // even-y compressed SEC1
            compressed[1..].copy_from_slice(&x);
            let Ok(encoded) = EncodedPoint::from_bytes(compressed) else {
                continue;
            };
            let maybe_point = p256::AffinePoint::from_encoded_point(&encoded);
            if maybe_point.is_some().into() {
                let uncompressed = maybe_point.unwrap().to_encoded_point(false);
                let mut out = [0u8; CONTROL_PUBKEY_LEN];
                out.copy_from_slice(uncompressed.as_bytes());
                return out;
            }
        }
        panic!("no valid P-256 point found deriving the non-upgradable control key");
    }

    /// The baked constant must equal the live derivation. This is the
    /// audit anchor: a change to the DST or the derivation that is not
    /// mirrored into the constant trips here, forcing a deliberate
    /// review (changing the value would orphan every already-pinned
    /// non-upgradable enclave).
    #[test]
    fn constant_matches_derivation() {
        assert_eq!(
            NON_UPGRADABLE_CONTROL_KEY,
            derive_non_upgradable_control_key(),
            "baked non-upgradable control key drifted from its DST derivation"
        );
    }

    #[test]
    fn is_uncompressed_sec1_shape() {
        assert_eq!(NON_UPGRADABLE_CONTROL_KEY.len(), CONTROL_PUBKEY_LEN);
        assert_eq!(NON_UPGRADABLE_CONTROL_KEY[0], 0x04);
    }

    /// It must parse as a real P-256 verifying key, so `Register` and the
    /// `verify_transition_link` decode step accept it (the un-signability
    /// bites at the signature check, not at decode: a Transition cannot
    /// be rejected merely because the key looks malformed, it must be a
    /// well-formed key that simply no signature verifies against).
    #[test]
    fn parses_as_a_valid_verifying_key() {
        p256::ecdsa::VerifyingKey::from_sec1_bytes(NON_UPGRADABLE_CONTROL_KEY.as_slice())
            .expect("canonical non-upgradable control key must be a valid P-256 point");
    }
}