@blamejs/pki 0.2.1 → 0.2.2
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.
- package/CHANGELOG.md +10 -1
- package/README.md +1 -0
- package/index.js +5 -0
- package/lib/framework-error.js +2 -0
- package/lib/hpke.js +487 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,7 +4,16 @@ All notable changes to `@blamejs/pki` are documented here. The format
|
|
|
4
4
|
follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this
|
|
5
5
|
project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
-
## v0.2.
|
|
7
|
+
## v0.2.2 — 2026-07-12
|
|
8
|
+
|
|
9
|
+
Hybrid Public Key Encryption (RFC 9180) joins the toolkit as pki.hpke.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- pki.hpke.setupS(suiteIds, recipientPublicKey, opts) / pki.hpke.setupR(suiteIds, enc, recipientPrivateKey, opts) establish a sender / recipient HPKE context (RFC 9180 sec. 5.1); the returned context exposes seal(aad, pt) / open(aad, ct) with the sequence-counter nonce and a message-limit guard, and export(exporterContext, L) for the secret-export interface. pki.hpke.seal / pki.hpke.open are the single-shot wrappers (sec. 6). pki.hpke.suites carries the RFC 9180 sec. 7 KEM / KDF / AEAD / MODE code points. Keys are node KeyObjects or serialized bytes; the offered suites are DHKEM P-256, P-521, X25519, and X448, HKDF-SHA256 / HKDF-SHA512, the three AEADs plus export-only, and all four modes. RFC 9180.
|
|
14
|
+
- The error taxonomy gains HpkeError (hpke/*): a malformed, low-order, or otherwise invalid encapsulated or KEM key (hpke/bad-key, so a Diffie-Hellman that fails during derivation surfaces as a typed error, never a raw fault), an unknown or unsupported ciphersuite code point (hpke/unknown-suite, never a silent default), an unsupported mode (hpke/unknown-mode, so an out-of-registry mode is rejected rather than key-scheduled), an authenticated mode invoked without the sender's key (hpke/auth-key-required), inconsistent PSK inputs (hpke/inconsistent-psk), an AEAD tag that does not verify (hpke/open-failed, returning no plaintext), a sequence-number overflow (hpke/message-limit, before any nonce reuse), a seal/open against an export-only suite (hpke/export-only), and a wrong-direction context call (hpke/wrong-role, so a recipient context cannot seal nor a sender context open -- they share a key and base nonce).
|
|
15
|
+
|
|
16
|
+
## v0.2.1 — 2026-07-11
|
|
8
17
|
|
|
9
18
|
Stateful hash-based signature verification (HSS/LMS) joins the toolkit as pki.shbs.
|
|
10
19
|
|
package/README.md
CHANGED
|
@@ -225,6 +225,7 @@ is callable today; nothing below is a stub.
|
|
|
225
225
|
| `pki.merkle` | RFC 6962 / RFC 9162 Merkle-tree proof verification — `leafHash` / `nodeHash` / `emptyRootHash` build the domain-separated (0x00 leaf / 0x01 node) SHA-256 tree hashes; `verifyInclusion` folds an audit proof back to a root and `verifyConsistency` reconstructs both the old and new root (the append-only guarantee), each constant-time-compared to a trusted checkpoint root. Fail-closed on bad geometry, sync hashing, transport-free — `leafHash`, `nodeHash`, `emptyRootHash`, `verifyInclusion`, `verifyConsistency` |
|
|
226
226
|
| `pki.trust` | Mozilla / CCADB trust-store ingestion — `parseCertdata` reads the NSS `certdata.txt` object stream and `parseCcadbCsv` the CCADB CSV export into one identical constraint-carrying anchor shape: the per-purpose trust bits (only `CKT_NSS_TRUSTED_DELEGATOR` grants) and the per-purpose distrust-after dates the bare root list omits. Certificate and trust objects pair by byte-exact issuer + serial (never adjacency) and are cross-checked against the parsed DER, so metadata can never attach to the wrong root; `anchor()` hands an entry to `pki.path.validate({ trustAnchor, checkPurpose })`. Offline, fail-closed, bounded — `parseCertdata`, `parseCcadbCsv`, `anchor` |
|
|
227
227
|
| `pki.shbs` | Stateful hash-based signature **verification** — HSS/LMS (RFC 8554), carried in X.509 by RFC 9802 and CMS by RFC 9708, profiled by NIST SP 800-208 (CNSA 2.0 firmware signing). `verify` checks an HSS signature (every level must pass) and `verifyLms` a single-tree LMS, over the raw public-key / signature blobs the parsers already surface. Pure public-input SHA-256 / SHAKE256 hashing, a data-driven typecode registry, bounds-before-slice reads; a malformed blob throws a typed `ShbsError`, a well-formed-but-wrong signature returns `false`. **Verify only by design** — stateful signing needs atomic one-time-key state that belongs in an HSM — `verify`, `verifyLms` |
|
|
228
|
+
| `pki.hpke` | Hybrid Public Key Encryption (RFC 9180) — the encrypt-to-a-public-key primitive behind TLS ECH, MLS, and OHTTP. `setupS`/`setupR` establish a sender/recipient context (KEM encapsulation + HKDF key schedule); the context's `seal`/`open` AEAD-encrypt with a sequence-counter nonce and `export` derives further secrets; `seal`/`open` are single-shot wrappers. DHKEM (P-256, P-521, X25519, X448) × HKDF-SHA256/SHA512 × AES-GCM/ChaCha20Poly1305/export-only × all four modes, proven against the RFC 9180 Appendix A vectors. DHKEM(P-384) and HKDF-SHA384 are RFC-registered but Appendix A ships no vector for them, so they fail closed until an authoritative KAT exists. Pure composition over `node:crypto`; ML-KEM / X-Wing are a registry data-row extension pending stable drafts — `suites`, `setupS`, `setupR`, `seal`, `open` |
|
|
228
229
|
| `pki.C` / `pki.constants` | Version-stable constants — functional scale helpers (`C.TIME.*`, `C.BYTES.*`), codec `LIMITS`, `version` |
|
|
229
230
|
| `pki.errors` | The `PkiError` taxonomy — `defineClass` plus `ConstantsError` / `Asn1Error` / `OidError` / `PemError` / `CertificateError` / `CrlError` / `CsrError` / `Pkcs8Error` / `CmsError` / `OcspError` / `TspError` / `AttrCertError` / `CrmfError` / `Pkcs12Error` / `CmpError` / `PathError` / `CtError` / `JoseError` / `AcmeError`, each carrying a stable `code` in `domain/reason` form |
|
|
230
231
|
| `pki` CLI | `pki version`, `pki oid <dotted\|name>`, `pki parse <cert>` |
|
package/index.js
CHANGED
|
@@ -38,6 +38,7 @@ var path = require("./lib/path-validate");
|
|
|
38
38
|
var ct = require("./lib/ct");
|
|
39
39
|
var merkle = require("./lib/merkle");
|
|
40
40
|
var shbs = require("./lib/shbs");
|
|
41
|
+
var hpke = require("./lib/hpke");
|
|
41
42
|
var est = require("./lib/est");
|
|
42
43
|
var jose = require("./lib/jose");
|
|
43
44
|
var acme = require("./lib/acme");
|
|
@@ -79,6 +80,10 @@ module.exports = {
|
|
|
79
80
|
// VERIFY ONLY by design: stateful signing requires atomic one-time-key index
|
|
80
81
|
// state that belongs in an HSM, so this module never mints a signature.
|
|
81
82
|
shbs: shbs,
|
|
83
|
+
// `hpke` is RFC 9180 Hybrid Public Key Encryption -- the KEM + HKDF key
|
|
84
|
+
// schedule + AEAD context construction behind TLS ECH / MLS / OHTTP. Pure
|
|
85
|
+
// composition over node:crypto; the classical DHKEM suites, all four modes.
|
|
86
|
+
hpke: hpke,
|
|
82
87
|
// `est` is RFC 7030 / 8951 / 9908 Enrollment over Secure Transport -- the
|
|
83
88
|
// transport-agnostic client codecs (base64 transfer, multipart splitter),
|
|
84
89
|
// certs-only + serverkeygen validators over CMS, the enroll-attribute builders,
|
package/lib/framework-error.js
CHANGED
|
@@ -282,6 +282,7 @@ var AcmeError = defineClass("AcmeError", { withCause: true });
|
|
|
282
282
|
// asn1/* error) as `.cause`.
|
|
283
283
|
var TrustError = defineClass("TrustError", { withCause: true });
|
|
284
284
|
var ShbsError = defineClass("ShbsError", { withCause: true });
|
|
285
|
+
var HpkeError = defineClass("HpkeError", { withCause: true });
|
|
285
286
|
|
|
286
287
|
module.exports = {
|
|
287
288
|
PkiError: PkiError,
|
|
@@ -306,6 +307,7 @@ module.exports = {
|
|
|
306
307
|
PathError: PathError,
|
|
307
308
|
CtError: CtError,
|
|
308
309
|
ShbsError: ShbsError,
|
|
310
|
+
HpkeError: HpkeError,
|
|
309
311
|
MerkleError: MerkleError,
|
|
310
312
|
SmimeError: SmimeError,
|
|
311
313
|
CsrattrsError: CsrattrsError,
|
package/lib/hpke.js
ADDED
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (c) blamejs contributors
|
|
3
|
+
"use strict";
|
|
4
|
+
/**
|
|
5
|
+
* @module pki.hpke
|
|
6
|
+
* @nav Protocols
|
|
7
|
+
* @title HPKE
|
|
8
|
+
* @intro Hybrid Public Key Encryption (RFC 9180) -- the standard construction
|
|
9
|
+
* behind TLS Encrypted Client Hello, MLS, and OHTTP that turns a recipient KEM
|
|
10
|
+
* public key into an encapsulated key plus an AEAD-encrypting context. This is
|
|
11
|
+
* the RFC 9180 base construction: the DHKEM suites P-256, P-521, X25519, and
|
|
12
|
+
* X448, HKDF-SHA256 / HKDF-SHA512, the three AEADs plus export-only, and all
|
|
13
|
+
* four modes (base / psk / auth / auth-psk), each proven against the RFC 9180
|
|
14
|
+
* Appendix A known-answer vectors. DHKEM(P-384) and HKDF-SHA384 are RFC-registered
|
|
15
|
+
* but Appendix A ships no test vector for them, so they are omitted (a request
|
|
16
|
+
* fails closed) until an authoritative KAT is available. Pure composition over
|
|
17
|
+
* node:crypto -- no ASN.1, no schema engine. Post-quantum KEMs (ML-KEM, X-Wing)
|
|
18
|
+
* are a data-row extension the registry is shaped to admit once their
|
|
19
|
+
* specifications stabilize.
|
|
20
|
+
* The RFC 9180 sec. 7 registry code points live in pki.hpke.suites
|
|
21
|
+
* (KEM / KDF / AEAD / MODE), passed to the setup functions to select a suite.
|
|
22
|
+
* @spec RFC 9180
|
|
23
|
+
* @card Encrypt to a KEM public key (RFC 9180; the ECH / MLS / OHTTP primitive).
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
var nodeCrypto = require("crypto");
|
|
27
|
+
var frameworkError = require("./framework-error");
|
|
28
|
+
var guard = require("./guard-all");
|
|
29
|
+
|
|
30
|
+
var HpkeError = frameworkError.HpkeError;
|
|
31
|
+
function _err(code, message, cause) { return new HpkeError(code, message, cause); }
|
|
32
|
+
|
|
33
|
+
// ---- I2OSP + byte helpers ----------------------------------------------------
|
|
34
|
+
|
|
35
|
+
function i2osp(n, len) {
|
|
36
|
+
var b = Buffer.alloc(len);
|
|
37
|
+
var v = BigInt(n);
|
|
38
|
+
for (var i = len - 1; i >= 0; i--) { b[i] = Number(v & 0xffn); v >>= 8n; }
|
|
39
|
+
return b;
|
|
40
|
+
}
|
|
41
|
+
function concat(arr) { return Buffer.concat(arr); }
|
|
42
|
+
function xor(a, b) { var o = Buffer.alloc(a.length); for (var i = 0; i < a.length; i++) o[i] = a[i] ^ b[i]; return o; }
|
|
43
|
+
|
|
44
|
+
var HPKE_V1 = Buffer.from("HPKE-v1", "ascii");
|
|
45
|
+
function L(s) { return Buffer.from(s, "ascii"); }
|
|
46
|
+
|
|
47
|
+
// ---- registries (RFC 9180 sec. 7, Tables 2/3/5 -- data, not switch) ----------
|
|
48
|
+
|
|
49
|
+
// KDF: id -> { hash (node name), Nh }.
|
|
50
|
+
// HKDF-SHA384 (0x0002) and DHKEM(P-384) (0x0011) are registered by RFC 9180 sec.
|
|
51
|
+
// 7 but Appendix A / the cited [TestVectors] file ship no known-answer vector for
|
|
52
|
+
// them, so they are omitted here until an authoritative KAT exists -- a request
|
|
53
|
+
// for either fails closed as hpke/unknown-suite rather than running crypto no
|
|
54
|
+
// test vector proves. They admit as a one-row addition the moment a KAT lands.
|
|
55
|
+
var KDFS = {
|
|
56
|
+
0x0001: { hash: "sha256", Nh: 32 },
|
|
57
|
+
0x0003: { hash: "sha512", Nh: 64 },
|
|
58
|
+
};
|
|
59
|
+
// AEAD: id -> { cipher (node name), Nk, Nn, Nt } or exportOnly.
|
|
60
|
+
var AEADS = {
|
|
61
|
+
0x0001: { cipher: "aes-128-gcm", Nk: 16, Nn: 12, Nt: 16 },
|
|
62
|
+
0x0002: { cipher: "aes-256-gcm", Nk: 32, Nn: 12, Nt: 16 },
|
|
63
|
+
0x0003: { cipher: "chacha20-poly1305", Nk: 32, Nn: 12, Nt: 16 },
|
|
64
|
+
0xFFFF: { exportOnly: true, Nn: 12 },
|
|
65
|
+
};
|
|
66
|
+
// KEM: id -> DHKEM group descriptor. `group` is the node keygen/import family;
|
|
67
|
+
// `kdf` is the KEM-internal KDF (its ExtractAndExpand); Nsecret = that KDF's Nh.
|
|
68
|
+
var KEMS = {
|
|
69
|
+
0x0010: { kind: "ec", curve: "P-256", nodeCurve: "prime256v1", kdf: 0x0001, Nsecret: 32, Npub: 65 },
|
|
70
|
+
0x0012: { kind: "ec", curve: "P-521", nodeCurve: "secp521r1", kdf: 0x0003, Nsecret: 64, Npub: 133 },
|
|
71
|
+
0x0020: { kind: "okp", curve: "X25519", oid: "2b656e", kdf: 0x0001, Nsecret: 32, Npub: 32 },
|
|
72
|
+
0x0021: { kind: "okp", curve: "X448", oid: "2b656f", kdf: 0x0003, Nsecret: 64, Npub: 56 },
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
var MODE_BASE = 0x00, MODE_PSK = 0x01, MODE_AUTH = 0x02, MODE_AUTH_PSK = 0x03;
|
|
76
|
+
|
|
77
|
+
// Format a code point for an error message without assuming it is a number: a
|
|
78
|
+
// missing suiteIds member arrives as undefined, and undefined.toString(16) would
|
|
79
|
+
// crash the very error path meant to report it.
|
|
80
|
+
function _idStr(id) { return typeof id === "number" ? "0x" + id.toString(16) : String(id); }
|
|
81
|
+
function _kem(id) { var s = KEMS[id]; if (!s) throw _err("hpke/unknown-suite", "unsupported KEM id " + _idStr(id)); return s; }
|
|
82
|
+
function _kdf(id) { var s = KDFS[id]; if (!s) throw _err("hpke/unknown-suite", "unsupported KDF id " + _idStr(id)); return s; }
|
|
83
|
+
function _aead(id) { var s = AEADS[id]; if (!s) throw _err("hpke/unknown-suite", "unsupported AEAD id " + _idStr(id)); return s; }
|
|
84
|
+
|
|
85
|
+
// ---- labeled KDF (RFC 9180 sec. 4): HKDF Extract/Expand SPLIT over HMAC -------
|
|
86
|
+
// suite_id is the caller's ("KEM"||... for the KEM layer, "HPKE"||... for the
|
|
87
|
+
// rest). Extract = HMAC(salt-or-zeros, ikm); Expand = RFC 5869 sec. 2.3 feedback.
|
|
88
|
+
|
|
89
|
+
function _extract(hash, Nh, salt, ikm) {
|
|
90
|
+
var key = (salt && salt.length) ? salt : Buffer.alloc(Nh);
|
|
91
|
+
return nodeCrypto.createHmac(hash, key).update(ikm).digest();
|
|
92
|
+
}
|
|
93
|
+
function _expand(hash, Nh, prk, info, len) {
|
|
94
|
+
if (len > 255 * Nh) throw _err("hpke/export-length", "requested length " + len + " exceeds 255*Nh");
|
|
95
|
+
var out = [], t = Buffer.alloc(0), n = Math.ceil(len / Nh);
|
|
96
|
+
for (var i = 1; i <= n; i++) {
|
|
97
|
+
t = nodeCrypto.createHmac(hash, prk).update(concat([t, info, Buffer.from([i])])).digest();
|
|
98
|
+
out.push(t);
|
|
99
|
+
}
|
|
100
|
+
return concat(out).subarray(0, len);
|
|
101
|
+
}
|
|
102
|
+
function _labeledExtract(kdf, suiteId, salt, label, ikm) {
|
|
103
|
+
return _extract(kdf.hash, kdf.Nh, salt, concat([HPKE_V1, suiteId, L(label), ikm]));
|
|
104
|
+
}
|
|
105
|
+
function _labeledExpand(kdf, suiteId, prk, label, info, len) {
|
|
106
|
+
return _expand(kdf.hash, kdf.Nh, prk, concat([i2osp(len, 2), HPKE_V1, suiteId, L(label), info]), len);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ---- KEM key (de)serialization + Diffie-Hellman (node:crypto) ----------------
|
|
110
|
+
|
|
111
|
+
var OKP_SPKI = { X25519: "302a300506032b656e032100", X448: "3042300506032b656f033900" };
|
|
112
|
+
var OKP_PKCS8 = { X25519: "302e020100300506032b656e04220420", X448: "3046020100300506032b656f043a0438" };
|
|
113
|
+
|
|
114
|
+
// Deserialize a serialized KEM public key (enc / pkRm / pkSm) into a node
|
|
115
|
+
// KeyObject. EC is an uncompressed 0x04 point (SerializePublicKey, sec. 7.1.1);
|
|
116
|
+
// OKP is the raw curve point. A wrong length / shape fails closed.
|
|
117
|
+
function _importPublic(kem, raw) {
|
|
118
|
+
if (!Buffer.isBuffer(raw) || raw.length !== kem.Npub) {
|
|
119
|
+
throw _err("hpke/bad-key", kem.curve + " public key must be " + kem.Npub + " bytes");
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
if (kem.kind === "ec") {
|
|
123
|
+
if (raw[0] !== 0x04) throw _err("hpke/bad-key", "EC public key must be an uncompressed 0x04 point (RFC 9180 sec. 7.1.4)");
|
|
124
|
+
var half = (raw.length - 1) / 2;
|
|
125
|
+
var jwk = { kty: "EC", crv: kem.curve, x: raw.subarray(1, 1 + half).toString("base64url"), y: raw.subarray(1 + half).toString("base64url") };
|
|
126
|
+
return nodeCrypto.createPublicKey({ key: jwk, format: "jwk" });
|
|
127
|
+
}
|
|
128
|
+
return nodeCrypto.createPublicKey({ key: Buffer.from(OKP_SPKI[kem.curve] + raw.toString("hex"), "hex"), format: "der", type: "spki" });
|
|
129
|
+
} catch (e) {
|
|
130
|
+
if (e instanceof HpkeError) throw e;
|
|
131
|
+
throw _err("hpke/bad-key", "invalid " + kem.curve + " public key", e);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// Serialize a node public KeyObject back to the RFC 9180 wire encoding.
|
|
135
|
+
function _exportPublic(kem, keyObject) {
|
|
136
|
+
var jwk = keyObject.export({ format: "jwk" });
|
|
137
|
+
if (kem.kind === "ec") return concat([Buffer.from([0x04]), _b64u(jwk.x), _b64u(jwk.y)]);
|
|
138
|
+
return _b64u(jwk.x);
|
|
139
|
+
}
|
|
140
|
+
// Node's own JWK export -- decode its canonical base64url coordinates through the
|
|
141
|
+
// guard so the alphabet/canonicality check is the single shared choke point.
|
|
142
|
+
function _b64u(s) { return guard.encoding.base64url(s, null, _err, "hpke/bad-key", "JWK coordinate"); }
|
|
143
|
+
|
|
144
|
+
// Import a serialized KEM private key (skRm / skEm / skSm) into a node KeyObject,
|
|
145
|
+
// given the matching serialized public key for the EC JWK (or none for OKP).
|
|
146
|
+
function _importPrivate(kem, rawSk, rawPk) {
|
|
147
|
+
try {
|
|
148
|
+
if (kem.kind === "ec") {
|
|
149
|
+
var jwk = { kty: "EC", crv: kem.curve, d: rawSk.toString("base64url") };
|
|
150
|
+
if (rawPk) { jwk.x = rawPk.subarray(1, 1 + (rawPk.length - 1) / 2).toString("base64url"); jwk.y = rawPk.subarray(1 + (rawPk.length - 1) / 2).toString("base64url"); }
|
|
151
|
+
return nodeCrypto.createPrivateKey({ key: jwk, format: "jwk" });
|
|
152
|
+
}
|
|
153
|
+
return nodeCrypto.createPrivateKey({ key: Buffer.from(OKP_PKCS8[kem.curve] + rawSk.toString("hex"), "hex"), format: "der", type: "pkcs8" });
|
|
154
|
+
} catch (e) {
|
|
155
|
+
throw _err("hpke/bad-key", "invalid " + kem.curve + " private key", e);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
// The single KEM Diffie-Hellman choke point. node:crypto throws a raw
|
|
159
|
+
// ERR_OSSL_FAILED_DURING_DERIVATION when the peer point is low-order / invalid
|
|
160
|
+
// (an all-zero X25519 shared secret, a point not on the curve). RFC 9180 sec.
|
|
161
|
+
// 4.1: Decap raises an error on DH failure -- surface it as a typed hpke/bad-key
|
|
162
|
+
// so no raw OpenSSL error escapes any encap / decap path.
|
|
163
|
+
function _dh(privateKey, publicKey) {
|
|
164
|
+
try {
|
|
165
|
+
return nodeCrypto.diffieHellman({ privateKey: privateKey, publicKey: publicKey });
|
|
166
|
+
} catch (e) {
|
|
167
|
+
throw _err("hpke/bad-key", "KEM Diffie-Hellman failed: invalid or low-order public key", e);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
function _generate(kem) {
|
|
171
|
+
if (kem.kind === "ec") return nodeCrypto.generateKeyPairSync("ec", { namedCurve: kem.nodeCurve });
|
|
172
|
+
return nodeCrypto.generateKeyPairSync(kem.curve.toLowerCase());
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ---- DHKEM (RFC 9180 sec. 4.1) -----------------------------------------------
|
|
176
|
+
|
|
177
|
+
function _kemSuiteId(kemId) { return concat([L("KEM"), i2osp(kemId, 2)]); }
|
|
178
|
+
function _extractAndExpand(kem, dh, kemContext) {
|
|
179
|
+
var kdf = _kdf(kem.kdf), sid = _kemSuiteId(_kemId(kem));
|
|
180
|
+
var eaePrk = _labeledExtract(kdf, sid, Buffer.alloc(0), "eae_prk", dh);
|
|
181
|
+
return _labeledExpand(kdf, sid, eaePrk, "shared_secret", kemContext, kem.Nsecret);
|
|
182
|
+
}
|
|
183
|
+
function _kemId(kem) { for (var k in KEMS) if (KEMS[k] === kem) return Number(k); return 0; }
|
|
184
|
+
|
|
185
|
+
// Encap(pkR) -> { sharedSecret, enc }. An injected ephemeral (skE, pkEnc) makes
|
|
186
|
+
// the KAT deterministic; otherwise a fresh ephemeral is generated.
|
|
187
|
+
function _ephemeral(kem, eph) {
|
|
188
|
+
if (eph) return { skE: _importPrivate(kem, eph.skm, eph.pkm), enc: _buf(eph.pkm) };
|
|
189
|
+
var kp = _generate(kem);
|
|
190
|
+
return { skE: kp.privateKey, enc: _exportPublic(kem, kp.publicKey) };
|
|
191
|
+
}
|
|
192
|
+
function _encap(kem, pkR, pkRm, eph) {
|
|
193
|
+
var e = _ephemeral(kem, eph);
|
|
194
|
+
var dh = _dh(e.skE, pkR);
|
|
195
|
+
return { sharedSecret: _extractAndExpand(kem, dh, concat([e.enc, pkRm])), enc: e.enc };
|
|
196
|
+
}
|
|
197
|
+
function _decap(kem, enc, skR, pkRm) {
|
|
198
|
+
var pkE = _importPublic(kem, enc);
|
|
199
|
+
var dh = _dh(skR, pkE);
|
|
200
|
+
return _extractAndExpand(kem, dh, concat([enc, pkRm]));
|
|
201
|
+
}
|
|
202
|
+
function _authEncap(kem, pkR, pkRm, skS, pkSm, eph) {
|
|
203
|
+
var e = _ephemeral(kem, eph);
|
|
204
|
+
var dh = concat([_dh(e.skE, pkR), _dh(skS, pkR)]);
|
|
205
|
+
return { sharedSecret: _extractAndExpand(kem, dh, concat([e.enc, pkRm, pkSm])), enc: e.enc };
|
|
206
|
+
}
|
|
207
|
+
function _authDecap(kem, enc, skR, pkRm, pkS, pkSm) {
|
|
208
|
+
var pkE = _importPublic(kem, enc);
|
|
209
|
+
var dh = concat([_dh(skR, pkE), _dh(skR, pkS)]);
|
|
210
|
+
return _extractAndExpand(kem, dh, concat([enc, pkRm, pkSm]));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ---- key schedule (RFC 9180 sec. 5.1) ----------------------------------------
|
|
214
|
+
|
|
215
|
+
function _hpkeSuiteId(kemId, kdfId, aeadId) { return concat([L("HPKE"), i2osp(kemId, 2), i2osp(kdfId, 2), i2osp(aeadId, 2)]); }
|
|
216
|
+
|
|
217
|
+
function _verifyPsk(mode, psk, pskId) {
|
|
218
|
+
var gotPsk = psk.length > 0, gotId = pskId.length > 0;
|
|
219
|
+
if (gotPsk !== gotId) throw _err("hpke/inconsistent-psk", "psk and psk_id must be provided together (RFC 9180 sec. 5.1)");
|
|
220
|
+
if (gotPsk && (mode === MODE_BASE || mode === MODE_AUTH)) throw _err("hpke/inconsistent-psk", "a PSK was provided for a non-PSK mode");
|
|
221
|
+
if (!gotPsk && (mode === MODE_PSK || mode === MODE_AUTH_PSK)) throw _err("hpke/inconsistent-psk", "mode requires a PSK");
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function _keySchedule(suite, mode, sharedSecret, info, psk, pskId, role) {
|
|
225
|
+
_verifyPsk(mode, psk, pskId);
|
|
226
|
+
var kdf = suite.kdf, sid = suite.suiteId, aead = suite.aead;
|
|
227
|
+
var pskIdHash = _labeledExtract(kdf, sid, Buffer.alloc(0), "psk_id_hash", pskId);
|
|
228
|
+
var infoHash = _labeledExtract(kdf, sid, Buffer.alloc(0), "info_hash", info);
|
|
229
|
+
var ksc = concat([Buffer.from([mode]), pskIdHash, infoHash]);
|
|
230
|
+
var secret = _labeledExtract(kdf, sid, sharedSecret, "secret", psk);
|
|
231
|
+
var exporterSecret = _labeledExpand(kdf, sid, secret, "exp", ksc, kdf.Nh);
|
|
232
|
+
var key = null, baseNonce = null;
|
|
233
|
+
if (!aead.exportOnly) {
|
|
234
|
+
key = _labeledExpand(kdf, sid, secret, "key", ksc, aead.Nk);
|
|
235
|
+
baseNonce = _labeledExpand(kdf, sid, secret, "base_nonce", ksc, aead.Nn);
|
|
236
|
+
}
|
|
237
|
+
return new Context(suite, key, baseNonce, exporterSecret, role);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ---- AEAD context (RFC 9180 sec. 5.2 / 5.3) ----------------------------------
|
|
241
|
+
|
|
242
|
+
// role is "S" (sender: seal only) or "R" (recipient: open only). Sender and
|
|
243
|
+
// recipient derive the same key + base_nonce, so a wrong-direction call would
|
|
244
|
+
// reuse a nonce (RFC 9180 sec. 5.2); export is available to both.
|
|
245
|
+
function Context(suite, key, baseNonce, exporterSecret, role) {
|
|
246
|
+
this._suite = suite; this._key = key; this._baseNonce = baseNonce;
|
|
247
|
+
this._exporterSecret = exporterSecret; this._seq = 0n; this._role = role;
|
|
248
|
+
}
|
|
249
|
+
Context.prototype._nonce = function () {
|
|
250
|
+
var aead = this._suite.aead;
|
|
251
|
+
var seqBytes = i2osp(this._seq, aead.Nn);
|
|
252
|
+
return xor(this._baseNonce, seqBytes);
|
|
253
|
+
};
|
|
254
|
+
Context.prototype._inc = function () {
|
|
255
|
+
var aead = this._suite.aead;
|
|
256
|
+
if (this._seq >= (1n << BigInt(8 * aead.Nn)) - 1n) throw _err("hpke/message-limit", "AEAD sequence number would overflow (RFC 9180 sec. 5.2)");
|
|
257
|
+
this._seq += 1n;
|
|
258
|
+
};
|
|
259
|
+
Context.prototype.seal = function (aad, pt) {
|
|
260
|
+
var aead = this._suite.aead;
|
|
261
|
+
if (this._role !== "S") throw _err("hpke/wrong-role", "seal is only available on a sender context (RFC 9180 sec. 5.2)");
|
|
262
|
+
if (aead.exportOnly) throw _err("hpke/export-only", "seal is not available for an export-only AEAD");
|
|
263
|
+
var nonce = this._nonce();
|
|
264
|
+
var c = nodeCrypto.createCipheriv(aead.cipher, this._key, nonce, { authTagLength: aead.Nt });
|
|
265
|
+
if (aad && aad.length) c.setAAD(aad);
|
|
266
|
+
var body = concat([c.update(_buf(pt)), c.final()]);
|
|
267
|
+
var ct = concat([body, c.getAuthTag()]);
|
|
268
|
+
this._inc();
|
|
269
|
+
return ct;
|
|
270
|
+
};
|
|
271
|
+
Context.prototype.open = function (aad, ct) {
|
|
272
|
+
var aead = this._suite.aead;
|
|
273
|
+
if (this._role !== "R") throw _err("hpke/wrong-role", "open is only available on a recipient context (RFC 9180 sec. 5.2)");
|
|
274
|
+
if (aead.exportOnly) throw _err("hpke/export-only", "open is not available for an export-only AEAD");
|
|
275
|
+
ct = _buf(ct);
|
|
276
|
+
if (ct.length < aead.Nt) throw _err("hpke/open-failed", "ciphertext is shorter than the AEAD tag");
|
|
277
|
+
var nonce = this._nonce();
|
|
278
|
+
var d = nodeCrypto.createDecipheriv(aead.cipher, this._key, nonce, { authTagLength: aead.Nt });
|
|
279
|
+
if (aad && aad.length) d.setAAD(aad);
|
|
280
|
+
d.setAuthTag(ct.subarray(ct.length - aead.Nt));
|
|
281
|
+
var pt;
|
|
282
|
+
try { pt = concat([d.update(ct.subarray(0, ct.length - aead.Nt)), d.final()]); }
|
|
283
|
+
catch (e) { throw _err("hpke/open-failed", "AEAD authentication failed (RFC 9180 sec. 5.2)", e); }
|
|
284
|
+
this._inc();
|
|
285
|
+
return pt;
|
|
286
|
+
};
|
|
287
|
+
Context.prototype.export = function (exporterContext, len) {
|
|
288
|
+
if (!Number.isInteger(len) || len < 0) throw _err("hpke/export-length", "export length must be a non-negative integer");
|
|
289
|
+
return _labeledExpand(this._suite.kdf, this._suite.suiteId, this._exporterSecret, "sec", _buf(exporterContext), len);
|
|
290
|
+
};
|
|
291
|
+
function _buf(x) { return Buffer.isBuffer(x) ? x : Buffer.from(x || []); }
|
|
292
|
+
|
|
293
|
+
// ---- suite resolution + setup ------------------------------------------------
|
|
294
|
+
|
|
295
|
+
function _suite(ids) {
|
|
296
|
+
if (!ids || typeof ids !== "object") throw _err("hpke/unknown-suite", "suiteIds must be an object { kem, kdf, aead } from pki.hpke.suites");
|
|
297
|
+
var kemId = ids.kem, kdfId = ids.kdf, aeadId = ids.aead;
|
|
298
|
+
var kem = _kem(kemId), kdf = _kdf(kdfId), aead = _aead(aeadId);
|
|
299
|
+
return { kem: kem, kdf: kdf, aead: aead, kemId: kemId, kdfId: kdfId, aeadId: aeadId, suiteId: _hpkeSuiteId(kemId, kdfId, aeadId) };
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// A KEM key argument is a node KeyObject or serialized bytes: a public key as a
|
|
303
|
+
// raw buffer (or { pkm }), a private key as { skm, pkm } (raw scalar + public
|
|
304
|
+
// point). A KAT drives the deterministic path via those serialized forms.
|
|
305
|
+
function _recipPublic(suite, pk) {
|
|
306
|
+
if (Buffer.isBuffer(pk)) return { key: _importPublic(suite.kem, pk), pkm: pk };
|
|
307
|
+
if (pk && pk.pkm) return { key: _importPublic(suite.kem, pk.pkm), pkm: pk.pkm };
|
|
308
|
+
// A node KeyObject: derive its wire form. A null/undefined/invalid value must
|
|
309
|
+
// fail closed here rather than let node's export throw a raw error upward.
|
|
310
|
+
try {
|
|
311
|
+
return { key: pk, pkm: _exportPublic(suite.kem, pk) };
|
|
312
|
+
} catch (e) {
|
|
313
|
+
if (e instanceof HpkeError) throw e;
|
|
314
|
+
throw _err("hpke/bad-key", "invalid KEM public key (expected a node KeyObject, a raw buffer, or { pkm })", e);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
function _recipPrivate(suite, sk) {
|
|
318
|
+
if (sk && sk.skm) return { key: _importPrivate(suite.kem, sk.skm, sk.pkm), pkm: sk.pkm };
|
|
319
|
+
// A raw scalar alone cannot be imported (an EC private key needs its public
|
|
320
|
+
// point for the JWK), so the serialized private form is { skm, pkm }; reject a
|
|
321
|
+
// bare buffer, and fail any other bad key closed rather than let node's
|
|
322
|
+
// createPublicKey throw a raw error out of the setup path.
|
|
323
|
+
if (Buffer.isBuffer(sk)) throw _err("hpke/bad-key", "a serialized private key must be provided as { skm, pkm } (raw scalar + public point), not a bare buffer");
|
|
324
|
+
try {
|
|
325
|
+
var pkm = _exportPublic(suite.kem, nodeCrypto.createPublicKey(sk));
|
|
326
|
+
return { key: sk, pkm: pkm };
|
|
327
|
+
} catch (e) {
|
|
328
|
+
if (e instanceof HpkeError) throw e;
|
|
329
|
+
throw _err("hpke/bad-key", "invalid recipient private key (expected a node KeyObject or { skm, pkm })", e);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Resolve and validate the HPKE mode: RFC 9180 sec. 5.1 defines exactly base /
|
|
334
|
+
// psk / auth / auth-psk. An unknown mode must fail closed, never key-schedule
|
|
335
|
+
// with an out-of-registry mode byte.
|
|
336
|
+
function _mode(opts) {
|
|
337
|
+
var mode = (opts.mode == null) ? MODE_BASE : opts.mode;
|
|
338
|
+
if (mode !== MODE_BASE && mode !== MODE_PSK && mode !== MODE_AUTH && mode !== MODE_AUTH_PSK) {
|
|
339
|
+
throw _err("hpke/unknown-mode", "unsupported HPKE mode " + JSON.stringify(mode) + " (RFC 9180 sec. 5.1 defines base / psk / auth / auth-psk)");
|
|
340
|
+
}
|
|
341
|
+
return mode;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// pki.hpke.suites -- the RFC 9180 sec. 7 registry code points (KEM / KDF / AEAD /
|
|
345
|
+
// MODE), passed to the setup functions to select a ciphersuite (documented in the
|
|
346
|
+
// @module @intro; a data registry, not a callable primitive).
|
|
347
|
+
var suites = {
|
|
348
|
+
KEM: { DHKEM_P256_HKDF_SHA256: 0x0010, DHKEM_P521_HKDF_SHA512: 0x0012, DHKEM_X25519_HKDF_SHA256: 0x0020, DHKEM_X448_HKDF_SHA512: 0x0021 },
|
|
349
|
+
KDF: { HKDF_SHA256: 0x0001, HKDF_SHA512: 0x0003 },
|
|
350
|
+
AEAD: { AES_128_GCM: 0x0001, AES_256_GCM: 0x0002, CHACHA20_POLY1305: 0x0003, EXPORT_ONLY: 0xFFFF },
|
|
351
|
+
MODE: { BASE: MODE_BASE, PSK: MODE_PSK, AUTH: MODE_AUTH, AUTH_PSK: MODE_AUTH_PSK },
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
// setup*S(suiteIds, pkR, opts) -> { enc, context }. opts: { info, psk, pskId,
|
|
355
|
+
// senderKey (auth), eph (KAT determinism) }. setup*R(suiteIds, enc, skR, opts).
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* @primitive pki.hpke.setupS
|
|
359
|
+
* @signature pki.hpke.setupS(suiteIds, recipientPublicKey, opts?) -> { enc, context }
|
|
360
|
+
* @since 0.2.2
|
|
361
|
+
* @status experimental
|
|
362
|
+
* @spec RFC 9180
|
|
363
|
+
* @related pki.hpke.setupR, pki.hpke.seal
|
|
364
|
+
*
|
|
365
|
+
* Establish a sender HPKE context for a recipient KEM public key: encapsulate a
|
|
366
|
+
* shared secret and run the key schedule, returning the encapsulated key `enc`
|
|
367
|
+
* (send it to the recipient) and a `context` whose `.seal(aad, pt)` /
|
|
368
|
+
* `.export(ctx, L)` encrypt and derive further secrets. `suiteIds` is
|
|
369
|
+
* `{ kem, kdf, aead }` from `pki.hpke.suites`; `recipientPublicKey` is a node
|
|
370
|
+
* KeyObject or the serialized public key bytes. `opts.mode` selects base / psk /
|
|
371
|
+
* auth / auth-psk (default base); `opts.info`, `opts.psk`/`opts.pskId`, and
|
|
372
|
+
* `opts.senderKey` (auth modes) supply the corresponding inputs.
|
|
373
|
+
*
|
|
374
|
+
* @example
|
|
375
|
+
* var s = pki.hpke.suites, ids = { kem: s.KEM.DHKEM_X25519_HKDF_SHA256, kdf: s.KDF.HKDF_SHA256, aead: s.AEAD.AES_128_GCM };
|
|
376
|
+
* var pkR = Buffer.from("8c7781768956b9dd38997c5a83ab5b9315270a9f73d87d676573c5bca74e3e48", "hex");
|
|
377
|
+
* var sender = pki.hpke.setupS(ids, pkR, { info: Buffer.from("app") });
|
|
378
|
+
* var ct = sender.context.seal(Buffer.from("aad"), Buffer.from("secret"));
|
|
379
|
+
*/
|
|
380
|
+
function _setupS(ids, pkR, opts) {
|
|
381
|
+
opts = opts || {};
|
|
382
|
+
var suite = _suite(ids);
|
|
383
|
+
var mode = _mode(opts);
|
|
384
|
+
var r = _recipPublic(suite, pkR);
|
|
385
|
+
var kem = suite.kem, k;
|
|
386
|
+
if (mode === MODE_AUTH || mode === MODE_AUTH_PSK) {
|
|
387
|
+
if (kem.kind !== "ec" && kem.kind !== "okp") throw _err("hpke/auth-unsupported", "the KEM does not support authenticated modes");
|
|
388
|
+
if (opts.senderKey == null) throw _err("hpke/auth-key-required", "an authenticated mode requires opts.senderKey (the sender's KEM private key, RFC 9180 sec. 5.1.3)");
|
|
389
|
+
var s = _recipPrivate(suite, opts.senderKey);
|
|
390
|
+
k = _authEncap(kem, r.key, r.pkm, s.key, s.pkm, opts.eph);
|
|
391
|
+
} else {
|
|
392
|
+
k = _encap(kem, r.key, r.pkm, opts.eph);
|
|
393
|
+
}
|
|
394
|
+
var ctx = _keySchedule(suite, mode, k.sharedSecret, _buf(opts.info), _buf(opts.psk), _buf(opts.pskId), "S");
|
|
395
|
+
return { enc: k.enc, context: ctx, sharedSecret: k.sharedSecret };
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* @primitive pki.hpke.setupR
|
|
399
|
+
* @signature pki.hpke.setupR(suiteIds, enc, recipientPrivateKey, opts?) -> context
|
|
400
|
+
* @since 0.2.2
|
|
401
|
+
* @status experimental
|
|
402
|
+
* @spec RFC 9180
|
|
403
|
+
* @related pki.hpke.setupS, pki.hpke.open
|
|
404
|
+
*
|
|
405
|
+
* Establish the recipient HPKE context from the sender's encapsulated key `enc`
|
|
406
|
+
* and the recipient KEM private key (a node KeyObject or `{ skm, pkm }` -- the
|
|
407
|
+
* raw private scalar plus its public point),
|
|
408
|
+
* recovering the same shared secret and key schedule. The returned `context`
|
|
409
|
+
* `.open(aad, ct)` decrypts and `.export(ctx, L)` derives secrets. `opts` mirrors
|
|
410
|
+
* `setupS` (mode / info / psk / pskId), with `opts.senderPublicKey` for the auth
|
|
411
|
+
* modes. A ciphertext whose tag does not verify throws `hpke/open-failed`.
|
|
412
|
+
*
|
|
413
|
+
* @example
|
|
414
|
+
* var s = pki.hpke.suites, ids = { kem: s.KEM.DHKEM_X25519_HKDF_SHA256, kdf: s.KDF.HKDF_SHA256, aead: s.AEAD.AES_128_GCM };
|
|
415
|
+
* var pkR = Buffer.from("8c7781768956b9dd38997c5a83ab5b9315270a9f73d87d676573c5bca74e3e48", "hex");
|
|
416
|
+
* var skR = Buffer.from("009f2181fba5f8908632c10ea1137c40a849728fde016c4602458b943a5dc048", "hex");
|
|
417
|
+
* var sender = pki.hpke.setupS(ids, pkR);
|
|
418
|
+
* var recipient = pki.hpke.setupR(ids, sender.enc, { skm: skR, pkm: pkR });
|
|
419
|
+
* var pt = recipient.open(Buffer.alloc(0), sender.context.seal(Buffer.alloc(0), Buffer.from("hi")));
|
|
420
|
+
*/
|
|
421
|
+
function _setupR(ids, enc, skR, opts) {
|
|
422
|
+
opts = opts || {};
|
|
423
|
+
var suite = _suite(ids);
|
|
424
|
+
var mode = _mode(opts);
|
|
425
|
+
var r = _recipPrivate(suite, skR);
|
|
426
|
+
var kem = suite.kem, ss;
|
|
427
|
+
if (mode === MODE_AUTH || mode === MODE_AUTH_PSK) {
|
|
428
|
+
if (opts.senderPublicKey == null) throw _err("hpke/auth-key-required", "an authenticated mode requires opts.senderPublicKey (the sender's KEM public key, RFC 9180 sec. 5.1.3)");
|
|
429
|
+
var pkS = _recipPublic(suite, opts.senderPublicKey);
|
|
430
|
+
ss = _authDecap(kem, _buf(enc), r.key, r.pkm, pkS.key, pkS.pkm);
|
|
431
|
+
} else {
|
|
432
|
+
ss = _decap(kem, _buf(enc), r.key, r.pkm);
|
|
433
|
+
}
|
|
434
|
+
return _keySchedule(suite, mode, ss, _buf(opts.info), _buf(opts.psk), _buf(opts.pskId), "R");
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* @primitive pki.hpke.seal
|
|
439
|
+
* @signature pki.hpke.seal(suiteIds, recipientPublicKey, opts, aad, pt) -> { enc, ct }
|
|
440
|
+
* @since 0.2.2
|
|
441
|
+
* @status experimental
|
|
442
|
+
* @spec RFC 9180
|
|
443
|
+
* @related pki.hpke.open, pki.hpke.setupS
|
|
444
|
+
*
|
|
445
|
+
* Single-shot HPKE encryption (RFC 9180 sec. 6): set up a sender context and
|
|
446
|
+
* encrypt one plaintext, returning the encapsulated key `enc` and ciphertext
|
|
447
|
+
* `ct`. Equivalent to `setupS` followed by one `context.seal`. `opts` is the
|
|
448
|
+
* `setupS` options object (mode / info / psk / senderKey).
|
|
449
|
+
*
|
|
450
|
+
* @example
|
|
451
|
+
* var s = pki.hpke.suites, ids = { kem: s.KEM.DHKEM_X25519_HKDF_SHA256, kdf: s.KDF.HKDF_SHA256, aead: s.AEAD.AES_256_GCM };
|
|
452
|
+
* var pkR = Buffer.from("8c7781768956b9dd38997c5a83ab5b9315270a9f73d87d676573c5bca74e3e48", "hex");
|
|
453
|
+
* var out = pki.hpke.seal(ids, pkR, {}, Buffer.from("aad"), Buffer.from("msg"));
|
|
454
|
+
*/
|
|
455
|
+
function seal(ids, pkR, opts, aad, pt) {
|
|
456
|
+
var s = _setupS(ids, pkR, opts);
|
|
457
|
+
return { enc: s.enc, ct: s.context.seal(_buf(aad), _buf(pt)) };
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* @primitive pki.hpke.open
|
|
462
|
+
* @signature pki.hpke.open(suiteIds, enc, recipientPrivateKey, opts, aad, ct) -> pt
|
|
463
|
+
* @since 0.2.2
|
|
464
|
+
* @status experimental
|
|
465
|
+
* @spec RFC 9180
|
|
466
|
+
* @related pki.hpke.seal, pki.hpke.setupR
|
|
467
|
+
*
|
|
468
|
+
* Single-shot HPKE decryption (RFC 9180 sec. 6): set up a recipient context from
|
|
469
|
+
* `enc` and decrypt one ciphertext, returning the plaintext. A tag that does not
|
|
470
|
+
* verify throws `hpke/open-failed` and returns no plaintext.
|
|
471
|
+
*
|
|
472
|
+
* @example
|
|
473
|
+
* var s = pki.hpke.suites, ids = { kem: s.KEM.DHKEM_X25519_HKDF_SHA256, kdf: s.KDF.HKDF_SHA256, aead: s.AEAD.AES_256_GCM };
|
|
474
|
+
* var pkR = Buffer.from("8c7781768956b9dd38997c5a83ab5b9315270a9f73d87d676573c5bca74e3e48", "hex");
|
|
475
|
+
* var skR = Buffer.from("009f2181fba5f8908632c10ea1137c40a849728fde016c4602458b943a5dc048", "hex");
|
|
476
|
+
* var o = pki.hpke.seal(ids, pkR, {}, Buffer.alloc(0), Buffer.from("m"));
|
|
477
|
+
* var pt = pki.hpke.open(ids, o.enc, { skm: skR, pkm: pkR }, {}, Buffer.alloc(0), o.ct);
|
|
478
|
+
*/
|
|
479
|
+
function open(ids, enc, skR, opts, aad, ct) {
|
|
480
|
+
return _setupR(ids, enc, skR, opts).open(_buf(aad), _buf(ct));
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
module.exports = {
|
|
484
|
+
suites: suites,
|
|
485
|
+
setupS: _setupS, setupR: _setupR,
|
|
486
|
+
seal: seal, open: open,
|
|
487
|
+
};
|
package/package.json
CHANGED
package/sbom.cdx.json
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
|
|
3
3
|
"bomFormat": "CycloneDX",
|
|
4
4
|
"specVersion": "1.5",
|
|
5
|
-
"serialNumber": "urn:uuid:
|
|
5
|
+
"serialNumber": "urn:uuid:78e0173e-dadb-4062-861f-ed7ded314dab",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-07-
|
|
8
|
+
"timestamp": "2026-07-12T03:33:57.948Z",
|
|
9
9
|
"lifecycles": [
|
|
10
10
|
{
|
|
11
11
|
"phase": "build"
|
|
@@ -19,14 +19,14 @@
|
|
|
19
19
|
}
|
|
20
20
|
],
|
|
21
21
|
"component": {
|
|
22
|
-
"bom-ref": "@blamejs/pki@0.2.
|
|
22
|
+
"bom-ref": "@blamejs/pki@0.2.2",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "pki",
|
|
25
|
-
"version": "0.2.
|
|
25
|
+
"version": "0.2.2",
|
|
26
26
|
"scope": "required",
|
|
27
27
|
"author": "blamejs contributors",
|
|
28
28
|
"description": "Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.",
|
|
29
|
-
"purl": "pkg:npm/%40blamejs/pki@0.2.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/pki@0.2.2",
|
|
30
30
|
"properties": [],
|
|
31
31
|
"externalReferences": [
|
|
32
32
|
{
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"components": [],
|
|
55
55
|
"dependencies": [
|
|
56
56
|
{
|
|
57
|
-
"ref": "@blamejs/pki@0.2.
|
|
57
|
+
"ref": "@blamejs/pki@0.2.2",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|