From 7394b84be2f6772233222b5091e52197f8af2504 Mon Sep 17 00:00:00 2001 From: Burak Date: Sat, 15 Aug 2026 11:49:50 +0300 Subject: [PATCH 1/2] fix(keys): reject invalid private JWK fields --- .changeset/harden-private-jwk-guard.md | 5 +++++ packages/keys/src/encoding/jwk.test.ts | 14 ++++++++++++++ packages/keys/src/encoding/jwk.ts | 10 +++++++--- 3 files changed, 26 insertions(+), 3 deletions(-) create mode 100644 .changeset/harden-private-jwk-guard.md diff --git a/.changeset/harden-private-jwk-guard.md b/.changeset/harden-private-jwk-guard.md new file mode 100644 index 00000000..ce03b33a --- /dev/null +++ b/.changeset/harden-private-jwk-guard.md @@ -0,0 +1,5 @@ +--- +"@agentcommercekit/keys": patch +--- + +Reject JWK objects with invalid private key fields in key type guards. diff --git a/packages/keys/src/encoding/jwk.test.ts b/packages/keys/src/encoding/jwk.test.ts index 0cb9edb1..29dfc627 100644 --- a/packages/keys/src/encoding/jwk.test.ts +++ b/packages/keys/src/encoding/jwk.test.ts @@ -2,6 +2,7 @@ import { describe, expect, test } from "vitest" import { bytesToBase64url, isBase64url } from "./base64" import { + isJwk, isPrivateKeyJwk, isPublicKeyJwk, isPublicKeyJwkEd25519, @@ -173,6 +174,19 @@ describe("JWK encoding", () => { } expect(isPrivateKeyJwk(invalidJwk)).toBe(false) }) + + test("rejects private key JWKs with invalid d values", () => { + const baseJwk = { + kty: "OKP" as const, + crv: "Ed25519" as const, + x: "base64x", + } + + expect(isPrivateKeyJwk({ ...baseJwk, d: 1 })).toBe(false) + expect(isPrivateKeyJwk({ ...baseJwk, d: "" })).toBe(false) + expect(isJwk({ ...baseJwk, d: 1 })).toBe(false) + expect(isJwk({ ...baseJwk, d: "" })).toBe(false) + }) }) describe("roundtrip", () => { diff --git a/packages/keys/src/encoding/jwk.ts b/packages/keys/src/encoding/jwk.ts index 257ac330..7e355917 100644 --- a/packages/keys/src/encoding/jwk.ts +++ b/packages/keys/src/encoding/jwk.ts @@ -6,6 +6,10 @@ function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null } +function hasValidPrivateKey(jwk: Record): boolean { + return !("d" in jwk) || (typeof jwk.d === "string" && jwk.d.length > 0) +} + /** * JWK-encoding */ @@ -96,7 +100,7 @@ function isJwkSecp256( return false } - return true + return hasValidPrivateKey(jwk) } /** @@ -142,7 +146,7 @@ export function isJwkEd25519(jwk: unknown): jwk is JwkEd25519 { return false } - return true + return hasValidPrivateKey(jwk) } export function isJwk(jwk: unknown): jwk is Jwk { @@ -178,7 +182,7 @@ export function isPublicKeyJwkEd25519( * Check if an object is a valid private key JWK */ export function isPrivateKeyJwk(jwk: unknown): jwk is PrivateKeyJwk { - return isJwk(jwk) && !!jwk.d + return isJwk(jwk) && "d" in jwk } export function isPrivateKeyJwkSecp256k1( From 4b2ba8a2878c92f2ce25823a764ae277d7956e67 Mon Sep 17 00:00:00 2001 From: Burak Date: Mon, 17 Aug 2026 03:41:48 +0300 Subject: [PATCH 2/2] test(keys): cover invalid EC private JWK fields --- packages/keys/src/encoding/jwk.test.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/keys/src/encoding/jwk.test.ts b/packages/keys/src/encoding/jwk.test.ts index 29dfc627..c0979c72 100644 --- a/packages/keys/src/encoding/jwk.test.ts +++ b/packages/keys/src/encoding/jwk.test.ts @@ -181,11 +181,21 @@ describe("JWK encoding", () => { crv: "Ed25519" as const, x: "base64x", } + const secp256k1Jwk = { + kty: "EC" as const, + crv: "secp256k1" as const, + x: "base64x", + y: "base64y", + } expect(isPrivateKeyJwk({ ...baseJwk, d: 1 })).toBe(false) expect(isPrivateKeyJwk({ ...baseJwk, d: "" })).toBe(false) expect(isJwk({ ...baseJwk, d: 1 })).toBe(false) expect(isJwk({ ...baseJwk, d: "" })).toBe(false) + expect(isPrivateKeyJwk({ ...secp256k1Jwk, d: 1 })).toBe(false) + expect(isPrivateKeyJwk({ ...secp256k1Jwk, d: "" })).toBe(false) + expect(isJwk({ ...secp256k1Jwk, d: 1 })).toBe(false) + expect(isJwk({ ...secp256k1Jwk, d: "" })).toBe(false) }) })