diff --git a/.changeset/lovely-rabbits-swim.md b/.changeset/lovely-rabbits-swim.md new file mode 100644 index 0000000..145dc8f --- /dev/null +++ b/.changeset/lovely-rabbits-swim.md @@ -0,0 +1,6 @@ +--- +"@agentcommercekit/keys": patch +"@agentcommercekit/did": patch +--- + +Improve JWK methods, add did:jwks support diff --git a/packages/did/package.json b/packages/did/package.json index 8c976db..714f5ae 100644 --- a/packages/did/package.json +++ b/packages/did/package.json @@ -58,6 +58,7 @@ "@agentcommercekit/caip": "workspace:*", "@agentcommercekit/keys": "workspace:*", "did-resolver": "^4.1.0", + "jwks-did-resolver": "^0.3.0", "key-did-resolver": "^4.0.0", "valibot": "^1.1.0", "varint": "^6.0.0" diff --git a/packages/did/src/create-did-document.test.ts b/packages/did/src/create-did-document.test.ts index 65a05d7..5bce751 100644 --- a/packages/did/src/create-did-document.test.ts +++ b/packages/did/src/create-did-document.test.ts @@ -4,6 +4,10 @@ import { keyCurves, publicKeyEncodings } from "@agentcommercekit/keys" +import { + bytesToMultibase, + publicKeyBytesToJwk +} from "@agentcommercekit/keys/encoding" import { beforeEach, describe, expect, test } from "vitest" import { createDidDocument, @@ -32,13 +36,6 @@ const contextMap = { base58: "https://w3id.org/security/multikey/v1" } -const encodingToPropertyMap = { - hex: "publicKeyMultibase", - jwk: "publicKeyJwk", - multibase: "publicKeyMultibase", - base58: "publicKeyMultibase" -} as const - describe("createDidDocument() and createDidDocumentFromKeypair()", () => { const did = "did:web:example.com" let secp256k1Keypair: Keypair @@ -76,19 +73,25 @@ describe("createDidDocument() and createDidDocumentFromKeypair()", () => { }) const keyId = `${did}#${encodingMap[encoding]}-1` + const expectedVerificationMethod = + encoding === "jwk" + ? { + id: keyId, + type: keyTypeMap[encoding], + controller: did, + publicKeyJwk: publicKeyBytesToJwk(keypair.publicKey, curve) + } + : { + id: keyId, + type: keyTypeMap[encoding], + controller: did, + publicKeyMultibase: bytesToMultibase(keypair.publicKey) + } + const expectedDocument = { "@context": ["https://www.w3.org/ns/did/v1", contextMap[encoding]], id: did, - verificationMethod: [ - { - id: keyId, - type: keyTypeMap[encoding], - controller: did, - [encodingToPropertyMap[encoding]]: expect.any( - encoding === "jwk" ? Object : String - ) as unknown - } - ], + verificationMethod: [expectedVerificationMethod], authentication: [keyId], assertionMethod: [keyId] } diff --git a/packages/did/src/create-did-document.ts b/packages/did/src/create-did-document.ts index a308e03..14d9d6c 100644 --- a/packages/did/src/create-did-document.ts +++ b/packages/did/src/create-did-document.ts @@ -35,9 +35,11 @@ interface CreateVerificationMethodOptions { */ export function createVerificationMethod({ did, - publicKey + publicKey: publicKeyWithEncoding }: CreateVerificationMethodOptions): VerificationMethod { - const { encoding, value } = convertLegacyPublicKeyToMultibase(publicKey) + const { encoding, value: publicKey } = convertLegacyPublicKeyToMultibase( + publicKeyWithEncoding + ) const verificationMethod: VerificationMethod = { id: `${did}#${encoding}-1`, @@ -49,11 +51,11 @@ export function createVerificationMethod({ switch (encoding) { case "jwk": verificationMethod.type = "JsonWebKey2020" - verificationMethod.publicKeyJwk = value + verificationMethod.publicKeyJwk = publicKey break case "multibase": verificationMethod.type = "Multikey" - verificationMethod.publicKeyMultibase = value + verificationMethod.publicKeyMultibase = publicKey break } diff --git a/packages/did/src/did-resolvers/get-did-resolver.ts b/packages/did/src/did-resolvers/get-did-resolver.ts index 98d8580..61b8ec8 100644 --- a/packages/did/src/did-resolvers/get-did-resolver.ts +++ b/packages/did/src/did-resolvers/get-did-resolver.ts @@ -1,3 +1,4 @@ +import { getResolver as getJwksDidResolver } from "jwks-did-resolver" import { getResolver as getKeyDidResolver } from "key-did-resolver" import { DidResolver } from "./did-resolver" import { getResolver as getPkhDidResolver } from "./pkh-did-resolver" @@ -26,12 +27,14 @@ export function getDidResolver({ }: GetDidResolverOptions = {}): DidResolver { const keyResolver = getKeyDidResolver() const webResolver = getWebDidResolver(webOptions) + const jwksResolver = getJwksDidResolver(webOptions) const pkhResolver = getPkhDidResolver() const didResolver = new DidResolver( { ...keyResolver, ...webResolver, + ...jwksResolver, ...pkhResolver }, options diff --git a/packages/keys/src/encoding/jwk.ts b/packages/keys/src/encoding/jwk.ts index a5a3f74..d319ade 100644 --- a/packages/keys/src/encoding/jwk.ts +++ b/packages/keys/src/encoding/jwk.ts @@ -3,44 +3,79 @@ import { getPublicKeyFromPrivateKey } from "../public-key" import type { KeyCurve } from "../key-curves" /** - * JWK-encoding, specifically limited to public keys + * JWK-encoding */ -export type PublicKeyJwkSecp256k1 = { +export type JwkSecp256k1 = { kty: "EC" crv: "secp256k1" x: string // base64url encoded x-coordinate y: string // base64url encoded y-coordinate + d?: string // base64url encoded private key } -export type PublicKeyJwkSecp256r1 = { +export type PublicKeyJwkSecp256k1 = JwkSecp256k1 & { + d?: never +} + +export type PrivateKeyJwkSecp256k1 = JwkSecp256k1 & { + d: string // base64url encoded private key +} + +export type JwkSecp256r1 = { kty: "EC" crv: "secp256r1" x: string // base64url encoded x-coordinate y: string // base64url encoded y-coordinate + d?: string // base64url encoded private key } -export type PublicKeyJwkEd25519 = { +export type PublicKeyJwkSecp256r1 = JwkSecp256r1 & { + d?: never +} + +export type PrivateKeyJwkSecp256r1 = JwkSecp256r1 & { + d: string // base64url encoded private key +} + +export type JwkEd25519 = { kty: "OKP" crv: "Ed25519" x: string // base64url encoded x-coordinate + d?: string // base64url encoded private key +} + +export type PublicKeyJwkEd25519 = JwkEd25519 & { + d?: never +} + +export type PrivateKeyJwkEd25519 = JwkEd25519 & { + d: string // base64url encoded private key } +export type Jwk = JwkSecp256k1 | JwkSecp256r1 | JwkEd25519 + export type PublicKeyJwk = | PublicKeyJwkSecp256k1 | PublicKeyJwkSecp256r1 | PublicKeyJwkEd25519 +export type PrivateKeyJwk = + | PrivateKeyJwkSecp256k1 + | PrivateKeyJwkSecp256r1 + | PrivateKeyJwkEd25519 + /** - * JWK-encoding for private keys + * Check if an object is a valid secp256k1 or secp256r1 public key (or private + * key) JWK + * + * @param jwk - The JWK to check + * @param crv - The curve to check + * @returns True if the JWK is a valid secp256k1 or secp256r1 public key JWK */ -export type PrivateKeyJwk = PublicKeyJwk & { - d: string // base64url encoded private key -} - -function isPublicKeyJwkSecp256( +function isJwkSecp256( jwk: unknown, crv: "secp256k1" | "secp256r1" -): jwk is PublicKeyJwkSecp256k1 | PublicKeyJwkSecp256r1 { +): jwk is JwkSecp256k1 | JwkSecp256r1 { if (typeof jwk !== "object" || jwk === null) { return false } @@ -62,21 +97,33 @@ function isPublicKeyJwkSecp256( return true } -export function isPublicKeyJwkSecp256k1( - jwk: unknown -): jwk is PublicKeyJwkSecp256k1 { - return isPublicKeyJwkSecp256(jwk, "secp256k1") +/** + * Check if an object is a valid secp256k1 public key (or private key) JWK + * + * @param jwk - The JWK to check + * @returns True if the JWK is a valid secp256k1 public key JWK + */ +export function isJwkSecp256k1(jwk: unknown): jwk is JwkSecp256k1 { + return isJwkSecp256(jwk, "secp256k1") } -export function isPublicKeyJwkSecp256r1( - jwk: unknown -): jwk is PublicKeyJwkSecp256r1 { - return isPublicKeyJwkSecp256(jwk, "secp256r1") +/** + * Check if an object is a valid secp256r1 public key (or private key) JWK + * + * @param jwk - The JWK to check + * @returns True if the JWK is a valid secp256r1 public key JWK + */ +export function isJwkSecp256r1(jwk: unknown): jwk is JwkSecp256r1 { + return isJwkSecp256(jwk, "secp256r1") } -export function isPublicKeyJwkEd25519( - jwk: unknown -): jwk is PublicKeyJwkEd25519 { +/** + * Check if an object is a valid Ed25519 public key (or private key) JWK + * + * @param jwk - The JWK to check + * @returns True if the JWK is a valid Ed25519 public key JWK + */ +export function isJwkEd25519(jwk: unknown): jwk is JwkEd25519 { if (typeof jwk !== "object" || jwk === null) { return false } @@ -98,27 +145,75 @@ export function isPublicKeyJwkEd25519( return true } +export function isJwk(jwk: unknown): jwk is Jwk { + return isJwkSecp256k1(jwk) || isJwkSecp256r1(jwk) || isJwkEd25519(jwk) +} + /** * Check if an object is a valid public key JWK */ export function isPublicKeyJwk(jwk: unknown): jwk is PublicKeyJwk { - return ( - isPublicKeyJwkSecp256k1(jwk) || - isPublicKeyJwkSecp256r1(jwk) || - isPublicKeyJwkEd25519(jwk) - ) + return isJwk(jwk) && !("d" in jwk) +} + +export function isPublicKeyJwkSecp256k1( + jwk: unknown +): jwk is PublicKeyJwkSecp256k1 { + return isJwkSecp256k1(jwk) && isPublicKeyJwk(jwk) +} + +export function isPublicKeyJwkSecp256r1( + jwk: unknown +): jwk is PublicKeyJwkSecp256r1 { + return isJwkSecp256r1(jwk) && isPublicKeyJwk(jwk) +} + +export function isPublicKeyJwkEd25519( + jwk: unknown +): jwk is PublicKeyJwkEd25519 { + return isJwkEd25519(jwk) && isPublicKeyJwk(jwk) } /** * Check if an object is a valid private key JWK */ export function isPrivateKeyJwk(jwk: unknown): jwk is PrivateKeyJwk { - if (!isPublicKeyJwk(jwk)) { - return false + return isJwk(jwk) && !!jwk.d +} + +export function isPrivateKeyJwkSecp256k1( + jwk: unknown +): jwk is PrivateKeyJwkSecp256k1 { + return isJwkSecp256k1(jwk) && isPrivateKeyJwk(jwk) +} + +export function isPrivateKeyJwkSecp256r1( + jwk: unknown +): jwk is PrivateKeyJwkSecp256r1 { + return isJwkSecp256r1(jwk) && isPrivateKeyJwk(jwk) +} + +export function isPrivateKeyJwkEd25519( + jwk: unknown +): jwk is PrivateKeyJwkEd25519 { + return isJwkEd25519(jwk) && isPrivateKeyJwk(jwk) +} + +/** + * Get the public key JWK from a private key JWK + * + * @param jwk - The private key JWK + * @returns The public key JWK + */ +export function getPublicKeyJwk( + jwk: PrivateKeyJwk | PublicKeyJwk +): PublicKeyJwk { + if (isPrivateKeyJwk(jwk)) { + const { d: _d, ...publicKeyJwk } = jwk + return publicKeyJwk } - const obj = jwk as Record - return typeof obj.d === "string" && obj.d.length > 0 + return jwk } /** diff --git a/packages/keys/src/keypair.ts b/packages/keys/src/keypair.ts index 98f6779..5530e40 100644 --- a/packages/keys/src/keypair.ts +++ b/packages/keys/src/keypair.ts @@ -2,7 +2,11 @@ import * as ed25519 from "./curves/ed25519" import * as secp256k1 from "./curves/secp256k1" import * as secp256r1 from "./curves/secp256r1" import { base64urlToBytes } from "./encoding/base64" -import { privateKeyBytesToJwk, publicKeyJwkToBytes } from "./encoding/jwk" +import { + getPublicKeyJwk, + privateKeyBytesToJwk, + publicKeyJwkToBytes +} from "./encoding/jwk" import type { PrivateKeyJwk } from "./encoding/jwk" import type { KeyCurve } from "./key-curves" @@ -63,8 +67,10 @@ export function keypairToJwk(keypair: Keypair): PrivateKeyJwk { * @returns A Keypair */ export function jwkToKeypair(jwk: PrivateKeyJwk): Keypair { + const publicKeyJwk = getPublicKeyJwk(jwk) + return { - publicKey: publicKeyJwkToBytes(jwk), + publicKey: publicKeyJwkToBytes(publicKeyJwk), privateKey: base64urlToBytes(jwk.d), curve: jwk.crv } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3d69701..bcc1f3c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -592,6 +592,9 @@ importers: did-resolver: specifier: ^4.1.0 version: 4.1.0 + jwks-did-resolver: + specifier: ^0.3.0 + version: 0.3.0(typescript@5.8.3)(zod@3.25.4) key-did-resolver: specifier: ^4.0.0 version: 4.0.0 @@ -3285,6 +3288,10 @@ packages: devtools-protocol@0.0.1312386: resolution: {integrity: sha512-DPnhUXvmvKT2dFA/j7B+riVLUt9Q6RKJlcppojL5CoRywJJKLDYnRlw0gTFKfgDPHP5E04UoB71SxoJlVZy8FA==} + did-jwks@0.3.0: + resolution: {integrity: sha512-eMV9BVKGvyWk6q8fuAca3ioPMCjz172HwtfmYukZ5lS0xZuQV6PdrlQdj3CvuoG3G3QwGoStdrsOccPeuP9O0Q==} + hasBin: true + did-jwt-vc@4.0.13: resolution: {integrity: sha512-T1IUneS7Rgpao8dOeZy7dMUvAvcLLn7T8YlWRk/8HsEpaVLDx5NrjRfbfDJU8FL8CI8aBIAhoDnPQO3PNV+BWg==} engines: {node: '>=18'} @@ -4422,6 +4429,9 @@ packages: resolution: {integrity: sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==} engines: {node: '>=0.10.0'} + jwks-did-resolver@0.3.0: + resolution: {integrity: sha512-ptSjTICfy6T97C7WqHwAHeJnDPbokDmfosI4nHtuiAnmi15wOAteMQFNuAzvmscj0ml2HevVRfa5DIXB2TO6GA==} + katex@0.16.22: resolution: {integrity: sha512-XCHRdUw4lf3SKBaJe4EvgqIuWwkPSo9XoeO8GjQW94Bp7TWv9hNhzZjZ+OH9yf1UmLygb7DIT5GSFQiyt16zYg==} hasBin: true @@ -6272,6 +6282,17 @@ packages: wcwidth@1.0.1: resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} + web-identity-schemas@0.1.6: + resolution: {integrity: sha512-BDjKlPjiwlbb5nDxGRpjdJGU6uMa793u+dR8HtJ2309cGf1hcHzccdl5moXj1LEnjVlty8yETEWLcDXaFq6ZDw==} + peerDependencies: + valibot: ^1.0.0 + zod: ^4.0.0 + peerDependenciesMeta: + valibot: + optional: true + zod: + optional: true + web-namespaces@2.0.1: resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} @@ -9065,6 +9086,14 @@ snapshots: devtools-protocol@0.0.1312386: {} + did-jwks@0.3.0(typescript@5.8.3)(zod@3.25.4): + dependencies: + valibot: 1.1.0(typescript@5.8.3) + web-identity-schemas: 0.1.6(valibot@1.1.0(typescript@5.8.3))(zod@3.25.4) + transitivePeerDependencies: + - typescript + - zod + did-jwt-vc@4.0.13: dependencies: did-jwt: 8.0.15 @@ -10414,6 +10443,13 @@ snapshots: jsonpointer@5.0.1: {} + jwks-did-resolver@0.3.0(typescript@5.8.3)(zod@3.25.4): + dependencies: + did-jwks: 0.3.0(typescript@5.8.3)(zod@3.25.4) + transitivePeerDependencies: + - typescript + - zod + katex@0.16.22: dependencies: commander: 8.3.0 @@ -12841,6 +12877,11 @@ snapshots: dependencies: defaults: 1.0.4 + web-identity-schemas@0.1.6(valibot@1.1.0(typescript@5.8.3))(zod@3.25.4): + optionalDependencies: + valibot: 1.1.0(typescript@5.8.3) + zod: 3.25.4 + web-namespaces@2.0.1: {} webidl-conversions@3.0.1: {}