@cardanowall/sdk-ts 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -879,6 +879,15 @@ function hkdfSha2562(opts2) {
879
879
  }
880
880
  var MLKEM768X25519_ENC_LENGTH = 1120;
881
881
  var MLKEM768X25519_SEED_LENGTH2 = 32;
882
+ function mlkem768x25519Keygen2(seed) {
883
+ if (seed.length !== MLKEM768X25519_SEED_LENGTH2) {
884
+ throw new Error(
885
+ `mlkem768x25519 seed must be ${MLKEM768X25519_SEED_LENGTH2} bytes, got ${seed.length}`
886
+ );
887
+ }
888
+ const { secretKey, publicKey } = XWing.keygen(seed);
889
+ return { secretSeed: secretKey, publicKey };
890
+ }
882
891
  function mlkem768x25519Decapsulate(opts2) {
883
892
  if (opts2.secretSeed.length !== MLKEM768X25519_SEED_LENGTH2) {
884
893
  throw new Error(
@@ -921,14 +930,6 @@ var EciesSealedPoeError = class extends Error {
921
930
  this.code = code;
922
931
  }
923
932
  };
924
- function encodeCanonicalCbor(value) {
925
- return cbor2.encode(value, {
926
- cde: true,
927
- collapseBigInts: true,
928
- rejectDuplicateKeys: true,
929
- sortKeys: sorts.sortCoreDeterministic
930
- });
931
- }
932
933
  var CHUNK_MAX_BYTES = 64;
933
934
  function chunkKemCt(value) {
934
935
  if (value.length === 0) {
@@ -951,27 +952,114 @@ function joinKemCt(chunks) {
951
952
  }
952
953
  return out;
953
954
  }
954
- function slotsToMacCbor(slots, kem) {
955
- let value;
955
+ function canonicalizeSlots(slots, kem) {
956
956
  if (kem === "x25519") {
957
- value = slots.map((s) => ({ epk: s.epk, wrap: s.wrap }));
958
- } else {
959
- value = slots.map((s) => ({
960
- // Canonicalize the chunk boundaries before the MAC commits to them:
961
- // reassemble the logical ciphertext and re-split into canonical ≤ 64-byte
962
- // chunks. The on-wire `kem_ct` array is a transport detail (the Cardano
963
- // ledger's 64-byte metadatum cap), and a hostile or non-canonical chunking
964
- // ([1, 63, …] instead of [64, …]) reassembles to the SAME bytes — so the
965
- // MAC must be invariant to it. Committing to the verbatim wire chunks would
966
- // let an attacker re-chunk an honest envelope and break the slots_mac match
967
- // for an honest recipient. Honest (already-64B-chunked) records are
968
- // unchanged; a real byte flip still changes the reassembled bytes and is
969
- // still rejected.
970
- kem_ct: chunkKemCt(joinKemCt(s.kem_ct)),
971
- wrap: s.wrap
972
- }));
957
+ return slots.map((s) => ({ epk: s.epk, wrap: s.wrap }));
973
958
  }
974
- return encodeCanonicalCbor(value);
959
+ return slots.map((s) => ({
960
+ kem_ct: chunkKemCt(joinKemCt(s.kem_ct)),
961
+ wrap: s.wrap
962
+ }));
963
+ }
964
+ function encodeCanonicalCbor(value) {
965
+ return cbor2.encode(value, {
966
+ cde: true,
967
+ collapseBigInts: true,
968
+ rejectDuplicateKeys: true,
969
+ sortKeys: sorts.sortCoreDeterministic
970
+ });
971
+ }
972
+ var CARDANO_POE_SLOTS_TRANSCRIPT_PREFIX = new TextEncoder().encode(
973
+ "cardano-poe-slots-transcript-v1"
974
+ );
975
+ var CARDANO_POE_HKDF_INFO_PAYLOAD = new TextEncoder().encode(
976
+ "cardano-poe-payload-v1"
977
+ );
978
+ var CARDANO_POE_HKDF_INFO_PAYLOAD_PASSPHRASE = new TextEncoder().encode(
979
+ "cardano-poe-payload-passphrase-v1"
980
+ );
981
+ var CARDANO_POE_XWING_KEK_SALT_PREFIX = new TextEncoder().encode(
982
+ "cardano-poe-xwing-kek-salt-v1"
983
+ );
984
+ if (CARDANO_POE_SLOTS_TRANSCRIPT_PREFIX.length !== 31) {
985
+ throw new Error(
986
+ "CARDANO_POE_SLOTS_TRANSCRIPT_PREFIX byte-length invariant violated (expected 31)"
987
+ );
988
+ }
989
+ if (CARDANO_POE_HKDF_INFO_PAYLOAD.length !== 22) {
990
+ throw new Error("CARDANO_POE_HKDF_INFO_PAYLOAD byte-length invariant violated (expected 22)");
991
+ }
992
+ if (CARDANO_POE_HKDF_INFO_PAYLOAD_PASSPHRASE.length !== 33) {
993
+ throw new Error(
994
+ "CARDANO_POE_HKDF_INFO_PAYLOAD_PASSPHRASE byte-length invariant violated (expected 33)"
995
+ );
996
+ }
997
+ if (CARDANO_POE_XWING_KEK_SALT_PREFIX.length !== 29) {
998
+ throw new Error("CARDANO_POE_XWING_KEK_SALT_PREFIX byte-length invariant violated (expected 29)");
999
+ }
1000
+ var MAX_SLOTS = 1024;
1001
+ var MAX_DECODED_ENVELOPE_BYTES = 65536;
1002
+ var MAX_SEALED_PLAINTEXT = 274877906880;
1003
+ var MAX_SEALED_CIPHERTEXT = MAX_SEALED_PLAINTEXT + 16;
1004
+ function assertCiphertextWithinBound(ciphertextLength) {
1005
+ if (ciphertextLength >= MAX_SEALED_CIPHERTEXT) {
1006
+ throw new SealedPayloadTooLargeError(
1007
+ `ciphertext length ${ciphertextLength} is at or above the maximum sealed ciphertext size ${MAX_SEALED_CIPHERTEXT}`
1008
+ );
1009
+ }
1010
+ }
1011
+ var SealedPayloadTooLargeError = class extends Error {
1012
+ constructor(message) {
1013
+ super(message);
1014
+ this.name = "SealedPayloadTooLargeError";
1015
+ }
1016
+ };
1017
+ function computeSlotsHash(args) {
1018
+ const transcript = {
1019
+ scheme: 1,
1020
+ path: "slots",
1021
+ aead: "xchacha20-poly1305",
1022
+ kem: args.kem,
1023
+ nonce: args.nonce,
1024
+ slots: canonicalizeSlots(args.slots, args.kem)
1025
+ };
1026
+ const encoded = encodeCanonicalCbor(transcript);
1027
+ const message = new Uint8Array(CARDANO_POE_SLOTS_TRANSCRIPT_PREFIX.length + encoded.length);
1028
+ message.set(CARDANO_POE_SLOTS_TRANSCRIPT_PREFIX, 0);
1029
+ message.set(encoded, CARDANO_POE_SLOTS_TRANSCRIPT_PREFIX.length);
1030
+ return sha2_js.sha256(message);
1031
+ }
1032
+ function adContentSlots(args) {
1033
+ const ad = {
1034
+ scheme: 1,
1035
+ path: "slots",
1036
+ aead: "xchacha20-poly1305",
1037
+ kem: args.kem,
1038
+ nonce: args.nonce,
1039
+ slots_hash: args.slotsHash,
1040
+ slots_mac: args.slotsMac
1041
+ };
1042
+ return encodeCanonicalCbor(ad);
1043
+ }
1044
+ function slotsPayloadKey(args) {
1045
+ return hkdfSha2562({
1046
+ ikm: args.cek,
1047
+ salt: args.nonce,
1048
+ info: CARDANO_POE_HKDF_INFO_PAYLOAD,
1049
+ length: 32
1050
+ });
1051
+ }
1052
+ function xwingKekSalt(args) {
1053
+ const message = new Uint8Array(
1054
+ CARDANO_POE_XWING_KEK_SALT_PREFIX.length + args.kemCt.length + args.pubR.length
1055
+ );
1056
+ let offset = 0;
1057
+ message.set(CARDANO_POE_XWING_KEK_SALT_PREFIX, offset);
1058
+ offset += CARDANO_POE_XWING_KEK_SALT_PREFIX.length;
1059
+ message.set(args.kemCt, offset);
1060
+ offset += args.kemCt.length;
1061
+ message.set(args.pubR, offset);
1062
+ return sha2_js.sha256(message);
975
1063
  }
976
1064
  var CARDANO_POE_HKDF_INFO_KEK = new TextEncoder().encode("cardano-poe-kek-v1");
977
1065
  var CARDANO_POE_HKDF_INFO_KEK_MLKEM768X25519 = new TextEncoder().encode(
@@ -1017,6 +1105,13 @@ function concat2(a, b) {
1017
1105
  out.set(b, a.length);
1018
1106
  return out;
1019
1107
  }
1108
+ function bytesKey(bytes) {
1109
+ let s = "";
1110
+ for (let i = 0; i < bytes.length; i++) {
1111
+ s += String.fromCharCode(bytes[i]);
1112
+ }
1113
+ return s;
1114
+ }
1020
1115
  function assertEnvelopeStructure(envelope, multiPrivKeys, singlePrivKey) {
1021
1116
  if (envelope.scheme !== 1) {
1022
1117
  throw new EciesSealedPoeError(
@@ -1040,6 +1135,12 @@ function assertEnvelopeStructure(envelope, multiPrivKeys, singlePrivKey) {
1040
1135
  if (n < 1) {
1041
1136
  throw new EciesSealedPoeError("ENC_SLOTS_EMPTY", `envelope.slots.length=${n} must be >= 1`);
1042
1137
  }
1138
+ if (n > MAX_SLOTS) {
1139
+ throw new EciesSealedPoeError(
1140
+ "ENC_SLOTS_TOO_MANY",
1141
+ `envelope.slots.length=${n} exceeds MAX_SLOTS=${MAX_SLOTS}`
1142
+ );
1143
+ }
1043
1144
  if (envelope.nonce.length !== NONCE_LENGTH2) {
1044
1145
  throw new EciesSealedPoeError(
1045
1146
  "NONCE_LENGTH_MISMATCH",
@@ -1052,6 +1153,7 @@ function assertEnvelopeStructure(envelope, multiPrivKeys, singlePrivKey) {
1052
1153
  `envelope.slots_mac MUST be exactly ${SLOTS_MAC_LENGTH2} bytes, got ${envelope.slots_mac.length}`
1053
1154
  );
1054
1155
  }
1156
+ const seenKemMaterial = /* @__PURE__ */ new Set();
1055
1157
  if (envelope.kem === "x25519") {
1056
1158
  for (let i = 0; i < n; i++) {
1057
1159
  const slot = envelope.slots[i];
@@ -1067,6 +1169,14 @@ function assertEnvelopeStructure(envelope, multiPrivKeys, singlePrivKey) {
1067
1169
  `envelope.slots[${i}].wrap MUST be exactly ${WRAP_LENGTH2} bytes, got ${slot.wrap.length}`
1068
1170
  );
1069
1171
  }
1172
+ const key = bytesKey(slot.epk);
1173
+ if (seenKemMaterial.has(key)) {
1174
+ throw new EciesSealedPoeError(
1175
+ "ENC_SLOTS_DUPLICATE_KEM_MATERIAL",
1176
+ `envelope.slots[${i}].epk duplicates an earlier slot \u2014 per-slot KEK uniqueness is violated`
1177
+ );
1178
+ }
1179
+ seenKemMaterial.add(key);
1070
1180
  }
1071
1181
  } else {
1072
1182
  for (let i = 0; i < n; i++) {
@@ -1084,8 +1194,24 @@ function assertEnvelopeStructure(envelope, multiPrivKeys, singlePrivKey) {
1084
1194
  `envelope.slots[${i}].wrap MUST be exactly ${WRAP_LENGTH2} bytes, got ${slot.wrap.length}`
1085
1195
  );
1086
1196
  }
1197
+ const key = bytesKey(enc);
1198
+ if (seenKemMaterial.has(key)) {
1199
+ throw new EciesSealedPoeError(
1200
+ "ENC_SLOTS_DUPLICATE_KEM_MATERIAL",
1201
+ `envelope.slots[${i}].kem_ct duplicates an earlier slot \u2014 per-slot KEK uniqueness is violated`
1202
+ );
1203
+ }
1204
+ seenKemMaterial.add(key);
1087
1205
  }
1088
1206
  }
1207
+ const perSlotBytes = envelope.kem === "x25519" ? X25519_PUBLIC_KEY_LENGTH2 + WRAP_LENGTH2 : MLKEM768X25519_ENC_LENGTH + WRAP_LENGTH2;
1208
+ const decodedEnvelopeBytes = NONCE_LENGTH2 + SLOTS_MAC_LENGTH2 + n * perSlotBytes;
1209
+ if (decodedEnvelopeBytes > MAX_DECODED_ENVELOPE_BYTES) {
1210
+ throw new EciesSealedPoeError(
1211
+ "ENC_ENVELOPE_TOO_LARGE",
1212
+ `decoded envelope size ${decodedEnvelopeBytes} exceeds MAX_DECODED_ENVELOPE_BYTES=${MAX_DECODED_ENVELOPE_BYTES}`
1213
+ );
1214
+ }
1089
1215
  if (multiPrivKeys !== void 0) {
1090
1216
  for (let i = 0; i < multiPrivKeys.length; i++) {
1091
1217
  if (multiPrivKeys[i].length !== X25519_SECRET_KEY_LENGTH2) {
@@ -1104,60 +1230,42 @@ function assertEnvelopeStructure(envelope, multiPrivKeys, singlePrivKey) {
1104
1230
  }
1105
1231
  }
1106
1232
  }
1233
+ var ZERO_IKM_32 = new Uint8Array(32);
1107
1234
  function tryX25519Slot(args) {
1108
- if (args.liveSlot) {
1109
- try {
1110
- const shared = x25519Ecdh({
1111
- secretKey: args.recipientSecretKey,
1112
- theirPublicKey: args.slot.epk
1113
- });
1114
- const kek = hkdfSha2562({
1115
- ikm: shared,
1116
- salt: concat2(args.slot.epk, args.pubRLocal),
1117
- info: CARDANO_POE_HKDF_INFO_KEK,
1118
- length: 32
1119
- });
1120
- return chacha20Poly1305Decrypt({
1121
- key: kek,
1122
- nonce: ZERO_NONCE_122,
1123
- aad: CARDANO_POE_HKDF_INFO_KEK,
1124
- ciphertext: args.slot.wrap
1125
- });
1126
- } catch (e) {
1127
- if (!(e instanceof AeadVerificationError) && !(e instanceof X25519LowOrderPointError)) {
1128
- throw e;
1129
- }
1130
- return null;
1131
- }
1132
- }
1235
+ const salt = concat2(args.slot.epk, args.pubRLocal);
1236
+ let shared;
1133
1237
  try {
1134
- const shared = x25519Ecdh({
1238
+ shared = x25519Ecdh({
1135
1239
  secretKey: args.recipientSecretKey,
1136
1240
  theirPublicKey: args.slot.epk
1137
1241
  });
1138
- hkdfSha2562({
1139
- ikm: shared,
1140
- salt: concat2(args.slot.epk, args.pubRLocal),
1141
- info: CARDANO_POE_HKDF_INFO_KEK,
1142
- length: 32
1143
- });
1144
1242
  } catch (e) {
1145
1243
  if (!(e instanceof X25519LowOrderPointError)) throw e;
1244
+ hkdfSha2562({ ikm: ZERO_IKM_32, salt, info: CARDANO_POE_HKDF_INFO_KEK, length: 32 });
1245
+ return null;
1246
+ }
1247
+ const kek = hkdfSha2562({ ikm: shared, salt, info: CARDANO_POE_HKDF_INFO_KEK, length: 32 });
1248
+ try {
1249
+ return chacha20Poly1305Decrypt({
1250
+ key: kek,
1251
+ nonce: ZERO_NONCE_122,
1252
+ aad: CARDANO_POE_HKDF_INFO_KEK,
1253
+ ciphertext: args.slot.wrap
1254
+ });
1255
+ } catch (e) {
1256
+ if (!(e instanceof AeadVerificationError)) throw e;
1257
+ return null;
1146
1258
  }
1147
- return null;
1148
1259
  }
1149
1260
  function tryMlkem768X25519Slot(args) {
1150
1261
  const enc = joinKemCt(args.slot.kem_ct);
1151
1262
  const ss = mlkem768x25519Decapsulate({ secretSeed: args.recipientSecretKey, enc });
1152
1263
  const kek = hkdfSha2562({
1153
1264
  ikm: ss,
1154
- salt: EMPTY_SALT22,
1265
+ salt: xwingKekSalt({ kemCt: enc, pubR: args.pubR }),
1155
1266
  info: CARDANO_POE_HKDF_INFO_KEK_MLKEM768X25519,
1156
1267
  length: 32
1157
1268
  });
1158
- if (!args.liveSlot) {
1159
- return null;
1160
- }
1161
1269
  try {
1162
1270
  return chacha20Poly1305Decrypt({
1163
1271
  key: kek,
@@ -1174,51 +1282,43 @@ function tryRecipientUnwrapWithIdx(envelope, recipientSecretKey, constantTimeN,
1174
1282
  const n = envelope.slots.length;
1175
1283
  let cek = null;
1176
1284
  let matchedSlotIdx = -1;
1285
+ let cekConflict = false;
1286
+ const recordMatch = (candidate, i) => {
1287
+ if (candidate === null) return;
1288
+ if (cek === null) {
1289
+ cek = candidate;
1290
+ matchedSlotIdx = i;
1291
+ } else if (!compareCt(candidate, cek)) {
1292
+ cekConflict = true;
1293
+ }
1294
+ };
1177
1295
  if (envelope.kem === "x25519") {
1178
1296
  const pubRLocal = x25519PublicKey2({ secretKey: recipientSecretKey });
1179
1297
  for (let i = 0; i < n; i++) {
1180
1298
  if (slotsAttemptedOut !== void 0) {
1181
1299
  slotsAttemptedOut.count = i + 1;
1182
1300
  }
1183
- const candidate = tryX25519Slot({
1184
- slot: envelope.slots[i],
1185
- recipientSecretKey,
1186
- pubRLocal,
1187
- liveSlot: cek === null
1188
- });
1189
- if (cek === null && candidate !== null) {
1190
- cek = candidate;
1191
- matchedSlotIdx = i;
1192
- }
1301
+ recordMatch(tryX25519Slot({ slot: envelope.slots[i], recipientSecretKey, pubRLocal }), i);
1193
1302
  if (cek !== null && !constantTimeN) break;
1194
1303
  }
1195
1304
  } else {
1305
+ const pubR = mlkem768x25519Keygen2(recipientSecretKey).publicKey;
1196
1306
  for (let i = 0; i < n; i++) {
1197
1307
  if (slotsAttemptedOut !== void 0) {
1198
1308
  slotsAttemptedOut.count = i + 1;
1199
1309
  }
1200
- const candidate = tryMlkem768X25519Slot({
1201
- slot: envelope.slots[i],
1202
- recipientSecretKey,
1203
- liveSlot: cek === null
1204
- });
1205
- if (cek === null && candidate !== null) {
1206
- cek = candidate;
1207
- matchedSlotIdx = i;
1208
- }
1310
+ recordMatch(tryMlkem768X25519Slot({ slot: envelope.slots[i], recipientSecretKey, pubR }), i);
1209
1311
  if (cek !== null && !constantTimeN) break;
1210
1312
  }
1211
1313
  }
1212
- return cek === null ? null : { cek, slotIdx: matchedSlotIdx };
1314
+ return cek === null ? null : { cek, slotIdx: matchedSlotIdx, cekConflict };
1213
1315
  }
1214
- function tryRecipientUnwrap(envelope, recipientSecretKey, constantTimeN, slotsAttemptedOut) {
1215
- return tryRecipientUnwrapWithIdx(envelope, recipientSecretKey, constantTimeN, slotsAttemptedOut)?.cek ?? null;
1216
- }
1217
- function slotsMacCborBytes(envelope) {
1218
- return slotsToMacCbor(
1219
- envelope.slots,
1220
- envelope.kem
1221
- );
1316
+ function slotsHashBytes(envelope) {
1317
+ return computeSlotsHash({
1318
+ kem: envelope.kem,
1319
+ nonce: envelope.nonce,
1320
+ slots: envelope.slots
1321
+ });
1222
1322
  }
1223
1323
  function eciesSealedPoeUnwrap(args) {
1224
1324
  const { envelope, ciphertext } = args;
@@ -1247,34 +1347,38 @@ function eciesSealedPoeUnwrap(args) {
1247
1347
  } else {
1248
1348
  assertEnvelopeStructure(envelope, void 0, args.recipientSecretKey);
1249
1349
  }
1350
+ assertCiphertextWithinBound(ciphertext.length);
1351
+ const slotsHash = slotsHashBytes(envelope);
1250
1352
  let matchedCek = null;
1251
1353
  let anyCandidateRecovered = false;
1252
1354
  if (hasSingle) {
1253
1355
  const recipientSecretKey = args.recipientSecretKey;
1254
- const cek = tryRecipientUnwrap(
1356
+ const candidate = tryRecipientUnwrapWithIdx(
1255
1357
  envelope,
1256
1358
  recipientSecretKey,
1257
1359
  constantTimeN,
1258
1360
  args._slotsAttemptedOut
1259
1361
  );
1260
- if (cek === null) {
1362
+ if (candidate === null) {
1261
1363
  return { matched: false, reason: "WRONG_RECIPIENT_KEY" };
1262
1364
  }
1263
- const slotsCbor = slotsMacCborBytes(envelope);
1365
+ if (candidate.cekConflict) {
1366
+ return { matched: false, reason: "TAMPERED_HEADER" };
1367
+ }
1264
1368
  const hmacKey = hkdfSha2562({
1265
- ikm: cek,
1369
+ ikm: candidate.cek,
1266
1370
  salt: EMPTY_SALT22,
1267
1371
  info: CARDANO_POE_HKDF_INFO_SLOTS_MAC,
1268
1372
  length: 32
1269
1373
  });
1270
- const slotsMacCalc = hmac_js.hmac(sha2_js.sha256, hmacKey, slotsCbor);
1374
+ const slotsMacCalc = hmac_js.hmac(sha2_js.sha256, hmacKey, slotsHash);
1271
1375
  if (!compareCt(slotsMacCalc, envelope.slots_mac)) {
1272
1376
  return { matched: false, reason: "TAMPERED_HEADER" };
1273
1377
  }
1274
- matchedCek = cek;
1378
+ matchedCek = candidate.cek;
1275
1379
  } else {
1276
- const slotsCbor = slotsMacCborBytes(envelope);
1277
1380
  const recipientSecretKeys = multiPrivKeys;
1381
+ let cekConflict = false;
1278
1382
  for (let k = 0; k < recipientSecretKeys.length; k++) {
1279
1383
  if (args._privsAttemptedOut !== void 0) {
1280
1384
  args._privsAttemptedOut.count = k + 1;
@@ -1282,7 +1386,7 @@ function eciesSealedPoeUnwrap(args) {
1282
1386
  if (args._slotsAttemptedOut !== void 0) {
1283
1387
  args._slotsAttemptedOut.count = 0;
1284
1388
  }
1285
- const cek = tryRecipientUnwrap(
1389
+ const candidate = tryRecipientUnwrapWithIdx(
1286
1390
  envelope,
1287
1391
  recipientSecretKeys[k],
1288
1392
  constantTimeN,
@@ -1291,20 +1395,25 @@ function eciesSealedPoeUnwrap(args) {
1291
1395
  if (args._slotsAttemptedOut?.perPrivCounts !== void 0) {
1292
1396
  args._slotsAttemptedOut.perPrivCounts.push(args._slotsAttemptedOut.count);
1293
1397
  }
1294
- if (cek === null) continue;
1398
+ if (candidate === null) continue;
1399
+ if (candidate.cekConflict) cekConflict = true;
1400
+ const cek = candidate.cek;
1295
1401
  const hmacKey = hkdfSha2562({
1296
1402
  ikm: cek,
1297
1403
  salt: EMPTY_SALT22,
1298
1404
  info: CARDANO_POE_HKDF_INFO_SLOTS_MAC,
1299
1405
  length: 32
1300
1406
  });
1301
- const slotsMacCalc = hmac_js.hmac(sha2_js.sha256, hmacKey, slotsCbor);
1407
+ const slotsMacCalc = hmac_js.hmac(sha2_js.sha256, hmacKey, slotsHash);
1302
1408
  if (compareCt(slotsMacCalc, envelope.slots_mac)) {
1303
1409
  matchedCek = cek;
1304
1410
  break;
1305
1411
  }
1306
1412
  anyCandidateRecovered = true;
1307
1413
  }
1414
+ if (matchedCek !== null && cekConflict) {
1415
+ return { matched: false, reason: "TAMPERED_HEADER" };
1416
+ }
1308
1417
  if (matchedCek === null) {
1309
1418
  return {
1310
1419
  matched: false,
@@ -1312,10 +1421,16 @@ function eciesSealedPoeUnwrap(args) {
1312
1421
  };
1313
1422
  }
1314
1423
  }
1315
- const adContent = concat2(envelope.nonce, envelope.slots_mac);
1424
+ const payloadKey = slotsPayloadKey({ cek: matchedCek, nonce: envelope.nonce });
1425
+ const adContent = adContentSlots({
1426
+ kem: envelope.kem,
1427
+ nonce: envelope.nonce,
1428
+ slotsHash,
1429
+ slotsMac: envelope.slots_mac
1430
+ });
1316
1431
  try {
1317
1432
  const plaintext = xchacha20Poly1305Decrypt({
1318
- key: matchedCek,
1433
+ key: payloadKey,
1319
1434
  nonce: envelope.nonce,
1320
1435
  aad: adContent,
1321
1436
  ciphertext