From 50904aec212eaa799eae4369ddcd67e2858700ad Mon Sep 17 00:00:00 2001 From: Dusk1e Date: Sun, 16 Aug 2026 18:18:52 +0300 Subject: [PATCH] fix(did): reject `did:key:z` in isDidKeyUri The guard checked `startsWith("did:key:z")` and then tested the rest of the string against the base58btc class, slicing from index 8. That is the length of `"did:key:"`, not of `"did:key:z"`, so the `z` stayed in the string being tested and satisfied the `+` in `z[a-km-zA-HJ-NP-Z1-9]+` on its own. `isDidKeyUri("did:key:z")` returned true for a DID carrying no key material, which `getDidResolver().resolve()` reports as `invalidDid`. Replace the index arithmetic with a single pattern spanning the whole URI, so the quantifier applies to the base58btc value as the documented grammar intends. `z` is itself a base58btc character, so that was the only string the old slice let through; no other input changes. --- .changeset/did-key-uri-guard.md | 13 +++++++++++++ packages/did/src/methods/did-key.test.ts | 24 ++++++++++++++++++++++++ packages/did/src/methods/did-key.ts | 14 ++++++++------ 3 files changed, 45 insertions(+), 6 deletions(-) create mode 100644 .changeset/did-key-uri-guard.md diff --git a/.changeset/did-key-uri-guard.md b/.changeset/did-key-uri-guard.md new file mode 100644 index 00000000..de8b811d --- /dev/null +++ b/.changeset/did-key-uri-guard.md @@ -0,0 +1,13 @@ +--- +"@agentcommercekit/did": patch +--- + +`isDidKeyUri` no longer accepts `did:key:z`, the multibase prefix carrying no +key material. The check sliced from index 8 — the length of `"did:key:"`, not of +`"did:key:z"` — so the `z` stayed in the string it tested and satisfied the `+` +in `z[a-km-zA-HJ-NP-Z1-9]+` by itself. The guard therefore vouched for a DID +that `getDidResolver().resolve()` reports as `invalidDid`. The check is now a +single pattern spanning the whole URI, so the quantifier applies to the +base58btc value as the documented grammar intends. Every other input is +unaffected: `z` is itself a base58btc character, so it was the only string the +old slice let through. diff --git a/packages/did/src/methods/did-key.test.ts b/packages/did/src/methods/did-key.test.ts index 635cfb9c..e66a7b6f 100644 --- a/packages/did/src/methods/did-key.test.ts +++ b/packages/did/src/methods/did-key.test.ts @@ -61,4 +61,28 @@ describe("isDidKeyUri", () => { it("returns false for invalid did:key", () => { expect(isDidKeyUri("invalid-did-key")).toBe(false) }) + + it("returns false for a multibase prefix with no key material", () => { + expect(isDidKeyUri("did:key:z")).toBe(false) + expect(isDidKeyUri("did:key:")).toBe(false) + }) + + it("returns false for characters outside the base58btc alphabet", () => { + for (const char of ["0", "O", "I", "l"]) { + expect(isDidKeyUri(`did:key:z${char}`)).toBe(false) + } + }) + + it("returns false for a non-string", () => { + expect(isDidKeyUri(undefined)).toBe(false) + expect(isDidKeyUri(123)).toBe(false) + }) + + it("does not vouch for a did:key the resolver rejects", async () => { + const resolver = getDidResolver() + const resolved = await resolver.resolve("did:key:z") + + expect(resolved.didResolutionMetadata.error).toBe("invalidDid") + expect(isDidKeyUri("did:key:z")).toBe(false) + }) }) diff --git a/packages/did/src/methods/did-key.ts b/packages/did/src/methods/did-key.ts index 18450829..4359b8ea 100644 --- a/packages/did/src/methods/did-key.ts +++ b/packages/did/src/methods/did-key.ts @@ -36,6 +36,13 @@ export const KEY_CONFIG = { }, } as const +/** + * The `did:key` grammar documented on {@link isDidKeyUri}, as a single pattern. + * The `+` applies to the base58btc value that follows the `z`, so the multibase + * prefix on its own is not a `did:key` URI. + */ +const didKeyUriRegex = /^did:key:z[a-km-zA-HJ-NP-Z1-9]+$/ + /** * Checks if a given item is a valid did:key URI, according to the following format: * did-key-format := did:key: @@ -45,12 +52,7 @@ export const KEY_CONFIG = { * @returns `true` if the value is a did:key URI, `false` otherwise */ export function isDidKeyUri(did: unknown): did is DidKeyUri { - if (typeof did !== "string" || !did.startsWith("did:key:z")) { - return false - } - - const mbValue = did.slice(8) // Get everything after "did:key:z" - return /^[a-km-zA-HJ-NP-Z1-9]+$/.test(mbValue) + return typeof did === "string" && didKeyUriRegex.test(did) } /**