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
5 changes: 5 additions & 0 deletions .changeset/real-mails-press.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@agentcommercekit/keys": patch
---

Add isValidPublicKey for each of the supported curves
44 changes: 29 additions & 15 deletions packages/keys/src/curves/ed25519.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { describe, expect, test } from "vitest"
import { generateKeypair } from "./ed25519"
import {
generateKeypair,
generatePrivateKeyBytes,

Check warning on line 4 in packages/keys/src/curves/ed25519.test.ts

View workflow job for this annotation

GitHub Actions / check

'generatePrivateKeyBytes' is defined but never used. Allowed unused vars must match /^_/u
isValidPublicKey
} from "./ed25519"
import { base58ToBytes } from "../encoding/base58"

describe("Ed25519", () => {
Expand All @@ -13,7 +17,7 @@
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()

Expand All @@ -22,23 +26,33 @@
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)
})
})
})
18 changes: 18 additions & 0 deletions packages/keys/src/curves/ed25519.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
45 changes: 31 additions & 14 deletions packages/keys/src/curves/secp256k1.test.ts
Original file line number Diff line number Diff line change
@@ -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", () => {
Expand All @@ -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)
})
})
})
19 changes: 19 additions & 0 deletions packages/keys/src/curves/secp256k1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
45 changes: 31 additions & 14 deletions packages/keys/src/curves/secp256r1.test.ts
Original file line number Diff line number Diff line change
@@ -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", () => {
Expand All @@ -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)
})
})
})
19 changes: 19 additions & 0 deletions packages/keys/src/curves/secp256r1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
49 changes: 37 additions & 12 deletions packages/keys/src/public-key.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
})
Expand Down Expand Up @@ -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
})
Expand Down
30 changes: 24 additions & 6 deletions packages/keys/src/public-key.ts
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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)
}

/**
Expand All @@ -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)
*/
Expand Down
Loading