js-hpke-rfc9180-base-mode-aead-key-schedule-exactness
I implemented and verified it in ~/hpke/ (full write-up in ~/hpke/SOLUTION.md). All RFC 9180 A.1 + A.3 base-mode vectors reproduce byte-for-byte.
Two byte-exactness bugs break the vectors while seal/open still "work":
1. HPKE suite_id must include the KEM id (10 bytes, not 8).
RFC 9180 §5.1:
suite_id = "HPKE" || I2OSP(kem_id,2) || I2OSP(kdf_id,2) || I2OSP(aead_id,2)
kem_suite_id = "KEM" || I2OSP(kem_id,2)
Omitting kem_id makes shared_secret match (it uses kem_suite_id) but corrupts key_schedule_context, secret, key, base_nonce, exporter_secret. A.1 key_schedule_context came out 0015c264… instead of 00725611….
2. Export uses label "sec", not "exp".
The key schedule derives the exporter secret with "exp":
exporter_secret = LabeledExpand(secret, "exp", key_schedule_context, Nh)
but the §5.3 Export API uses "sec":
Context.Export(ctx, L) = LabeledExpand(exporter_secret, "sec", ctx, L)
With "exp", the first A.1 export is 73006be5…; with "sec" it is the expected 3853fe2b….
(Also: A.3 is P-256, not X25519, so the code additionally implements DHKEM(P-256) with the counter-based candidate DeriveKeyPair.)
hpke.js)this.suiteId = concat( // 10 bytes
Buffer.from('HPKE'), i2osp(kemId,2), i2osp(kdfId,2), i2osp(aeadId,2));
this.suiteId = concat(Buffer.from('KEM'), i2osp(kemId,2)); // DHKEM
// key schedule
const secret = labeledExtract(suiteId, sharedSecret, 'secret', psk);
const key = labeledExpand (suiteId, secret, 'key', ksc, Nk);
const baseNonce = labeledExpand (suiteId, secret, 'base_nonce', ksc, Nn);
const exporterSecret = labeledExpand (suiteId, secret, 'exp', ksc, Nh);
// Export API -> "sec"
export(exporterSecret, ctx, L) {
return labeledExpand(this.suiteId, exporterSecret, 'sec', ctx, L);
}
// nonce = base_nonce XOR I2OSP(seq, Nn), reject seq >= 2^(8*Nn)
if (s >= 1n << BigInt(8 * Nn)) throw new SequenceOverflowError(...);
open wraps decipher.final() in try/catch, throws AEADAuthenticationError, and never returns/logs partial plaintext.
$ node verify.js
ok RFC 9180 Appendix A.1
ok RFC 9180 Appendix A.3
ok sequence overflow + AEAD auth errors + 100 round-trips
ALL VERIFICATION TESTS PASSED
$ node test-cfrg.js
ok CFRG 2 base-mode suite vectors (X25519 + P-256) reproduced byte-for-byte
Verified: A.1/A.3 shared_secret, key_schedule_context, secret, key, base_nonce, exporter_secret; all nonces/ciphertexts at seq 0,1,2,4,255,256; all exported values for contexts "", 00, TestContext; nonce overflow at 2^96 → SequenceOverflowError; tampered tag/AAD/short ct → AEADAuthenticationError with undefined plaintext; 100+ randomized round-trips.
Files: hpke.js (implementation), verify.js (self-contained vector+error test), test-cfrg.js + tv.json, and the complete write-up SOLUTION.md.
# Evidence - Problem class: js-hpke-rfc9180-base-mode-aead-key-schedule-exactness - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-10-01T22:15:18.523Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "Implement RFC 9180 HPKE base mode in JavaScript on Node 20 for KEM X25519-HKDF-SHA256, KDF HKDF-SHA256 and AEAD AES-128-GCM, including labeled_extract and labeled_expand with suite_id 'HPKE' and the exact labels (eae_prk, shared_secret, key, base_nonce, secret, exp), DHKEM Encap/Decap with kem_suite_id and the 0x00 || 0x01 || Nsecret context string, and seal/open with the RFC nonce = base_nonce XOR I2OSP(seq, Nn). The implementation must reproduce the RFC 9180 Appendix A.1 and A.3 test vectors byte-for-byte and must export export(exporter_context, L) under the 'exp' label. It must reject a sequence number that overflows the nonce width and any AEAD authentication failure with a typed error, without ever returning or logging plaintext on failure.", "environment": "node20", "language": "js", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "js-hpke-rfc9180-base-mode-aead-key-schedule-exactness", "provider": "openrouter", "solved_at": "2026-10-01T22:15:18.525Z", "version": "20"}