UNPKG

edhoc

Version:

A Node.js implementation of EDHOC (Ephemeral Diffie-Hellman Over COSE) protocol for lightweight authenticated key exchange in IoT and other constrained environments.

569 lines (568 loc) 30.4 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.EDHOC = exports.EdhocSuite = exports.EdhocMethod = exports.EdhocCredentialsCertificateHashAlgorithm = exports.EdhocCredentialsFormat = exports.EdhocCipherSuiteError = exports.EdhocError = void 0; const cbor_1 = __importDefault(require("cbor")); const cipher_suites_1 = require("./cipher-suites"); const cbor_utils_1 = require("./cbor-utils"); // ── Error Types ───────────────────────────────────────────────────────── class EdhocError extends Error { constructor(message) { super(message); this.name = 'EdhocError'; } } exports.EdhocError = EdhocError; class EdhocCipherSuiteError extends EdhocError { peerCipherSuites; constructor(message, peerCipherSuites) { super(message); this.name = 'EdhocCipherSuiteError'; this.peerCipherSuites = peerCipherSuites; } } exports.EdhocCipherSuiteError = EdhocCipherSuiteError; // ── Public Types & Interfaces ────────────────────────────────────────── var EdhocCredentialsFormat; (function (EdhocCredentialsFormat) { EdhocCredentialsFormat[EdhocCredentialsFormat["kid"] = 4] = "kid"; EdhocCredentialsFormat[EdhocCredentialsFormat["x5chain"] = 33] = "x5chain"; EdhocCredentialsFormat[EdhocCredentialsFormat["x5t"] = 34] = "x5t"; })(EdhocCredentialsFormat || (exports.EdhocCredentialsFormat = EdhocCredentialsFormat = {})); var EdhocCredentialsCertificateHashAlgorithm; (function (EdhocCredentialsCertificateHashAlgorithm) { EdhocCredentialsCertificateHashAlgorithm[EdhocCredentialsCertificateHashAlgorithm["Sha256"] = -16] = "Sha256"; EdhocCredentialsCertificateHashAlgorithm[EdhocCredentialsCertificateHashAlgorithm["Sha256_64"] = -15] = "Sha256_64"; })(EdhocCredentialsCertificateHashAlgorithm || (exports.EdhocCredentialsCertificateHashAlgorithm = EdhocCredentialsCertificateHashAlgorithm = {})); var EdhocMethod; (function (EdhocMethod) { EdhocMethod[EdhocMethod["Method0"] = 0] = "Method0"; EdhocMethod[EdhocMethod["Method1"] = 1] = "Method1"; EdhocMethod[EdhocMethod["Method2"] = 2] = "Method2"; EdhocMethod[EdhocMethod["Method3"] = 3] = "Method3"; })(EdhocMethod || (exports.EdhocMethod = EdhocMethod = {})); var EdhocSuite; (function (EdhocSuite) { EdhocSuite[EdhocSuite["Suite0"] = 0] = "Suite0"; EdhocSuite[EdhocSuite["Suite1"] = 1] = "Suite1"; EdhocSuite[EdhocSuite["Suite2"] = 2] = "Suite2"; EdhocSuite[EdhocSuite["Suite3"] = 3] = "Suite3"; EdhocSuite[EdhocSuite["Suite4"] = 4] = "Suite4"; EdhocSuite[EdhocSuite["Suite5"] = 5] = "Suite5"; EdhocSuite[EdhocSuite["Suite6"] = 6] = "Suite6"; EdhocSuite[EdhocSuite["Suite24"] = 24] = "Suite24"; EdhocSuite[EdhocSuite["Suite25"] = 25] = "Suite25"; })(EdhocSuite || (exports.EdhocSuite = EdhocSuite = {})); // ── EDHOC Class ──────────────────────────────────────────────────────── class EDHOC { // ── public properties (matching original API) ────────────────────── connectionID; get peerConnectionID() { return this._peerCid; } methods; get selectedMethod() { return this._method; } set selectedMethod(v) { this._method = v; } cipherSuites; get selectedSuite() { return this._suite; } set selectedSuite(v) { this._suite = v; } logger; // ── private dependencies ─────────────────────────────────────────── credMgr; crypto; // ── private protocol state ───────────────────────────────────────── _state = 0 /* State.START */; _role = null; _method; _suite; _suiteParams; _peerCid = null; _ephPrivateKey = null; _ephPub = null; _peerEphPub = null; _th = null; _prk2e = null; _prk3e2m = null; _prk4e3m = null; _prkOut = null; _prkExporter = null; _peerCredentials = null; // ── constructor / reset ──────────────────────────────────────────── constructor(connectionID, methods, suites, credentials, crypto) { this.connectionID = connectionID; this.methods = methods; this.cipherSuites = suites; this.credMgr = credentials; this.crypto = crypto; this._method = methods[0]; this._suite = suites[suites.length - 1]; this._suiteParams = (0, cipher_suites_1.getCipherSuiteParams)(this._suite); } reset() { this._state = 0 /* State.START */; this._role = null; this._method = this.methods[0]; this._suite = this.cipherSuites[this.cipherSuites.length - 1]; this._suiteParams = (0, cipher_suites_1.getCipherSuiteParams)(this._suite); this._peerCid = null; this._ephPrivateKey = null; this._ephPub = null; this._peerEphPub = null; this._th = null; this._prk2e = null; this._prk3e2m = null; this._prk4e3m = null; this._prkOut = null; this._prkExporter = null; this._peerCredentials = null; } // ── public message API ───────────────────────────────────────────── async composeMessage1(ead) { this.assertState(0 /* State.START */, 'composeMessage1'); this._role = 'initiator'; this._method = this.methods[0]; this._suite = this.cipherSuites[this.cipherSuites.length - 1]; this._suiteParams = (0, cipher_suites_1.getCipherSuiteParams)(this._suite); // Generate ephemeral DH keypair await this.generateEphemeralKey(); // Build message_1 CBOR sequence: METHOD, SUITES_I, G_X, C_I, ?EAD_1 const parts = [ this._method, (0, cbor_utils_1.encodeSuites)(this.cipherSuites, this._suite), this._ephPub, this.connectionID, ]; if (ead?.length) for (const t of ead) { parts.push(t.label); if (t.value?.length) parts.push(t.value); } const msg1 = (0, cbor_utils_1.encodeCborSequence)(...parts); this.log('message_1', msg1); // TH_1 = H(message_1) — hash of the raw CBOR-sequence bytes this._th = await this.hash(msg1); this.log('TH_1', this._th); this._state = 1 /* State.WAIT_M2 */; return msg1; } async processMessage1(message) { this.assertState(0 /* State.START */, 'processMessage1'); this._role = 'responder'; const items = (0, cbor_utils_1.decodeCborSequence)(message); if (items.length < 4) throw new EdhocError('Invalid message_1'); // Parse METHOD const method = items[0]; if (!this.methods.includes(method)) throw new EdhocError(`Unsupported method: ${method}`); this._method = method; // Parse SUITES_I const rawSuites = items[1]; const selected = typeof rawSuites === 'number' ? rawSuites : rawSuites[rawSuites.length - 1]; if (!this.cipherSuites.includes(selected)) { throw new EdhocCipherSuiteError(`Unsupported cipher suite: ${selected}`, typeof rawSuites === 'number' ? [rawSuites] : rawSuites); } this._suite = selected; this._suiteParams = (0, cipher_suites_1.getCipherSuiteParams)(selected); // Parse G_X, C_I const gx = items[2]; this._peerEphPub = Buffer.isBuffer(gx) ? gx : Buffer.from(gx); this._peerCid = (0, cbor_utils_1.connectionIdFromCbor)(items[3]); // Parse ?EAD_1 const eadTokens = items.length > 4 ? (0, cbor_utils_1.parseEadItems)(items.slice(4)) : []; this.log('message_1', message); // TH_1 = H(message_1) this._th = await this.hash(message); this.log('TH_1', this._th); this._state = 2 /* State.VERIFIED_M1 */; return eadTokens; } async composeMessage2(ead) { this.assertState(2 /* State.VERIFIED_M1 */, 'composeMessage2'); // Generate ephemeral DH keypair (G_Y) await this.generateEphemeralKey(); const gY = this._ephPub; this.log('G_Y', gY); // ECDH → G_XY const gXY = Buffer.from(await this.crypto.keyAgreement(this, this._ephPrivateKey, this._peerEphPub)); this.log('G_XY', gXY); // TH_2 = H( G_Y, H(message_1) ) — RFC 9528 §5.3.2 this._th = await this.hash((0, cbor_utils_1.encodeCborSequence)(gY, this._th)); this.log('TH_2', this._th); // PRK_2e = HKDF-Extract(TH_2, G_XY) this._prk2e = await this.hkdfExtract(gXY, this._th); this.log('PRK_2e', this._prk2e); // Fetch own credentials const cred = await this.credMgr.fetch(this); const credR = (0, cbor_utils_1.getCredBytes)(cred); const idCredR = (0, cbor_utils_1.encodeIdCred)(cred); const idCredRMap = (0, cbor_utils_1.encodeIdCredMap)(cred); const credRCbor = (0, cbor_utils_1.encodeCredItem)(cred, credR); // Static DH for methods 1, 3 (responder authenticates with static DH) let gRX; if (this._method === EdhocMethod.Method1 || this._method === EdhocMethod.Method3) { gRX = Buffer.from(await this.crypto.keyAgreement(this, cred.privateKey, this._peerEphPub)); } // PRK_3e2m this._prk3e2m = await this.derivePrk3e2m(gRX); this.log('PRK_3e2m', this._prk3e2m); // MAC_2 with context_2 = << C_R, ID_CRED_R, TH_2, CRED_R, ?EAD_2 >> const context2 = this.buildContext(cbor_1.default.encode(this.connectionID), idCredRMap, this._th, credRCbor, ead); const mac2Len = this.macLength('responder'); const mac2 = await this.kdf(this._prk3e2m, 2, context2, mac2Len); this.log('MAC_2', mac2); // Signature_or_MAC_2 const sigOrMac2 = await this.signOrMac('responder', cred, idCredRMap, this._th, credRCbor, ead, mac2); this.log('Signature_or_MAC_2', sigOrMac2); // PLAINTEXT_2 = ( C_R, ID_CRED_R, Signature_or_MAC_2, ?EAD_2 ) // Uses compact idCredR (bare kid) for the wire format const pt2 = Buffer.concat([ cbor_1.default.encode(this.connectionID), (0, cbor_utils_1.encodePlaintext)(idCredR, sigOrMac2, ead), ]); this.log('PLAINTEXT_2', pt2); // KEYSTREAM_2 = EDHOC-KDF(PRK_2e, 0, TH_2, |PLAINTEXT_2|) const ks2 = await this.kdf(this._prk2e, 0, this._th, pt2.length); const ct2 = this.xor(pt2, ks2); this.log('CIPHERTEXT_2', ct2); // TH_3 = H( TH_2, PLAINTEXT_2, CRED_R ) this._th = await this.hash(Buffer.concat([cbor_1.default.encode(this._th), pt2, credRCbor])); this.log('TH_3', this._th); // message_2 = bstr( G_Y || CIPHERTEXT_2 ) — RFC 9528 §5.3.1 const msg2 = cbor_1.default.encode(Buffer.concat([gY, ct2])); this.log('message_2', msg2); this._state = 3 /* State.WAIT_M3 */; return msg2; } async processMessage2(message) { this.assertState(1 /* State.WAIT_M2 */, 'processMessage2'); // Decode outer bstr → G_Y || CIPHERTEXT_2 const inner = Buffer.from(cbor_1.default.decodeFirstSync(message)); const gY = inner.subarray(0, this._suiteParams.eccKeyLength); const ct2 = inner.subarray(this._suiteParams.eccKeyLength); this._peerEphPub = Buffer.from(gY); this.log('G_Y', gY); // ECDH → G_XY const gXY = Buffer.from(await this.crypto.keyAgreement(this, this._ephPrivateKey, gY)); this.log('G_XY', gXY); // TH_2 = H( G_Y, H(message_1) ) this._th = await this.hash((0, cbor_utils_1.encodeCborSequence)(gY, this._th)); this.log('TH_2', this._th); // PRK_2e this._prk2e = await this.hkdfExtract(gXY, this._th); this.log('PRK_2e', this._prk2e); // Decrypt PLAINTEXT_2 const ks2 = await this.kdf(this._prk2e, 0, this._th, ct2.length); const pt2 = this.xor(ct2, ks2); this.log('PLAINTEXT_2', pt2); // Parse PLAINTEXT_2: C_R, ID_CRED_R, Signature_or_MAC_2, ?EAD_2 const pt2Items = (0, cbor_utils_1.decodeCborSequence)(pt2); if (pt2Items.length < 3) throw new EdhocError('Invalid PLAINTEXT_2'); this._peerCid = (0, cbor_utils_1.connectionIdFromCbor)(pt2Items[0]); const parsed = (0, cbor_utils_1.parsePlaintext)(Buffer.concat(pt2Items.slice(1).map((item) => cbor_1.default.encode(item)))); // Verify peer credentials const peerCredPartial = (0, cbor_utils_1.decodeIdCred)(parsed.idCredRaw); const peerCred = await this.credMgr.verify(this, peerCredPartial); this._peerCredentials = peerCred; const credR = (0, cbor_utils_1.getCredBytes)(peerCred); const idCredRMap = (0, cbor_utils_1.encodeIdCredMap)(peerCred); const credRCbor = (0, cbor_utils_1.encodeCredItem)(peerCred, credR); // Static DH for methods 1, 3 let gRX; if (this._method === EdhocMethod.Method1 || this._method === EdhocMethod.Method3) { gRX = Buffer.from(await this.crypto.keyAgreement(this, this._ephPrivateKey, peerCred.publicKey)); } // PRK_3e2m this._prk3e2m = await this.derivePrk3e2m(gRX); this.log('PRK_3e2m', this._prk3e2m); // Verify MAC_2 / Signature_or_MAC_2 const context2 = this.buildContext(cbor_1.default.encode(this._peerCid), idCredRMap, this._th, credRCbor, parsed.ead.length > 0 ? parsed.ead : undefined); const mac2Len = this.macLength('responder'); const mac2 = await this.kdf(this._prk3e2m, 2, context2, mac2Len); this.log('MAC_2', mac2); await this.verifySignatureOrMac('responder', peerCred, idCredRMap, this._th, credRCbor, parsed.ead.length > 0 ? parsed.ead : undefined, mac2, parsed.signatureOrMac); // TH_3 = H( TH_2, PLAINTEXT_2, CRED_R ) this._th = await this.hash(Buffer.concat([cbor_1.default.encode(this._th), pt2, credRCbor])); this.log('TH_3', this._th); this._state = 4 /* State.VERIFIED_M2 */; return parsed.ead; } async composeMessage3(ead) { this.assertState(4 /* State.VERIFIED_M2 */, 'composeMessage3'); const th3 = this._th; // Fetch own credentials const cred = await this.credMgr.fetch(this); const credI = (0, cbor_utils_1.getCredBytes)(cred); const idCredI = (0, cbor_utils_1.encodeIdCred)(cred); const idCredIMap = (0, cbor_utils_1.encodeIdCredMap)(cred); const credICbor = (0, cbor_utils_1.encodeCredItem)(cred, credI); // Static DH for methods 2, 3 (initiator authenticates with static DH) let gIX; if (this._method === EdhocMethod.Method2 || this._method === EdhocMethod.Method3) { gIX = Buffer.from(await this.crypto.keyAgreement(this, cred.privateKey, this._peerEphPub)); } // PRK_4e3m this._prk4e3m = await this.derivePrk4e3m(th3, gIX); this.log('PRK_4e3m', this._prk4e3m); // MAC_3 with context_3 = << ID_CRED_I, TH_3, CRED_I, ?EAD_3 >> const context3 = this.buildContext(null, idCredIMap, th3, credICbor, ead); const mac3Len = this.macLength('initiator'); const mac3 = await this.kdf(this._prk4e3m, 6, context3, mac3Len); this.log('MAC_3', mac3); // Signature_or_MAC_3 const sigOrMac3 = await this.signOrMac('initiator', cred, idCredIMap, th3, credICbor, ead, mac3); this.log('Signature_or_MAC_3', sigOrMac3); // PLAINTEXT_3 = ( ID_CRED_I, Signature_or_MAC_3, ?EAD_3 ) // Uses compact idCredI (bare kid) for the wire format const pt3 = (0, cbor_utils_1.encodePlaintext)(idCredI, sigOrMac3, ead); this.log('PLAINTEXT_3', pt3); // AEAD encrypt: K_3, IV_3 const k3 = await this.kdf(this._prk3e2m, 3, th3, this._suiteParams.aeadKeyLength); const iv3 = await this.kdf(this._prk3e2m, 4, th3, this._suiteParams.aeadIvLength); // external_aad_3 = << TH_3, CRED_I, ?EAD_3 >> const aad3 = this.buildEncAad(th3, credI, ead); const ct3 = await this.aeadEncrypt(k3, iv3, aad3, pt3); this.log('CIPHERTEXT_3', ct3); // TH_4 = H( TH_3, PLAINTEXT_3, CRED_I ) this._th = await this.hash(Buffer.concat([cbor_1.default.encode(th3), pt3, credICbor])); this.log('TH_4', this._th); // PRK_out, PRK_exporter this._prkOut = await this.kdf(this._prk4e3m, 7, this._th, this._suiteParams.hashLength); this._prkExporter = await this.kdf(this._prkOut, 10, Buffer.alloc(0), this._suiteParams.hashLength); // Destroy ephemeral key await this.destroyEphemeralKey(); // message_3 = CBOR bstr of CIPHERTEXT_3 const msg3 = cbor_1.default.encode(ct3); this.log('message_3', msg3); this._state = 5 /* State.WAIT_M4_OR_DONE */; return msg3; } async processMessage3(message) { this.assertState(3 /* State.WAIT_M3 */, 'processMessage3'); const th3 = this._th; const ct3 = Buffer.from(cbor_1.default.decodeFirstSync(message)); // AEAD decrypt const k3 = await this.kdf(this._prk3e2m, 3, th3, this._suiteParams.aeadKeyLength); const iv3 = await this.kdf(this._prk3e2m, 4, th3, this._suiteParams.aeadIvLength); // Fetch own credentials first to build AAD (we need CRED_I for verifying, but AAD needs CRED_I too) // Actually, for the responder processing message 3, the AAD uses the initiator's credentials. // But we don't know CRED_I yet. The AAD for decrypt uses TH_3 only? No. // RFC 9528: external_aad_3 = << TH_3, CRED_I, ?EAD_3 >> // But CRED_I isn't known until after decryption... // Actually, for AEAD decryption, we need the AAD. Since CRED_I is inside the encrypted // PLAINTEXT_3, the responder can't know it before decryption. // Looking at RFC 9528 more carefully: the Enc_structure external_aad is just TH_3 // The << TH_3, CRED_I, ?EAD_3 >> is for the Sig_structure, not the Enc_structure. // The AEAD AAD = Enc_structure = ["Encrypt0", h'', TH_3] const aad3 = cbor_1.default.encode(['Encrypt0', Buffer.alloc(0), th3]); const pt3 = await this.aeadDecrypt(k3, iv3, aad3, ct3); this.log('PLAINTEXT_3', pt3); // Parse PLAINTEXT_3: ID_CRED_I, Signature_or_MAC_3, ?EAD_3 const parsed = (0, cbor_utils_1.parsePlaintext)(pt3); // Verify peer credentials const peerCredPartial = (0, cbor_utils_1.decodeIdCred)(parsed.idCredRaw); const peerCred = await this.credMgr.verify(this, peerCredPartial); this._peerCredentials = peerCred; const credI = (0, cbor_utils_1.getCredBytes)(peerCred); const idCredIMap = (0, cbor_utils_1.encodeIdCredMap)(peerCred); const credICbor = (0, cbor_utils_1.encodeCredItem)(peerCred, credI); // Static DH for methods 2, 3 let gIX; if (this._method === EdhocMethod.Method2 || this._method === EdhocMethod.Method3) { if (this._ephPrivateKey) { gIX = Buffer.from(await this.crypto.keyAgreement(this, this._ephPrivateKey, peerCred.publicKey)); } } // PRK_4e3m this._prk4e3m = await this.derivePrk4e3m(th3, gIX); // Verify MAC_3 const context3 = this.buildContext(null, idCredIMap, th3, credICbor, parsed.ead.length > 0 ? parsed.ead : undefined); const mac3Len = this.macLength('initiator'); const mac3 = await this.kdf(this._prk4e3m, 6, context3, mac3Len); await this.verifySignatureOrMac('initiator', peerCred, idCredIMap, th3, credICbor, parsed.ead.length > 0 ? parsed.ead : undefined, mac3, parsed.signatureOrMac); // TH_4 this._th = await this.hash(Buffer.concat([cbor_1.default.encode(th3), pt3, credICbor])); // PRK_out, PRK_exporter this._prkOut = await this.kdf(this._prk4e3m, 7, this._th, this._suiteParams.hashLength); this._prkExporter = await this.kdf(this._prkOut, 10, Buffer.alloc(0), this._suiteParams.hashLength); await this.destroyEphemeralKey(); this._state = 6 /* State.DONE */; return parsed.ead; } async composeMessage4(ead) { this.assertState(5 /* State.WAIT_M4_OR_DONE */, 'composeMessage4'); const th4 = this._th; const k4 = await this.kdf(this._prk4e3m, 8, th4, this._suiteParams.aeadKeyLength); const iv4 = await this.kdf(this._prk4e3m, 9, th4, this._suiteParams.aeadIvLength); const pt4 = ead?.length ? (0, cbor_utils_1.encodeEadItems)(ead) : Buffer.alloc(0); const aad4 = cbor_1.default.encode(['Encrypt0', Buffer.alloc(0), th4]); const ct4 = await this.aeadEncrypt(k4, iv4, aad4, pt4); const msg4 = cbor_1.default.encode(ct4); this._state = 6 /* State.DONE */; return msg4; } async processMessage4(message) { this.assertState(6 /* State.DONE */, 'processMessage4'); const th4 = this._th; const ct4 = Buffer.from(cbor_1.default.decodeFirstSync(message)); const k4 = await this.kdf(this._prk4e3m, 8, th4, this._suiteParams.aeadKeyLength); const iv4 = await this.kdf(this._prk4e3m, 9, th4, this._suiteParams.aeadIvLength); const aad4 = cbor_1.default.encode(['Encrypt0', Buffer.alloc(0), th4]); const pt4 = await this.aeadDecrypt(k4, iv4, aad4, ct4); return pt4.length > 0 ? (0, cbor_utils_1.parseEadItems)((0, cbor_utils_1.decodeCborSequence)(pt4)) : []; } // ── export API ───────────────────────────────────────────────────── async exportOSCORE() { if (!this._prkExporter) throw new EdhocError('Handshake not completed'); const masterSecret = await this.kdf(this._prkExporter, 0, Buffer.alloc(0), 16); const masterSalt = await this.kdf(this._prkExporter, 1, Buffer.alloc(0), 8); // RFC 9528 §7.2.1: Initiator Sender ID = C_R, Responder Sender ID = C_I const senderId = (0, cbor_utils_1.connectionIdToBytes)(this._peerCid); const recipientId = (0, cbor_utils_1.connectionIdToBytes)(this.connectionID); return { masterSecret, masterSalt, senderId, recipientId }; } async exportKey(exporterLabel, length) { if (!this._prkExporter) throw new EdhocError('Handshake not completed'); return this.kdf(this._prkExporter, exporterLabel, Buffer.alloc(0), length); } exportUsedPeerCredentials() { return this._peerCredentials; } async keyUpdate(context) { if (!this._prkOut) throw new EdhocError('Handshake not completed'); this._prkOut = await this.kdf(this._prkOut, 11, context, this._suiteParams.hashLength); this._prkExporter = await this.kdf(this._prkOut, 10, Buffer.alloc(0), this._suiteParams.hashLength); } // ── private helpers: key schedule ────────────────────────────────── /** EDHOC-KDF(PRK, label, context, length) = HKDF-Expand(PRK, info, length) * info is a CBOR sequence: label, context, length (NOT array-wrapped) */ async kdf(prk, label, context, length) { const info = (0, cbor_utils_1.encodeCborSequence)(label, context, length); return Buffer.from(await this.crypto.hkdfExpand(this, prk, info, length)); } /** HKDF-Extract(IKM, salt) */ async hkdfExtract(ikm, salt) { return Buffer.from(await this.crypto.hkdfExtract(this, ikm, salt)); } /** Hash */ async hash(data) { return Buffer.from(await this.crypto.hash(this, data)); } // ── private helpers: PRK derivation ──────────────────────────────── /** PRK_3e2m: Methods 0,2 → PRK_2e; Methods 1,3 → Extract(SALT_3e2m, G_RX) */ async derivePrk3e2m(gRX) { if ((this._method === EdhocMethod.Method1 || this._method === EdhocMethod.Method3) && gRX) { const salt = await this.kdf(this._prk2e, 1, this._th, this._suiteParams.hashLength); return this.hkdfExtract(gRX, salt); } return this._prk2e; } /** PRK_4e3m: Methods 0,1 → PRK_3e2m; Methods 2,3 → Extract(SALT_4e3m, G_IX) */ async derivePrk4e3m(th3, gIX) { if ((this._method === EdhocMethod.Method2 || this._method === EdhocMethod.Method3) && gIX) { const salt = await this.kdf(this._prk3e2m, 5, th3, this._suiteParams.hashLength); return this.hkdfExtract(gIX, salt); } return this._prk3e2m; } // ── private helpers: ephemeral keys ──────────────────────────────── async generateEphemeralKey() { const kp = await this.crypto.generateKeyPair(this); this._ephPrivateKey = Buffer.from(kp.privateKey); this._ephPub = Buffer.from(kp.publicKey); } async destroyEphemeralKey() { this._ephPrivateKey = null; } // ── private helpers: MAC / Signature ─────────────────────────────── /** Determine MAC length based on role and method. * Responder signature methods (0, 2): tag length; otherwise hash length. * Initiator signature methods (0, 1): tag length; otherwise hash length. */ macLength(role) { // RFC 9528: when authenticating with signature → mac_length = hash_length // when authenticating with static DH → mac_length = EDHOC MAC length const usesSig = role === 'responder' ? (this._method === EdhocMethod.Method0 || this._method === EdhocMethod.Method2) : (this._method === EdhocMethod.Method0 || this._method === EdhocMethod.Method1); return usesSig ? this._suiteParams.hashLength : this._suiteParams.macLength; } /** Build context bytes: << [C_R,] ID_CRED_x, TH, CRED_x, ?EAD >> */ buildContext(cRCbor, idCredCbor, th, credXCbor, ead) { const parts = []; if (cRCbor) parts.push(cRCbor); parts.push(idCredCbor, cbor_1.default.encode(th), credXCbor); if (ead?.length) parts.push((0, cbor_utils_1.encodeEadItems)(ead)); return Buffer.concat(parts); } /** Build Enc_structure external_aad for CIPHERTEXT_3/4: ["Encrypt0", h'', TH] */ buildEncAad(th, _credX, _ead) { // RFC 9528: AAD for AEAD = Enc_structure = ["Encrypt0", h'', TH_x] return cbor_1.default.encode(['Encrypt0', Buffer.alloc(0), th]); } /** Compute Signature_or_MAC for the local party */ async signOrMac(role, cred, idCredCbor, th, credXCbor, ead, mac) { const usesSig = role === 'responder' ? (this._method === EdhocMethod.Method0 || this._method === EdhocMethod.Method2) : (this._method === EdhocMethod.Method0 || this._method === EdhocMethod.Method1); if (!usesSig) return mac; // Sig_structure = ["Signature1", << ID_CRED_x >>, << TH, CRED_x, ?EAD >>, MAC] const externalAad = this.buildSigExternalAad(th, credXCbor, ead); const sigStructure = cbor_1.default.encode(['Signature1', idCredCbor, externalAad, mac]); return Buffer.from(await this.crypto.sign(this, cred.privateKey, sigStructure)); } /** Verify Signature_or_MAC from the peer */ async verifySignatureOrMac(peerRole, peerCred, idCredCbor, th, credXCbor, ead, mac, received) { const usesSig = peerRole === 'responder' ? (this._method === EdhocMethod.Method0 || this._method === EdhocMethod.Method2) : (this._method === EdhocMethod.Method0 || this._method === EdhocMethod.Method1); if (!usesSig) { if (!mac.equals(received)) throw new EdhocError('MAC verification failed'); return; } const externalAad = this.buildSigExternalAad(th, credXCbor, ead); const sigStructure = cbor_1.default.encode(['Signature1', idCredCbor, externalAad, mac]); await this.crypto.verify(this, peerCred.publicKey, sigStructure, received); } /** Build the external_aad bstr for Sig_structure: concatenation of CBOR items */ buildSigExternalAad(th, credXCbor, ead) { const parts = [cbor_1.default.encode(th), credXCbor]; if (ead?.length) parts.push((0, cbor_utils_1.encodeEadItems)(ead)); return Buffer.concat(parts); } // ── private helpers: AEAD ────────────────────────────────────────── async aeadEncrypt(key, iv, aad, pt) { return Buffer.from(await this.crypto.encrypt(this, key, iv, aad, pt)); } async aeadDecrypt(key, iv, aad, ct) { return Buffer.from(await this.crypto.decrypt(this, key, iv, aad, ct)); } // ── private helpers: misc ────────────────────────────────────────── xor(a, b) { const out = Buffer.alloc(a.length); for (let i = 0; i < a.length; i++) out[i] = a[i] ^ b[i]; return out; } log(name, data) { if (this.logger) this.logger(name, data); } assertState(expected, method) { if (this._state !== expected) { throw new EdhocError(`Invalid state for ${method}`); } } } exports.EDHOC = EDHOC;