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.
213 lines (212 loc) • 8.53 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.encodeCborSequence = encodeCborSequence;
exports.decodeCborSequence = decodeCborSequence;
exports.encodeSuites = encodeSuites;
exports.connectionIdToBytes = connectionIdToBytes;
exports.connectionIdFromCbor = connectionIdFromCbor;
exports.encodeIdCred = encodeIdCred;
exports.encodeIdCredMap = encodeIdCredMap;
exports.encodeCredItem = encodeCredItem;
exports.decodeIdCred = decodeIdCred;
exports.getCredBytes = getCredBytes;
exports.encodePlaintext = encodePlaintext;
exports.parsePlaintext = parsePlaintext;
exports.encodeEadItems = encodeEadItems;
exports.parseEadItems = parseEadItems;
const cbor_1 = __importDefault(require("cbor"));
const edhoc_1 = require("./edhoc");
/** Encode a CBOR sequence (concatenated CBOR items) */
function encodeCborSequence(...items) {
return Buffer.concat(items.map(item => cbor_1.default.encode(item)));
}
/** Decode N items from a CBOR sequence buffer */
function decodeCborSequence(buf, count) {
const results = [];
let offset = 0;
while (offset < buf.length && (count === undefined || results.length < count)) {
const result = cbor_1.default.decodeFirstSync(buf.subarray(offset), { extendedResults: true });
results.push(result.value);
offset += result.length;
}
return results;
}
/** Encode SUITES_I: single int if one suite, array if multiple (selected last) */
function encodeSuites(suites, selected) {
if (suites.length === 1)
return suites[0];
const rest = suites.filter(s => s !== selected);
rest.push(selected);
return rest;
}
/** Convert a connection ID to its raw byte representation (for OSCORE IDs).
* RFC 9528 §3.3.2: the OSCORE identifier is a 1-byte bstr whose byte value
* is the CBOR encoding of the integer n ∈ [-24, 23]. */
function connectionIdToBytes(cid) {
if (Buffer.isBuffer(cid))
return cid;
// CBOR encoding of integer n:
// n >= 0 → major type 0, additional info = n → byte = n
// n < 0 → major type 1, additional info = -(n+1) → byte = 0x20 | (-(n+1))
if (cid >= 0)
return Buffer.from([cid]);
return Buffer.from([0x20 | (-(cid + 1))]);
}
/** Decode a CBOR-decoded value back to an EdhocConnectionID */
function connectionIdFromCbor(value) {
if (typeof value === 'number')
return value;
if (Buffer.isBuffer(value))
return value;
if (value instanceof Uint8Array)
return Buffer.from(value);
throw new Error(`Invalid connection ID: ${typeof value}`);
}
/** Encode ID_CRED_x for PLAINTEXT / context embedding (already CBOR) */
function encodeIdCred(credentials) {
switch (credentials.format) {
case edhoc_1.EdhocCredentialsFormat.kid: {
const kid = credentials.kid.kid;
return cbor_1.default.encode(kid);
}
case edhoc_1.EdhocCredentialsFormat.x5chain: {
const certs = credentials.x5chain.certificates;
const map = new Map();
map.set(edhoc_1.EdhocCredentialsFormat.x5chain, certs.length === 1 ? certs[0] : certs);
return cbor_1.default.encode(map);
}
case edhoc_1.EdhocCredentialsFormat.x5t: {
const x5t = credentials.x5t;
const map = new Map();
map.set(edhoc_1.EdhocCredentialsFormat.x5t, [x5t.hashAlgorithm, x5t.hash]);
return cbor_1.default.encode(map);
}
default:
throw new Error(`Unsupported credential format`);
}
}
/** Encode ID_CRED_x as a CBOR map (full form for MAC context / Sig_structure).
* For kid: {4: bstr(cbor(kid))}; for x5chain/x5t: same as encodeIdCred. */
function encodeIdCredMap(credentials) {
switch (credentials.format) {
case edhoc_1.EdhocCredentialsFormat.kid: {
const kid = credentials.kid.kid;
const kidCborBytes = cbor_1.default.encode(kid);
const map = new Map();
map.set(edhoc_1.EdhocCredentialsFormat.kid, kidCborBytes);
return cbor_1.default.encode(map);
}
default:
return encodeIdCred(credentials);
}
}
/** Encode CRED_x as a CBOR item for use in context / TH input.
* For CCS (kid + isCBOR): credBytes is already CBOR, return as-is.
* For DER certs: wrap as CBOR bstr. */
function encodeCredItem(credentials, credBytes) {
if (credentials.format === edhoc_1.EdhocCredentialsFormat.kid &&
credentials.kid.isCBOR) {
return credBytes;
}
return cbor_1.default.encode(credBytes);
}
/** Decode an ID_CRED_x value (already CBOR-decoded) into partial credentials */
function decodeIdCred(value) {
if (typeof value === 'number' || Buffer.isBuffer(value) || value instanceof Uint8Array) {
const kid = Buffer.isBuffer(value) || value instanceof Uint8Array
? Buffer.from(value) : value;
return { format: edhoc_1.EdhocCredentialsFormat.kid, kid: { kid } };
}
if (value instanceof Map) {
if (value.has(edhoc_1.EdhocCredentialsFormat.x5chain)) {
const d = value.get(edhoc_1.EdhocCredentialsFormat.x5chain);
const certificates = Array.isArray(d)
? d.map((c) => Buffer.from(c))
: [Buffer.from(d)];
return { format: edhoc_1.EdhocCredentialsFormat.x5chain, x5chain: { certificates } };
}
if (value.has(edhoc_1.EdhocCredentialsFormat.x5t)) {
const arr = value.get(edhoc_1.EdhocCredentialsFormat.x5t);
return {
format: edhoc_1.EdhocCredentialsFormat.x5t,
x5t: { hashAlgorithm: arr[0], hash: Buffer.from(arr[1]) },
};
}
if (value.has(edhoc_1.EdhocCredentialsFormat.kid)) {
return {
format: edhoc_1.EdhocCredentialsFormat.kid,
kid: { kid: value.get(edhoc_1.EdhocCredentialsFormat.kid) },
};
}
}
throw new Error(`Cannot decode ID_CRED_x`);
}
/** Get the raw credential bytes (CRED_x) from credentials */
function getCredBytes(credentials) {
switch (credentials.format) {
case edhoc_1.EdhocCredentialsFormat.kid: {
const c = credentials.kid.credentials;
if (!c)
throw new Error('KID credentials require credential data');
return c;
}
case edhoc_1.EdhocCredentialsFormat.x5chain:
return credentials.x5chain.certificates[0];
case edhoc_1.EdhocCredentialsFormat.x5t: {
const cert = credentials.x5t.certificate;
if (!cert)
throw new Error('x5t credentials require the certificate');
return cert;
}
default:
throw new Error(`Unsupported credential format for CRED_x`);
}
}
/** Encode PLAINTEXT_2/3: ID_CRED_x, Signature_or_MAC_x, ?EAD */
function encodePlaintext(idCredCbor, signatureOrMac, ead) {
const parts = [idCredCbor, cbor_1.default.encode(signatureOrMac)];
if (ead && ead.length > 0)
parts.push(encodeEadItems(ead));
return Buffer.concat(parts);
}
/** Parse PLAINTEXT_2/3 into { idCredRaw, signatureOrMac, ead } */
function parsePlaintext(data) {
const items = decodeCborSequence(data);
if (items.length < 2)
throw new Error('PLAINTEXT must contain at least 2 items');
const sigRaw = items[1];
return {
idCredRaw: items[0],
signatureOrMac: Buffer.isBuffer(sigRaw) ? sigRaw : Buffer.from(sigRaw),
ead: items.length > 2 ? parseEadItems(items.slice(2)) : [],
};
}
/** Encode EAD tokens as a CBOR sequence of (label, ?value) pairs */
function encodeEadItems(tokens) {
const parts = [];
for (const t of tokens) {
parts.push(cbor_1.default.encode(t.label));
if (t.value && t.value.length > 0)
parts.push(cbor_1.default.encode(t.value));
}
return Buffer.concat(parts);
}
/** Parse EAD items from decoded CBOR values */
function parseEadItems(items) {
const result = [];
let i = 0;
while (i < items.length) {
const label = items[i];
i++;
let value = Buffer.alloc(0);
if (i < items.length && typeof items[i] !== 'number') {
value = Buffer.isBuffer(items[i]) ? items[i] : Buffer.from(items[i]);
i++;
}
result.push({ label, value });
}
return result;
}