diff --git a/package.json b/package.json index bb8091e..1d9f350 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d452fc9..4f25c76 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -22,7 +22,7 @@ importers: dependencies: '@exadev/wire-mesh-core': specifier: github:ExaDev/wire-mesh#path:ts/packages/core - version: https://codeload.github.com/ExaDev/wire-mesh/tar.gz/7c9445b750debb0e1e5272b6084e2f76ffe33a10#path:ts/packages/core + version: https://codeload.github.com/ExaDev/wire-mesh/tar.gz/e2b984fcb29b102868b38eef8f45afe7b0a2118f#path:ts/packages/core '@modelcontextprotocol/sdk': specifier: 1.30.0 version: 1.30.0(zod@4.4.3) @@ -543,8 +543,8 @@ packages: resolution: {integrity: sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@exadev/wire-mesh-core@https://codeload.github.com/ExaDev/wire-mesh/tar.gz/7c9445b750debb0e1e5272b6084e2f76ffe33a10#path:ts/packages/core': - resolution: {path: ts/packages/core, tarball: https://codeload.github.com/ExaDev/wire-mesh/tar.gz/7c9445b750debb0e1e5272b6084e2f76ffe33a10} + '@exadev/wire-mesh-core@https://codeload.github.com/ExaDev/wire-mesh/tar.gz/e2b984fcb29b102868b38eef8f45afe7b0a2118f#path:ts/packages/core': + resolution: {path: ts/packages/core, tarball: https://codeload.github.com/ExaDev/wire-mesh/tar.gz/e2b984fcb29b102868b38eef8f45afe7b0a2118f} version: 0.0.0 '@google/genai@1.52.0': @@ -1187,9 +1187,8 @@ packages: ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} - cddl.js@https://codeload.github.com/ExaDev/cddl.js/tar.gz/8de0f0cf0e8eb14e347894a55bdb655d5f332bc5: - resolution: {tarball: https://codeload.github.com/ExaDev/cddl.js/tar.gz/8de0f0cf0e8eb14e347894a55bdb655d5f332bc5} - version: 0.0.0 + cddl.js@1.0.1: + resolution: {integrity: sha512-UTQZM1rCob6FFOxmYJh+csg9AaKyrFjFnp6Lar/1QicpHW77UxBZsuE2r4NOMaKuvZypIXyCSU+dhGOfhAJLVA==} chalk@2.4.2: resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} @@ -3796,10 +3795,10 @@ snapshots: '@eslint/core': 1.2.1 levn: 0.4.1 - '@exadev/wire-mesh-core@https://codeload.github.com/ExaDev/wire-mesh/tar.gz/7c9445b750debb0e1e5272b6084e2f76ffe33a10#path:ts/packages/core': + '@exadev/wire-mesh-core@https://codeload.github.com/ExaDev/wire-mesh/tar.gz/e2b984fcb29b102868b38eef8f45afe7b0a2118f#path:ts/packages/core': dependencies: cbor2: 2.3.0 - cddl.js: https://codeload.github.com/ExaDev/cddl.js/tar.gz/8de0f0cf0e8eb14e347894a55bdb655d5f332bc5 + cddl.js: 1.0.1 zod: 4.5.4 '@google/genai@1.52.0(@modelcontextprotocol/sdk@1.30.0(zod@4.4.3))': @@ -4554,7 +4553,7 @@ snapshots: ccount@2.0.1: {} - cddl.js@https://codeload.github.com/ExaDev/cddl.js/tar.gz/8de0f0cf0e8eb14e347894a55bdb655d5f332bc5: + cddl.js@1.0.1: dependencies: camelcase: 9.0.0 cbor2: 2.3.0 diff --git a/src/core/identity.ts b/src/core/identity.ts index 3cde880..7c4a167 100644 --- a/src/core/identity.ts +++ b/src/core/identity.ts @@ -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. */ diff --git a/src/core/wire-mesh-identity.ts b/src/core/wire-mesh-identity.ts new file mode 100644 index 0000000..cd9a354 --- /dev/null +++ b/src/core/wire-mesh-identity.ts @@ -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 { + 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, +): Promise { + 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); +} diff --git a/src/test/wire-mesh-identity.test.ts b/src/test/wire-mesh-identity.test.ts new file mode 100644 index 0000000..ced758f --- /dev/null +++ b/src/test/wire-mesh-identity.test.ts @@ -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"), + ); +});