From 578e342fabf0805f75feb5d4ce9aece32062482e Mon Sep 17 00:00:00 2001 From: Burak Date: Thu, 3 Sep 2026 04:15:37 +0300 Subject: [PATCH 1/3] 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 b706060280decb92caeabeebd094aad9bd85a880 Mon Sep 17 00:00:00 2001 From: Burak Date: Thu, 3 Sep 2026 04:15:37 +0300 Subject: [PATCH 2/3] 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) }) }) From 8ace4acc41c6be5e323846b84b5f7855f0869a77 Mon Sep 17 00:00:00 2001 From: Burak Date: Thu, 3 Sep 2026 04:15:37 +0300 Subject: [PATCH 3/3] fix(keys): clarify private JWK field helper name --- packages/keys/src/encoding/jwk.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/keys/src/encoding/jwk.ts b/packages/keys/src/encoding/jwk.ts index 7e355917..fdae2e99 100644 --- a/packages/keys/src/encoding/jwk.ts +++ b/packages/keys/src/encoding/jwk.ts @@ -6,7 +6,7 @@ function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null } -function hasValidPrivateKey(jwk: Record): boolean { +function hasValidPrivateKeyField(jwk: Record): boolean { return !("d" in jwk) || (typeof jwk.d === "string" && jwk.d.length > 0) } @@ -100,7 +100,7 @@ function isJwkSecp256( return false } - return hasValidPrivateKey(jwk) + return hasValidPrivateKeyField(jwk) } /** @@ -146,7 +146,7 @@ export function isJwkEd25519(jwk: unknown): jwk is JwkEd25519 { return false } - return hasValidPrivateKey(jwk) + return hasValidPrivateKeyField(jwk) } export function isJwk(jwk: unknown): jwk is Jwk {