diff --git a/bun.lock b/bun.lock index bb9d24d..4d6d626 100644 --- a/bun.lock +++ b/bun.lock @@ -1,5 +1,6 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "x402-agentkit-poc", @@ -13,9 +14,9 @@ }, "cli": { "name": "@worldcoin/agentkit-cli", - "version": "0.1.8", + "version": "0.2.0", "bin": { - "agentkit": "dist/index.js" + "agentkit": "dist/index.js", }, "dependencies": { "@worldcoin/idkit-core": "2.1.0", @@ -32,11 +33,11 @@ }, "core": { "name": "@worldcoin/agentkit-core", - "version": "0.1.8", + "version": "0.2.0", "dependencies": { + "@noble/curves": "^1.9.1", "@scure/base": "^1.2.6", "siwe": "^2.3.2", - "tweetnacl": "^1.0.3", "viem": "^2.46.2", "zod": "^3.24.2", }, @@ -47,7 +48,7 @@ }, "x402": { "name": "@worldcoin/agentkit", - "version": "0.1.8", + "version": "0.2.0", "dependencies": { "@worldcoin/agentkit-core": "^0.1.8", "@x402/core": "^2.4.0", @@ -784,8 +785,6 @@ "tsx": ["tsx@4.21.0", "", { "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw=="], - "tweetnacl": ["tweetnacl@1.0.3", "", {}, "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw=="], - "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="], diff --git a/core/package.json b/core/package.json index 0ef66bf..ced27d7 100644 --- a/core/package.json +++ b/core/package.json @@ -31,9 +31,9 @@ "build": "tsup" }, "dependencies": { + "@noble/curves": "^1.9.1", "@scure/base": "^1.2.6", "siwe": "^2.3.2", - "tweetnacl": "^1.0.3", "viem": "^2.46.2", "zod": "^3.24.2" }, diff --git a/core/src/solana.ts b/core/src/solana.ts index 98ea89e..c29b991 100644 --- a/core/src/solana.ts +++ b/core/src/solana.ts @@ -1,4 +1,4 @@ -import nacl from 'tweetnacl' +import { ed25519 } from '@noble/curves/ed25519' import { base58 } from '@scure/base' import type { CompleteAgentkitInfo } from './types' @@ -48,7 +48,13 @@ export function formatSIWSMessage(info: CompleteAgentkitInfo, address: string): export function verifySolanaSignature(message: string, signature: Uint8Array, publicKey: Uint8Array): boolean { const messageBytes = new TextEncoder().encode(message) - return nacl.sign.detached.verify(messageBytes, signature, publicKey) + try { + return ed25519.verify(signature, messageBytes, publicKey, { zip215: false }) + } catch { + // @noble/curves throws on malformed inputs (wrong length, non-canonical points); + // tweetnacl returned false. Preserve the boolean contract for callers. + return false + } } export function decodeBase58(encoded: string): Uint8Array { diff --git a/core/tests/solana.test.ts b/core/tests/solana.test.ts new file mode 100644 index 0000000..665d8fb --- /dev/null +++ b/core/tests/solana.test.ts @@ -0,0 +1,54 @@ +import { describe, expect, it } from 'bun:test' +import { verifySolanaSignature } from '../src/solana' + +function fromHex(hex: string): Uint8Array { + const out = new Uint8Array(hex.length / 2) + for (let i = 0; i < out.length; i++) { + out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16) + } + return out +} + +// Fixture generated with tweetnacl@1.0.3 from a deterministic seed. +// Guards against regressions if the underlying ed25519 implementation changes. +const FIXTURE = { + message: + 'example.com wants you to sign in with your Solana account:\nABC\n\nURI: https://example.com\nVersion: 1\nChain ID: mainnet\nNonce: abcd1234\nIssued At: 2026-05-13T00:00:00.000Z', + publicKey: fromHex('6b80f36fa38d2942de85ff15bff2c62704c9fc9a4c1174a2dd5b8e1cd91f4326'), + signature: fromHex( + '3cbd22d2a13d291ce5a52c379f9fc2f6de624fccdaf6de7e3b0d4a7168b06c111f6c6d82e81590c13503c374c966c448c8ef374613c822ba983f61d9dc463a01' + ), +} + +describe('verifySolanaSignature', () => { + it('accepts a valid signature produced by tweetnacl', () => { + expect(verifySolanaSignature(FIXTURE.message, FIXTURE.signature, FIXTURE.publicKey)).toBe(true) + }) + + it('rejects a tampered signature', () => { + const tampered = new Uint8Array(FIXTURE.signature) + tampered[0] ^= 0x01 + expect(verifySolanaSignature(FIXTURE.message, tampered, FIXTURE.publicKey)).toBe(false) + }) + + it('rejects a tampered message', () => { + expect(verifySolanaSignature(FIXTURE.message + ' ', FIXTURE.signature, FIXTURE.publicKey)).toBe(false) + }) + + it('rejects a signature checked against the wrong public key', () => { + const wrongKey = new Uint8Array(FIXTURE.publicKey) + wrongKey[0] ^= 0x01 + expect(verifySolanaSignature(FIXTURE.message, FIXTURE.signature, wrongKey)).toBe(false) + }) + + it('returns false for a malformed signature instead of throwing', () => { + const wrongLength = new Uint8Array(10) + expect(verifySolanaSignature(FIXTURE.message, wrongLength, FIXTURE.publicKey)).toBe(false) + + const allZero = new Uint8Array(64) + expect(verifySolanaSignature(FIXTURE.message, allZero, FIXTURE.publicKey)).toBe(false) + + const allOnes = new Uint8Array(64).fill(0xff) + expect(verifySolanaSignature(FIXTURE.message, allOnes, FIXTURE.publicKey)).toBe(false) + }) +})