Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
"generate:server-json": "tsx scripts/sync-release-metadata.ts",
"publish:mcp-registry": "tsx scripts/publish-mcp-registry.ts",
"lint": "eslint .",
"test": "node --test --test-concurrency=1 dist/test/coordinator-socket-error.integration.test.js dist/test/identity-store.test.js dist/test/identity-cert.test.js dist/test/identity-device-id.test.js dist/test/broadcast-window.integration.test.js dist/test/identity-restart.integration.test.js dist/test/mesh-e2e.integration.test.js dist/test/tls-transport.integration.test.js dist/test/peer-id-verification.integration.test.js dist/test/become-coordinator-actual-port.integration.test.js dist/test/state-sync-convergence.test.js dist/test/downtime-replay.test.js dist/test/downtime-replay.integration.test.js dist/test/filestore.test.js dist/test/handshake.test.js dist/test/approval.integration.test.js dist/test/listener-policy.integration.test.js dist/test/mesh-smoke.integration.test.js",
"test": "node --test --test-concurrency=1 dist/test/coordinator-socket-error.integration.test.js dist/test/identity-store.test.js dist/test/identity-cert.test.js dist/test/identity-device-id.test.js dist/test/wire-mesh-identity.test.js dist/test/broadcast-window.integration.test.js dist/test/identity-restart.integration.test.js dist/test/mesh-e2e.integration.test.js dist/test/tls-transport.integration.test.js dist/test/peer-id-verification.integration.test.js dist/test/become-coordinator-actual-port.integration.test.js dist/test/state-sync-convergence.test.js dist/test/downtime-replay.test.js dist/test/downtime-replay.integration.test.js dist/test/filestore.test.js dist/test/handshake.test.js dist/test/approval.integration.test.js dist/test/listener-policy.integration.test.js dist/test/mesh-smoke.integration.test.js",
"test:visibility": "node --test dist/test/visibility.integration.test.js",
"test:delivery": "node dist/test/delivery-receipt.runner.js",
"test:federation": "node dist/test/federation.integration.test.js",
Expand Down
17 changes: 8 additions & 9 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 10 additions & 4 deletions src/core/identity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -353,19 +353,25 @@ function pemToDer(pem: string): Buffer {
return Buffer.from(b64, "base64");
}

/** Derives wire-mesh's own device-id (SHA-256 of the raw, uncompressed SEC1 public-key point) from a PEM-encoded EC private key. */
export function deriveDeviceId(privateKeyPem: string): Uint8Array {
/** The raw, uncompressed SEC1 public-key point (0x04 || X || Y) for a PEM-encoded EC private key -- what wire-mesh's own device-id and identity-key.public-key are both derived from, never a certificate's DER encoding. */
export function rawPublicKeyFromPrivateKey(privateKeyPem: string): Uint8Array {
const publicKey = createPublicKey(privateKeyPem);
const jwk = publicKey.export({ format: "jwk" });
if (jwk.x === undefined || jwk.y === undefined) {
throw new Error("expected an EC JWK with x/y coordinates");
}
const rawPublicKey = Buffer.concat([
return Buffer.concat([
Buffer.from([UNCOMPRESSED_POINT_TAG]),
Buffer.from(jwk.x, "base64url"),
Buffer.from(jwk.y, "base64url"),
]);
return createHash("sha256").update(rawPublicKey).digest();
}

/** Derives wire-mesh's own device-id (SHA-256 of the raw, uncompressed SEC1 public-key point) from a PEM-encoded EC private key. */
export function deriveDeviceId(privateKeyPem: string): Uint8Array {
return createHash("sha256")
.update(rawPublicKeyFromPrivateKey(privateKeyPem))
.digest();
}

/** Encode DER bytes as a PEM certificate string. */
Expand Down
36 changes: 36 additions & 0 deletions src/core/wire-mesh-identity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* Adapts agent-comms' own PeerIdentity (a PEM-encoded ECDSA P-256 keypair plus a self-signed X.509 certificate, identity.ts's own concern) into wire-mesh-core's IdentityPort -- the shape MeshSession and createTlsTransport both expect. Both sides wrap the exact same keypair; only the envelope differs. The certificate itself plays no part here -- IdentityPort's own device-id is derived straight from the raw public key, identically to how identity.ts's own deriveDeviceId already works, so the two stay consistent by construction rather than by convention.
*/

import { webcrypto } from "node:crypto";
import { createNodeIdentity } from "@exadev/wire-mesh-core/adapters/node-identity";
import type { IdentityPort } from "@exadev/wire-mesh-core/ports/identity";
import type { PeerIdentity } from "./identity.js";
import { rawPublicKeyFromPrivateKey } from "./identity.js";

/** COSE algorithm identifier for ES256 (P-256 + SHA-256) -- wire-mesh's own identity-key.alg convention. */
const ES256 = -7;

/** Strips a PEM envelope down to its raw DER bytes, copied into a fresh, non-shared, whole-buffer Uint8Array -- Web Crypto's BufferSource parameters reject a view over a SharedArrayBuffer or a sub-range view, neither of which Buffer.from's return type is guaranteed not to be. */
function pemToDer(pem: string, label: string): Uint8Array<ArrayBuffer> {
const b64 = pem
.replace(new RegExp(`-----BEGIN ${label}-----`), "")
.replace(new RegExp(`-----END ${label}-----`), "")
.replace(/\s/g, "");
return Uint8Array.from(Buffer.from(b64, "base64"));
}

export async function toIdentityPort(
identity: Readonly<PeerIdentity>,
): Promise<IdentityPort> {
const privateKeyDer = pemToDer(identity.privateKey, "PRIVATE KEY");
const privateKey = await webcrypto.subtle.importKey(
"pkcs8",
privateKeyDer,
{ name: "ECDSA", namedCurve: "P-256" },
true,
["sign"],
);
const publicKeyBytes = rawPublicKeyFromPrivateKey(identity.privateKey);
return createNodeIdentity(privateKey, publicKeyBytes, ES256);
}
36 changes: 36 additions & 0 deletions src/test/wire-mesh-identity.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* toIdentityPort must produce the identical device-id identity.ts's own deriveDeviceId already computes for the same keypair -- the two are meant to be the same identity wrapped in two different envelopes, not two independent derivations that happen to usually agree.
*/

import * as assert from "node:assert/strict";
import { test } from "node:test";
import { generateIdentity } from "../core/identity.js";
import { toIdentityPort } from "../core/wire-mesh-identity.js";

void test("toIdentityPort's deviceId matches identity.ts's own deriveDeviceId", async () => {
const identity = generateIdentity();
const port = await toIdentityPort(identity);
assert.equal(
Buffer.from(port.deviceId).toString("hex"),
Buffer.from(identity.deviceId).toString("hex"),
);
});

void test("toIdentityPort can sign and verify its own signature", async () => {
const identity = generateIdentity();
const port = await toIdentityPort(identity);
const message = Buffer.from("hello wire-mesh");
const signature = await port.sign(message);
const valid = await port.verify(port.identityKey, message, signature);
assert.equal(valid, true);
});

void test("toIdentityPort's own deriveDeviceId agrees with its own deviceId for its own key", async () => {
const identity = generateIdentity();
const port = await toIdentityPort(identity);
const rederived = await port.deriveDeviceId(port.identityKey["public-key"]);
assert.equal(
Buffer.from(rederived).toString("hex"),
Buffer.from(port.deviceId).toString("hex"),
);
});