diff --git a/.changeset/real-mails-press.md b/.changeset/real-mails-press.md new file mode 100644 index 0000000..c5695a1 --- /dev/null +++ b/.changeset/real-mails-press.md @@ -0,0 +1,5 @@ +--- +"@agentcommercekit/keys": patch +--- + +Add isValidPublicKey for each of the supported curves diff --git a/packages/keys/src/curves/ed25519.test.ts b/packages/keys/src/curves/ed25519.test.ts index ac79321..203ca35 100644 --- a/packages/keys/src/curves/ed25519.test.ts +++ b/packages/keys/src/curves/ed25519.test.ts @@ -1,5 +1,9 @@ import { describe, expect, test } from "vitest" -import { generateKeypair } from "./ed25519" +import { + generateKeypair, + generatePrivateKeyBytes, + isValidPublicKey +} from "./ed25519" import { base58ToBytes } from "../encoding/base58" describe("Ed25519", () => { @@ -13,7 +17,7 @@ describe("Ed25519", () => { expect(keypair.curve).toBe("Ed25519") }) - test("generates a unique `Keypair`s", async () => { + test("generates unique `Keypair`s", async () => { const keypair1 = await generateKeypair() const keypair2 = await generateKeypair() @@ -22,23 +26,33 @@ describe("Ed25519", () => { expect(keypair1.curve).toBe("Ed25519") expect(keypair2.curve).toBe("Ed25519") }) - }) - test("generates keypair from valid private key", async () => { - // Using a Solana-like base58 private key - const privateKeyBase58 = "4dmKkXNHJmR1XNXbQwJhUT8Vo3PjU1GcJmZkQFRW3aqb" - const privateKeyBytes = base58ToBytes(privateKeyBase58) + test("generates keypair from valid private key", async () => { + // Using a Solana-like base58 private key + const privateKeyBase58 = "4dmKkXNHJmR1XNXbQwJhUT8Vo3PjU1GcJmZkQFRW3aqb" + const privateKeyBytes = base58ToBytes(privateKeyBase58) + + const keypair = await generateKeypair(privateKeyBytes) - const keypair = await generateKeypair(privateKeyBytes) + expect(keypair).toBeDefined() + expect(keypair.privateKey).toEqual(privateKeyBytes) + expect(keypair.publicKey).toBeInstanceOf(Uint8Array) + expect(keypair.curve).toBe("Ed25519") + }) - expect(keypair).toBeDefined() - expect(keypair.privateKey).toEqual(privateKeyBytes) - expect(keypair.publicKey).toBeInstanceOf(Uint8Array) - expect(keypair.curve).toBe("Ed25519") + test("throws error for invalid private key format", async () => { + const invalidPrivateKey = new Uint8Array([1, 2, 3]) // Too short for Ed25519 + await expect(generateKeypair(invalidPrivateKey)).rejects.toThrow() + }) }) - test("throws error for invalid private key format", async () => { - const invalidPrivateKey = new Uint8Array([1, 2, 3]) // Too short for Ed25519 - await expect(generateKeypair(invalidPrivateKey)).rejects.toThrow() + describe("isValidPublicKey()", () => { + test("validates ed25519 public keys correctly", async () => { + const keypair = await generateKeypair() + expect(isValidPublicKey(keypair.publicKey)).toBe(true) + + const invalid = keypair.publicKey.slice(0, keypair.publicKey.length - 1) + expect(isValidPublicKey(invalid)).toBe(false) + }) }) }) diff --git a/packages/keys/src/curves/ed25519.ts b/packages/keys/src/curves/ed25519.ts index 91bf672..93c5207 100644 --- a/packages/keys/src/curves/ed25519.ts +++ b/packages/keys/src/curves/ed25519.ts @@ -29,3 +29,21 @@ export async function generateKeypair( curve: "Ed25519" }) } + +/** + * Check if a public key is a valid ed25519 public key + * @param pubkey - The public key bytes to check + * @returns true if the public key is valid, false otherwise + */ +export function isValidPublicKey(pubkey: Uint8Array): boolean { + if (pubkey.length !== 32) { + return false + } + + try { + ed25519.ExtendedPoint.fromHex(pubkey) + return true + } catch { + return false + } +} diff --git a/packages/keys/src/curves/secp256k1.test.ts b/packages/keys/src/curves/secp256k1.test.ts index 185b8b2..7ac0ce8 100644 --- a/packages/keys/src/curves/secp256k1.test.ts +++ b/packages/keys/src/curves/secp256k1.test.ts @@ -1,5 +1,9 @@ import { describe, expect, test } from "vitest" -import { generateKeypair } from "./secp256k1" +import { + generateKeypair, + getPublicKeyBytes, + isValidPublicKey +} from "./secp256k1" import { hexStringToBytes } from "../encoding/hex" describe("secp256k1", () => { @@ -22,23 +26,36 @@ describe("secp256k1", () => { expect(keypair1.curve).toBe("secp256k1") expect(keypair2.curve).toBe("secp256k1") }) - }) - test("generates a Keypair from valid private key", async () => { - const privateKeyBytes = hexStringToBytes( - "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" - ) + test("generates a Keypair from valid private key", async () => { + const privateKeyBytes = hexStringToBytes( + "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" + ) + + const keypair = await generateKeypair(privateKeyBytes) - const keypair = await generateKeypair(privateKeyBytes) + expect(keypair).toBeDefined() + expect(keypair.privateKey).toEqual(privateKeyBytes) + expect(keypair.publicKey).toBeInstanceOf(Uint8Array) + expect(keypair.curve).toBe("secp256k1") + }) - expect(keypair).toBeDefined() - expect(keypair.privateKey).toEqual(privateKeyBytes) - expect(keypair.publicKey).toBeInstanceOf(Uint8Array) - expect(keypair.curve).toBe("secp256k1") + test("throws an error for invalid private key format", async () => { + const invalidPrivateKey = new Uint8Array([1, 2, 3]) // Too short for secp256k1 + await expect(generateKeypair(invalidPrivateKey)).rejects.toThrow() + }) }) - test("throws an error for invalid private key format", async () => { - const invalidPrivateKey = new Uint8Array([1, 2, 3]) // Too short for secp256k1 - await expect(generateKeypair(invalidPrivateKey)).rejects.toThrow() + describe("isValidPublicKey()", () => { + test("validates secp256k1 public keys correctly", async () => { + const keypair = await generateKeypair() + expect(isValidPublicKey(keypair.publicKey)).toBe(true) + + const compressed = getPublicKeyBytes(keypair.privateKey, true) + expect(isValidPublicKey(compressed)).toBe(true) + + const invalid = keypair.publicKey.slice(0, keypair.publicKey.length - 1) + expect(isValidPublicKey(invalid)).toBe(false) + }) }) }) diff --git a/packages/keys/src/curves/secp256k1.ts b/packages/keys/src/curves/secp256k1.ts index 07ab711..b00e08b 100644 --- a/packages/keys/src/curves/secp256k1.ts +++ b/packages/keys/src/curves/secp256k1.ts @@ -32,3 +32,22 @@ export async function generateKeypair( curve: "secp256k1" }) } + +/** + * Check if a public key is a valid secp256k1 public key (either compressed or + * uncompressed) + * @param pubkey - The public key bytes to check + * @returns true if the public key is valid, false otherwise + */ +export function isValidPublicKey(pubkey: Uint8Array): boolean { + if (![33, 65].includes(pubkey.length)) { + return false + } + + try { + secp256k1.ProjectivePoint.fromHex(pubkey) + return true + } catch { + return false + } +} diff --git a/packages/keys/src/curves/secp256r1.test.ts b/packages/keys/src/curves/secp256r1.test.ts index 2f9dda5..008dc20 100644 --- a/packages/keys/src/curves/secp256r1.test.ts +++ b/packages/keys/src/curves/secp256r1.test.ts @@ -1,5 +1,9 @@ import { describe, expect, test } from "vitest" -import { generateKeypair } from "./secp256r1" +import { + generateKeypair, + getPublicKeyBytes, + isValidPublicKey +} from "./secp256r1" import { hexStringToBytes } from "../encoding/hex" describe("secp256r1", () => { @@ -22,23 +26,36 @@ describe("secp256r1", () => { expect(keypair1.curve).toBe("secp256r1") expect(keypair2.curve).toBe("secp256r1") }) - }) - test("generates a Keypair from valid private key", async () => { - const privateKeyBytes = hexStringToBytes( - "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" - ) + test("generates a Keypair from valid private key", async () => { + const privateKeyBytes = hexStringToBytes( + "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" + ) + + const keypair = await generateKeypair(privateKeyBytes) - const keypair = await generateKeypair(privateKeyBytes) + expect(keypair).toBeDefined() + expect(keypair.privateKey).toEqual(privateKeyBytes) + expect(keypair.publicKey).toBeInstanceOf(Uint8Array) + expect(keypair.curve).toBe("secp256r1") + }) - expect(keypair).toBeDefined() - expect(keypair.privateKey).toEqual(privateKeyBytes) - expect(keypair.publicKey).toBeInstanceOf(Uint8Array) - expect(keypair.curve).toBe("secp256r1") + test("throws an error for invalid private key format", async () => { + const invalidPrivateKey = new Uint8Array([1, 2, 3]) // Too short for secp256r1 + await expect(generateKeypair(invalidPrivateKey)).rejects.toThrow() + }) }) - test("throws an error for invalid private key format", async () => { - const invalidPrivateKey = new Uint8Array([1, 2, 3]) // Too short for secp256r1 - await expect(generateKeypair(invalidPrivateKey)).rejects.toThrow() + describe("isValidPublicKey()", () => { + test("validates secp256r1 public keys correctly", async () => { + const keypair = await generateKeypair() + expect(isValidPublicKey(keypair.publicKey)).toBe(true) + + const compressed = getPublicKeyBytes(keypair.privateKey, true) + expect(isValidPublicKey(compressed)).toBe(true) + + const invalid = keypair.publicKey.slice(0, keypair.publicKey.length - 1) + expect(isValidPublicKey(invalid)).toBe(false) + }) }) }) diff --git a/packages/keys/src/curves/secp256r1.ts b/packages/keys/src/curves/secp256r1.ts index 812f82a..986d33d 100644 --- a/packages/keys/src/curves/secp256r1.ts +++ b/packages/keys/src/curves/secp256r1.ts @@ -32,3 +32,22 @@ export async function generateKeypair( curve: "secp256r1" }) } + +/** + * Check if a public key is a valid secp256k1 public key (either compressed or + * uncompressed) + * @param pubkey - The public key bytes to check + * @returns true if the public key is valid, false otherwise + */ +export function isValidPublicKey(pubkey: Uint8Array): boolean { + if (![33, 65].includes(pubkey.length)) { + return false + } + + try { + secp256r1.ProjectivePoint.fromHex(pubkey) + return true + } catch { + return false + } +} diff --git a/packages/keys/src/public-key.test.ts b/packages/keys/src/public-key.test.ts index 823f4d7..effeefa 100644 --- a/packages/keys/src/public-key.test.ts +++ b/packages/keys/src/public-key.test.ts @@ -2,16 +2,36 @@ import { describe, expect, test } from "vitest" import { isBase58 } from "./encoding/base58" import { base64urlToBytes, isBase64url } from "./encoding/base64" import { isHexString } from "./encoding/hex" -import { isPublicKeyJwkEd25519, isPublicKeyJwkSecp256k1 } from "./encoding/jwk" +import { + isPublicKeyJwkEd25519, + isPublicKeyJwkSecp256k1, + isPublicKeyJwkSecp256r1 +} from "./encoding/jwk" import { isMultibase } from "./encoding/multibase" +import { keyCurves } from "./key-curves" import { generateKeypair } from "./keypair" -import { encodePublicKeyFromKeypair, publicKeyEncodings } from "./public-key" +import { + encodePublicKeyFromKeypair, + isValidPublicKey, + publicKeyEncodings +} from "./public-key" -const keyCurves = ["secp256k1", "Ed25519"] as const - -describe("public key encoding", () => { +describe("public-key methods", () => { describe.each(keyCurves)("curve: %s", (curve) => { - describe.each(publicKeyEncodings)("format: %s", (format) => { + describe("isValidPublicKey()", () => { + test("validates public keys correctly", async () => { + const keypair = await generateKeypair(curve) + expect(isValidPublicKey(keypair.publicKey, curve)).toBe(true) + + const tooShort = keypair.publicKey.slice( + 0, + keypair.publicKey.length - 1 + ) + expect(isValidPublicKey(tooShort, curve)).toBe(false) + }) + }) + + describe.each(publicKeyEncodings)("encoding: %s", (format) => { test("encodes public key correctly", async () => { const keypair = await generateKeypair(curve) const publicKey = encodePublicKeyFromKeypair(format, keypair) @@ -22,14 +42,19 @@ describe("public key encoding", () => { expect(isHexString(publicKeyValue)).toBe(true) break case "jwk": - if (curve === "secp256k1") { - if (!isPublicKeyJwkSecp256k1(publicKeyValue)) { - throw new Error("Invalid JWK") + if (curve === "secp256k1" || curve === "secp256r1") { + if ( + !isPublicKeyJwkSecp256k1(publicKeyValue) && + !isPublicKeyJwkSecp256r1(publicKeyValue) + ) { + throw new Error( + `Invalid JWK: ${JSON.stringify(publicKeyValue)}` + ) } expect(publicKeyValue).toEqual({ kty: "EC", - crv: "secp256k1", + crv: curve, x: expect.any(String) as unknown, y: expect.any(String) as unknown }) @@ -76,10 +101,10 @@ describe("public key encoding", () => { test("formats to JWK", async () => { const keypair = await generateKeypair(curve) const jwk = encodePublicKeyFromKeypair("jwk", keypair) - if (curve === "secp256k1") { + if (curve === "secp256k1" || curve === "secp256r1") { expect(jwk.value).toEqual({ kty: "EC", - crv: "secp256k1", + crv: curve, x: expect.any(String) as unknown, y: expect.any(String) as unknown }) diff --git a/packages/keys/src/public-key.ts b/packages/keys/src/public-key.ts index 8283553..8256920 100644 --- a/packages/keys/src/public-key.ts +++ b/packages/keys/src/public-key.ts @@ -1,6 +1,6 @@ -import { getPublicKeyBytes as getEd25519PublicKeyBytes } from "./curves/ed25519" -import { getPublicKeyBytes as getSecp256k1PublicKeyBytes } from "./curves/secp256k1" -import { getPublicKeyBytes as getSecp256r1PublicKeyBytes } from "./curves/secp256r1" +import * as ed25519 from "./curves/ed25519" +import * as secp256k1 from "./curves/secp256k1" +import * as secp256r1 from "./curves/secp256r1" import { bytesToBase58 } from "./encoding/base58" import { bytesToHexString } from "./encoding/hex" import { publicKeyBytesToJwk } from "./encoding/jwk" @@ -46,14 +46,14 @@ export function getPublicKeyFromPrivateKey( compressed = false ): Uint8Array { if (curve === "secp256k1") { - return getSecp256k1PublicKeyBytes(privateKey, compressed) + return secp256k1.getPublicKeyBytes(privateKey, compressed) } if (curve === "secp256r1") { - return getSecp256r1PublicKeyBytes(privateKey, compressed) + return secp256r1.getPublicKeyBytes(privateKey, compressed) } - return getEd25519PublicKeyBytes(privateKey) + return ed25519.getPublicKeyBytes(privateKey) } /** @@ -63,6 +63,24 @@ export function getCompressedPublicKey(keypair: Keypair): Uint8Array { return getPublicKeyFromPrivateKey(keypair.privateKey, keypair.curve, true) } +/** + * Check if a public key is valid for a given curve + */ +export function isValidPublicKey( + publicKey: Uint8Array, + curve: KeyCurve +): boolean { + if (curve === "secp256k1") { + return secp256k1.isValidPublicKey(publicKey) + } + + if (curve === "secp256r1") { + return secp256r1.isValidPublicKey(publicKey) + } + + return ed25519.isValidPublicKey(publicKey) +} + /** * Convert a public key to a multibase string (used for DID:key) */