From f522fe81b607fd77720282cc82aaef23b6ca075b Mon Sep 17 00:00:00 2001 From: Matt Venables Date: Wed, 28 May 2025 20:21:57 -0400 Subject: [PATCH 1/5] Fix formatting in payments demo --- demos/payments/src/receipt-service.ts | 14 +++++++++----- examples/issuer/README.md | 4 ++-- examples/local-did-host/README.md | 2 +- examples/verifier/README.md | 4 ++-- packages/agentcommercekit/README.md | 2 +- 5 files changed, 15 insertions(+), 11 deletions(-) diff --git a/demos/payments/src/receipt-service.ts b/demos/payments/src/receipt-service.ts index 80a55b2..76c1d53 100644 --- a/demos/payments/src/receipt-service.ts +++ b/demos/payments/src/receipt-service.ts @@ -1,6 +1,12 @@ import { serve } from "@hono/node-server" import { logger } from "@repo/api-utils/middleware/logger" -import { colors, errorMessage, log, successMessage } from "@repo/cli-tools" +import { + colors, + errorMessage, + log, + logJson, + successMessage +} from "@repo/cli-tools" import { createPaymentReceipt, getDidResolver, @@ -77,10 +83,8 @@ app.post("/", async (c) => { const paymentDetails = v.parse(paymentDetailsSchema, parsed.payload) - log( - colors.dim("Payment details:"), - colors.cyan(JSON.stringify(paymentDetails, null, 2)) - ) + log(colors.dim("Payment details:")) + logJson(paymentDetails, colors.cyan) log(colors.dim("Verifying payment token...")) // Verify the payment token is not expired, etc. diff --git a/examples/issuer/README.md b/examples/issuer/README.md index 3d17b08..f757ae1 100644 --- a/examples/issuer/README.md +++ b/examples/issuer/README.md @@ -4,8 +4,8 @@ This example showcases a **Credential Issuer** for [ACK-ID](https://www.agentcom The API allows for the issuance, verification, and revocation of the following credential types: -- `ControllerCredential`: ACK-ID credentials that prove DID ownership heirarchies. -- `PaymentReceiptCredential`: ACK-Pay creddentials that provide proof of payment that satisfies a given Payment Request. +- `ControllerCredential`: ACK-ID credentials that prove DID ownership hierarchies. +- `PaymentReceiptCredential`: ACK-Pay credentials that provide proof of payment that satisfies a given Payment Request. This issuer supports credential revocation using [StatusList2021](https://www.w3.org/community/reports/credentials/CG-FINAL-vc-status-list-2021-20230102/), which is a privacy-preserving, space-efficient mechanism for maintaining a credential revocation list. diff --git a/examples/local-did-host/README.md b/examples/local-did-host/README.md index f0cfbe0..8fa1084 100644 --- a/examples/local-did-host/README.md +++ b/examples/local-did-host/README.md @@ -8,7 +8,7 @@ When someone attempts to resolve a `did:web` document, the resolver fetches `.we This example is a basic [Hono](https://hono.dev) server with dynamic `.well-known/did.json` routes, served at subpaths, by default on the host `0.0.0.0:3458`. There are 2 identities, `agent` and `controller`, served from the subpaths `/agent/.well-known/did.json` and `/controller/.well-known/did.json`, respectively. -Per the `did:web` spec, the `did.json` file is a full DID Document. By its nature, the document does not chane often, and should typically be served as a static file from the server. In this example, we are building the DID Document dynamically per-request, which is not suitable for production environments. +Per the `did:web` spec, the `did.json` file is a full DID Document. By its nature, the document does not change often, and should typically be served as a static file from the server. In this example, we are building the DID Document dynamically per-request, which is not suitable for production environments. ## Getting Started diff --git a/examples/verifier/README.md b/examples/verifier/README.md index 90ad423..5a0e151 100644 --- a/examples/verifier/README.md +++ b/examples/verifier/README.md @@ -2,8 +2,8 @@ This example showcases a **Credential Verifier** for [ACK-ID](https://www.agentcommercekit.com/ack-id) and [ACK-Pay](https://www.agentcommercekit.com/ack-pay) Verifiable Credentials. This API is built with [Hono](https://hono.dev). -- `ControllerCredential`: ACK-ID credentials that prove DID ownership heirarchies. -- `PaymentReceiptCredential`: ACK-Pay creddentials that provide proof of payment that satisfies a given Payment Request. +- `ControllerCredential`: ACK-ID credentials that prove DID ownership hierarchies. +- `PaymentReceiptCredential`: ACK-Pay credentials that provide proof of payment that satisfies a given Payment Request. This verifier uses [StatusList2021](https://www.w3.org/community/reports/credentials/CG-FINAL-vc-status-list-2021-20230102/), to check if a credential is revoked.# Installation diff --git a/packages/agentcommercekit/README.md b/packages/agentcommercekit/README.md index 9b144e1..c88d02b 100644 --- a/packages/agentcommercekit/README.md +++ b/packages/agentcommercekit/README.md @@ -8,7 +8,7 @@ To learn more about the Agent Commerce Kit, check out the [documentation](https: ## Installation -We recommend installing the `agentcommercekit` package, which is tree-shakeable and contains everything you need to build for the ACK protiocol, even if you choose to only target ACK-ID or ACK-Pay. This is the simplest way to get started, prevent version conflicts or duplication, and makes it easy to manage updates. +We recommend installing the `agentcommercekit` package, which is tree-shakeable and contains everything you need to build for the ACK protocol, even if you choose to only target ACK-ID or ACK-Pay. This is the simplest way to get started, prevent version conflicts or duplication, and makes it easy to manage updates. ```sh npm i agentcommercekit From ce0fa6e9beec71512350a6225daef9f095a2b679 Mon Sep 17 00:00:00 2001 From: Matt Venables Date: Wed, 28 May 2025 20:22:29 -0400 Subject: [PATCH 2/5] Auto convert legacy publicKey formats to Multibase In the did 1.0 spec, publicKeyHex, publicKeyBase58, publicKeyBase64 are all superseded by publicKeyMultibase, which can handle all of those formats. This change updates our handling when creating a didDocument to use either publicKeyMultibase or publicKeyJwk, regardless of the format that was provided to the createDidDocument method. --- packages/did/src/create-did-document.test.ts | 25 ++++++- packages/did/src/create-did-document.ts | 75 ++++++++++++++++---- packages/did/src/methods/did-web.test.ts | 51 +++++++++++-- packages/keys/src/public-key.test.ts | 10 +++ packages/keys/src/public-key.ts | 22 +++++- 5 files changed, 160 insertions(+), 23 deletions(-) diff --git a/packages/did/src/create-did-document.test.ts b/packages/did/src/create-did-document.test.ts index c0db00c..7d7cb7e 100644 --- a/packages/did/src/create-did-document.test.ts +++ b/packages/did/src/create-did-document.test.ts @@ -18,11 +18,20 @@ const keyTypeMap = { Ed25519: "Ed25519VerificationKey2018" } as const +const formatMap = { + hex: "multibase", + jwk: "jwk", + multibase: "multibase", + base58: "multibase", + base64: "multibase" +} + const formatToPropertyMap = { - hex: "publicKeyHex", + hex: "publicKeyMultibase", jwk: "publicKeyJwk", multibase: "publicKeyMultibase", - base58: "publicKeyBase58" + base58: "publicKeyMultibase", + base64: "publicKeyMultibase" } as const describe("createDidDocument() and createDidDocumentFromKeypair()", () => { @@ -93,9 +102,19 @@ describe("createDidDocument() and createDidDocumentFromKeypair()", () => { } }) break + case "base64": + document = createDidDocument({ + did, + publicKey: { + format: "base64", + algorithm, + value: formatPublicKey(keypair, "base64") + } + }) + break } - const keyId = `${did}#${format}-1` + const keyId = `${did}#${formatMap[format]}-1` const expectedDocument = { "@context": [ "https://www.w3.org/ns/did/v1", diff --git a/packages/did/src/create-did-document.ts b/packages/did/src/create-did-document.ts index 21d9471..b3a505a 100644 --- a/packages/did/src/create-did-document.ts +++ b/packages/did/src/create-did-document.ts @@ -1,4 +1,10 @@ import { formatPublicKey } from "@agentcommercekit/keys" +import { + base58ToBytes, + base64ToBytes, + bytesToMultibase, + hexStringToBytes +} from "@agentcommercekit/keys/encoding" import type { DidDocument } from "./did-document" import type { DidUri } from "./did-uri" import type { @@ -23,7 +29,7 @@ export const keyConfig = { } } as const -type PublicKeyWithFormat = { +export type PublicKeyWithFormat = { [K in PublicKeyFormat]: { format: K algorithm: KeypairAlgorithm @@ -31,6 +37,16 @@ type PublicKeyWithFormat = { } }[PublicKeyFormat] +type LegacyPublicKeyFormat = "hex" | "base58" | "base64" + +type DidDocumentPublicKey = { + [K in Exclude]: { + format: K + algorithm: KeypairAlgorithm + value: PublicKeyTypeMap[K] + } +}[Exclude] + interface CreateVerificationMethodOptions { did: DidUri publicKey: PublicKeyWithFormat @@ -43,31 +59,57 @@ export function createVerificationMethod({ did, publicKey }: CreateVerificationMethodOptions): VerificationMethod { + const { format, algorithm, value } = + convertLegacyPublicKeyToMultibase(publicKey) + const verificationMethod: VerificationMethod = { - id: `${did}#${publicKey.format}-1`, - type: keyConfig[publicKey.algorithm].type, + id: `${did}#${format}-1`, + type: keyConfig[algorithm].type, controller: did } // Add public key in the requested format - switch (publicKey.format) { - case "hex": - verificationMethod.publicKeyHex = publicKey.value - break + switch (format) { case "jwk": - verificationMethod.publicKeyJwk = publicKey.value + verificationMethod.publicKeyJwk = value break case "multibase": - verificationMethod.publicKeyMultibase = publicKey.value - break - case "base58": - verificationMethod.publicKeyBase58 = publicKey.value + verificationMethod.publicKeyMultibase = value break } return verificationMethod } +function convertLegacyPublicKeyToMultibase( + publicKey: PublicKeyWithFormat +): DidDocumentPublicKey { + switch (publicKey.format) { + case "hex": + return { + format: "multibase", + algorithm: publicKey.algorithm, + value: bytesToMultibase( + hexStringToBytes(publicKey.value.replace(/^0x/, "")) + ) + } + case "base58": + return { + format: "multibase", + algorithm: publicKey.algorithm, + value: bytesToMultibase(base58ToBytes(publicKey.value)) + } + case "base64": + return { + format: "multibase", + algorithm: publicKey.algorithm, + value: bytesToMultibase(base64ToBytes(publicKey.value)) + } + default: + return publicKey + } +} + /** * Base options for creating a DID document */ @@ -214,6 +256,15 @@ export function createDidDocumentFromKeypair({ value: formatPublicKey(keypair, "base58") } }) + case "base64": + return createDidDocument({ + ...options, + publicKey: { + format: "base64", + algorithm: keypair.algorithm, + value: formatPublicKey(keypair, "base64") + } + }) default: throw new Error(`Invalid format`) } diff --git a/packages/did/src/methods/did-web.test.ts b/packages/did/src/methods/did-web.test.ts index c9be8d9..9af87ec 100644 --- a/packages/did/src/methods/did-web.test.ts +++ b/packages/did/src/methods/did-web.test.ts @@ -1,5 +1,9 @@ import { generateKeypair } from "@agentcommercekit/keys" -import { bytesToHexString } from "@agentcommercekit/keys/encoding" +import { + bytesToHexString, + bytesToJwk, + bytesToMultibase +} from "@agentcommercekit/keys/encoding" import { describe, expect, it } from "vitest" import { createDidWebDocument, createDidWebUri } from "./did-web" @@ -24,9 +28,44 @@ describe("createDidWeb", () => { }) describe("createDidWebDocument", () => { - it("generates a valid Did and Did document", async () => { + it("generates a valid DidUri and DidDocument with JWK", async () => { + const keypair = await generateKeypair("secp256k1") + const publicKeyJwk = bytesToJwk(keypair.publicKey, keypair.algorithm) + + const { did, didDocument } = createDidWebDocument({ + publicKey: { + format: "jwk", + value: publicKeyJwk, + algorithm: keypair.algorithm + }, + baseUrl: "https://example.com" + }) + + expect(did).toEqual("did:web:example.com") + + expect(didDocument).toEqual({ + "@context": [ + "https://www.w3.org/ns/did/v1", + "https://w3id.org/security#EcdsaSecp256k1VerificationKey2019" + ], + id: "did:web:example.com", + verificationMethod: [ + { + id: "did:web:example.com#jwk-1", + type: "EcdsaSecp256k1VerificationKey2019", + controller: "did:web:example.com", + publicKeyJwk + } + ], + authentication: ["did:web:example.com#jwk-1"], + assertionMethod: ["did:web:example.com#jwk-1"] + }) + }) + + it("generates a valid DidUri and DidDocument, upgrading legacy hex to multibase", async () => { const keypair = await generateKeypair("secp256k1") const publicKeyHex = bytesToHexString(keypair.publicKey) + const publicKeyMultibase = bytesToMultibase(keypair.publicKey) const { did, didDocument } = createDidWebDocument({ publicKey: { @@ -47,14 +86,14 @@ describe("createDidWebDocument", () => { id: "did:web:example.com", verificationMethod: [ { - id: "did:web:example.com#hex-1", + id: "did:web:example.com#multibase-1", type: "EcdsaSecp256k1VerificationKey2019", controller: "did:web:example.com", - publicKeyHex: publicKeyHex.replace(/^0x/, "") + publicKeyMultibase } ], - authentication: ["did:web:example.com#hex-1"], - assertionMethod: ["did:web:example.com#hex-1"] + authentication: ["did:web:example.com#multibase-1"], + assertionMethod: ["did:web:example.com#multibase-1"] }) }) }) diff --git a/packages/keys/src/public-key.test.ts b/packages/keys/src/public-key.test.ts index 3c4ed56..12c17f5 100644 --- a/packages/keys/src/public-key.test.ts +++ b/packages/keys/src/public-key.test.ts @@ -7,6 +7,7 @@ import { generateKeypair } from "./keypair" import { formatPublicKey, formatPublicKeyBase58, + formatPublicKeyBase64, formatPublicKeyHex, formatPublicKeyJwk, formatPublicKeyMultibase, @@ -60,6 +61,9 @@ describe("public key encoding", () => { case "base58": expect(isBase58(result)).toBe(true) break + case "base64": + expect(isBase64(result)).toBe(true) + break } }) }) @@ -103,6 +107,12 @@ describe("public key encoding", () => { const base58 = formatPublicKeyBase58(keypair) expect(isBase58(base58)).toBe(true) }) + + test("formats to base64", async () => { + const keypair = await generateKeypair(algorithm) + const base64 = formatPublicKeyBase64(keypair) + expect(isBase64(base64)).toBe(true) + }) }) }) }) diff --git a/packages/keys/src/public-key.ts b/packages/keys/src/public-key.ts index 8435e6b..483ed28 100644 --- a/packages/keys/src/public-key.ts +++ b/packages/keys/src/public-key.ts @@ -1,5 +1,6 @@ import { compressPublicKey } from "./curves/secp256k1" import { bytesToBase58 } from "./encoding/base58" +import { bytesToBase64 } from "./encoding/base64" import { bytesToHexString } from "./encoding/hex" import { bytesToJwk } from "./encoding/jwk" import { bytesToMultibase } from "./encoding/multibase" @@ -9,13 +10,20 @@ import type { Keypair } from "./types" /** * Public key format types */ -export const publicKeyFormats = ["hex", "jwk", "multibase", "base58"] as const +export const publicKeyFormats = [ + "hex", + "jwk", + "multibase", + "base58", + "base64" +] as const export type PublicKeyFormat = (typeof publicKeyFormats)[number] export type PublicKeyTypeMap = { hex: string jwk: PublicKeyJwk multibase: string base58: string + base64: string } export function getCompressedPublicKey(keypair: Keypair): Uint8Array { @@ -62,11 +70,21 @@ export function formatPublicKeyBase58(keypair: Keypair): string { return bytesToBase58(keypair.publicKey) } +/** + * Convert a public key to a base64 string + * @param keypair - The Keypair containing the public key + * @returns A base64 string representation of the public key + */ +export function formatPublicKeyBase64(keypair: Keypair): string { + return bytesToBase64(keypair.publicKey) +} + export const publicKeyFormatters = { hex: (keypair: Keypair) => formatPublicKeyHex(keypair), jwk: (keypair: Keypair) => formatPublicKeyJwk(keypair), multibase: (keypair: Keypair) => formatPublicKeyMultibase(keypair), - base58: (keypair: Keypair) => formatPublicKeyBase58(keypair) + base58: (keypair: Keypair) => formatPublicKeyBase58(keypair), + base64: (keypair: Keypair) => formatPublicKeyBase64(keypair) } as const /** From 9af9f7b19825ae9769a4ca767d450282aa96d051 Mon Sep 17 00:00:00 2001 From: Matt Venables Date: Wed, 28 May 2025 20:40:11 -0400 Subject: [PATCH 3/5] Allow hex strings to be caps when converting to bytes --- packages/did/src/create-did-document.ts | 4 +--- packages/keys/src/encoding/hex.test.ts | 6 ++++++ packages/keys/src/encoding/hex.ts | 6 ++++-- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/did/src/create-did-document.ts b/packages/did/src/create-did-document.ts index b3a505a..b998af3 100644 --- a/packages/did/src/create-did-document.ts +++ b/packages/did/src/create-did-document.ts @@ -89,9 +89,7 @@ function convertLegacyPublicKeyToMultibase( return { format: "multibase", algorithm: publicKey.algorithm, - value: bytesToMultibase( - hexStringToBytes(publicKey.value.replace(/^0x/, "")) - ) + value: bytesToMultibase(hexStringToBytes(publicKey.value)) } case "base58": return { diff --git a/packages/keys/src/encoding/hex.test.ts b/packages/keys/src/encoding/hex.test.ts index 8d1b48a..2685a3e 100644 --- a/packages/keys/src/encoding/hex.test.ts +++ b/packages/keys/src/encoding/hex.test.ts @@ -14,6 +14,12 @@ describe("hex encoding and decoding", () => { expect(bytes).toEqual(new Uint8Array([1, 2, 3, 4])) }) + test("handles hex strings in caps", () => { + const hex = "0XABCDEF" + const bytes = hexStringToBytes(hex) + expect(bytes).toEqual(new Uint8Array([171, 205, 239])) + }) + test("converts hex string without 0x prefix to bytes", () => { const hex = "01020304" const bytes = hexStringToBytes(hex) diff --git a/packages/keys/src/encoding/hex.ts b/packages/keys/src/encoding/hex.ts index 559ce47..aa4de17 100644 --- a/packages/keys/src/encoding/hex.ts +++ b/packages/keys/src/encoding/hex.ts @@ -24,8 +24,10 @@ export function bytesToHexString(bytes: Uint8Array): string { * ``` */ export function hexStringToBytes(hex: string): Uint8Array { - const hexWithoutPrefix = hex.startsWith("0x") ? hex.slice(2) : hex - return fromString(hexWithoutPrefix, "base16") + const hexWithoutPrefix = hex.toLowerCase().startsWith("0x") + ? hex.slice(2) + : hex + return fromString(hexWithoutPrefix.toLowerCase(), "base16") } /** From 6632ea31462b78cadbb81d812c70863a9d974fae Mon Sep 17 00:00:00 2001 From: Matt Venables Date: Thu, 29 May 2025 06:02:26 -0400 Subject: [PATCH 4/5] Explicit base64url naming, change format->encode - Renames base64* methods to be explicit that they are base64url - Renames formatPublicKey to encodePublicKey - Updates encodePublicKey to return PublicKeyWithEncoding type - Simplifies publicKey encoding interfaces --- .../src/test-helpers/did-web-with-signer.ts | 2 +- packages/did/README.md | 2 +- packages/did/src/create-did-document.test.ts | 175 ++++++------------ packages/did/src/create-did-document.ts | 109 +++-------- .../src/did-resolvers/did-resolver.test.ts | 2 +- packages/did/src/methods/did-key.test.ts | 36 ++-- packages/did/src/methods/did-pkh.ts | 4 +- packages/did/src/methods/did-web.test.ts | 4 +- packages/did/src/resolve-did.test.ts | 2 +- packages/keys/README.md | 20 +- packages/keys/src/encoding/base64.test.ts | 20 +- packages/keys/src/encoding/base64.ts | 6 +- packages/keys/src/encoding/jwk.test.ts | 12 +- packages/keys/src/encoding/jwk.ts | 58 ++++-- packages/keys/src/index.ts | 2 +- packages/keys/src/keypair.ts | 37 +--- packages/keys/src/public-key.test.ts | 80 ++++---- packages/keys/src/public-key.ts | 131 ++++++++----- 18 files changed, 294 insertions(+), 408 deletions(-) diff --git a/examples/issuer/src/test-helpers/did-web-with-signer.ts b/examples/issuer/src/test-helpers/did-web-with-signer.ts index 1e9c278..fc44d55 100644 --- a/examples/issuer/src/test-helpers/did-web-with-signer.ts +++ b/examples/issuer/src/test-helpers/did-web-with-signer.ts @@ -27,7 +27,7 @@ export async function createDidWebWithSigner( keypair, baseUrl, controller, - format: "jwk" + encoding: "jwk" }) const signer = createJwtSigner(keypair) diff --git a/packages/did/README.md b/packages/did/README.md index e50994d..575ecce 100644 --- a/packages/did/README.md +++ b/packages/did/README.md @@ -85,7 +85,7 @@ const did = createDidKeyUri(keypair) const didDocument = createDidDocumentFromKeypair({ did, keypair, - format: "hex", // Optional, defaults to "jwk" + encoding: "jwk", // Optional, defaults to "jwk" controller: "did:web:controller.example.com" // Optional }) diff --git a/packages/did/src/create-did-document.test.ts b/packages/did/src/create-did-document.test.ts index 7d7cb7e..5697086 100644 --- a/packages/did/src/create-did-document.test.ts +++ b/packages/did/src/create-did-document.test.ts @@ -1,8 +1,8 @@ import { - formatPublicKey, + encodePublicKeyFromKeypair, generateKeypair, keypairAlgorithms, - publicKeyFormats + publicKeyEncodings } from "@agentcommercekit/keys" import { beforeEach, describe, expect, test } from "vitest" import { @@ -10,28 +10,25 @@ import { createDidDocumentFromKeypair, keyConfig } from "./create-did-document" -import type { DidDocument } from "./did-document" -import type { Keypair, PublicKeyFormat } from "@agentcommercekit/keys" +import type { Keypair, PublicKeyEncoding } from "@agentcommercekit/keys" const keyTypeMap = { secp256k1: "EcdsaSecp256k1VerificationKey2019", Ed25519: "Ed25519VerificationKey2018" } as const -const formatMap = { +const encodingMap = { hex: "multibase", jwk: "jwk", multibase: "multibase", - base58: "multibase", - base64: "multibase" + base58: "multibase" } -const formatToPropertyMap = { +const encodingToPropertyMap = { hex: "publicKeyMultibase", jwk: "publicKeyJwk", multibase: "publicKeyMultibase", - base58: "publicKeyMultibase", - base64: "publicKeyMultibase" + base58: "publicKeyMultibase" } as const describe("createDidDocument() and createDidDocumentFromKeypair()", () => { @@ -50,97 +47,51 @@ describe("createDidDocument() and createDidDocumentFromKeypair()", () => { } as const describe.each(keypairAlgorithms)("algorithm: %s", (algorithm) => { - describe.each(publicKeyFormats)("format: %s", (format: PublicKeyFormat) => { - test(`generates matching documents with ${algorithm} and ${format} format`, () => { - const keypair = keypairMap[algorithm]() - - const documentFromKeypair = createDidDocumentFromKeypair({ - did, - keypair, - format - }) - - let document: DidDocument - switch (format) { - case "hex": - document = createDidDocument({ - did, - publicKey: { - format: "hex", - algorithm, - value: formatPublicKey(keypair, "hex") - } - }) - break - case "jwk": - document = createDidDocument({ - did, - publicKey: { - format: "jwk", - algorithm, - value: formatPublicKey(keypair, "jwk") - } - }) - break - case "multibase": - document = createDidDocument({ - did, - publicKey: { - format: "multibase", - algorithm, - value: formatPublicKey(keypair, "multibase") - } - }) - break - case "base58": - document = createDidDocument({ - did, - publicKey: { - format: "base58", - algorithm, - value: formatPublicKey(keypair, "base58") + describe.each(publicKeyEncodings)( + "encoding: %s", + (encoding: PublicKeyEncoding) => { + test(`generates matching documents with ${algorithm} and ${encoding} encoding`, () => { + const keypair = keypairMap[algorithm]() + + const documentFromKeypair = createDidDocumentFromKeypair({ + did, + keypair, + encoding + }) + + const document = createDidDocument({ + did, + publicKey: encodePublicKeyFromKeypair(encoding, keypair) + }) + + const keyId = `${did}#${encodingMap[encoding]}-1` + const expectedDocument = { + "@context": [ + "https://www.w3.org/ns/did/v1", + ...keyConfig[algorithm].context + ], + id: did, + verificationMethod: [ + { + id: keyId, + type: keyTypeMap[algorithm], + controller: did, + [encodingToPropertyMap[encoding]]: expect.any( + encoding === "jwk" ? Object : String + ) as unknown } - }) - break - case "base64": - document = createDidDocument({ - did, - publicKey: { - format: "base64", - algorithm, - value: formatPublicKey(keypair, "base64") - } - }) - break - } - - const keyId = `${did}#${formatMap[format]}-1` - const expectedDocument = { - "@context": [ - "https://www.w3.org/ns/did/v1", - ...keyConfig[algorithm].context - ], - id: did, - verificationMethod: [ - { - id: keyId, - type: keyTypeMap[algorithm], - controller: did, - [formatToPropertyMap[format]]: expect.any( - format === "jwk" ? Object : String - ) as unknown - } - ], - authentication: [keyId], - assertionMethod: [keyId] - } - - expect(document).toEqual(expectedDocument) - expect(documentFromKeypair).toEqual(expectedDocument) - expect(document.verificationMethod![0]!.id).toBe(keyId) - expect(documentFromKeypair.verificationMethod![0]!.id).toBe(keyId) - }) - }) + ], + authentication: [keyId], + assertionMethod: [keyId] + } + + expect(document).toEqual(expectedDocument) + expect(documentFromKeypair).toEqual(expectedDocument) + expect(document.verificationMethod![0]!.id).toBe(keyId) + expect(documentFromKeypair.verificationMethod![0]!.id).toBe(keyId) + }) + } + ) }) test("includes controller when provided", () => { @@ -154,11 +105,7 @@ describe("createDidDocument() and createDidDocumentFromKeypair()", () => { const document = createDidDocument({ did, - publicKey: { - format: "jwk", - algorithm: "secp256k1", - value: formatPublicKey(secp256k1Keypair, "jwk") - }, + publicKey: encodePublicKeyFromKeypair("jwk", secp256k1Keypair), controller }) @@ -200,19 +147,11 @@ describe("createDidDocument() and createDidDocumentFromKeypair()", () => { const document1 = createDidDocument({ did: did1, - publicKey: { - format: "jwk", - algorithm: "secp256k1", - value: formatPublicKey(secp256k1Keypair, "jwk") - } + publicKey: encodePublicKeyFromKeypair("jwk", secp256k1Keypair) }) const document2 = createDidDocument({ did: did2, - publicKey: { - format: "jwk", - algorithm: "secp256k1", - value: formatPublicKey(secp256k1Keypair, "jwk") - } + publicKey: encodePublicKeyFromKeypair("jwk", secp256k1Keypair) }) expect(document1.verificationMethod![0]!.id).toBe(`${did1}#jwk-1`) @@ -233,11 +172,7 @@ describe("createDidDocument() and createDidDocumentFromKeypair()", () => { const document = createDidDocument({ did, - publicKey: { - format: "jwk", - algorithm: "secp256k1", - value: formatPublicKey(secp256k1Keypair, "jwk") - } + publicKey: encodePublicKeyFromKeypair("jwk", secp256k1Keypair) }) const requiredFields = [ diff --git a/packages/did/src/create-did-document.ts b/packages/did/src/create-did-document.ts index b998af3..0602582 100644 --- a/packages/did/src/create-did-document.ts +++ b/packages/did/src/create-did-document.ts @@ -1,7 +1,6 @@ -import { formatPublicKey } from "@agentcommercekit/keys" +import { encodePublicKeyFromKeypair } from "@agentcommercekit/keys" import { base58ToBytes, - base64ToBytes, bytesToMultibase, hexStringToBytes } from "@agentcommercekit/keys/encoding" @@ -10,8 +9,9 @@ import type { DidUri } from "./did-uri" import type { Keypair, KeypairAlgorithm, - PublicKeyFormat, - PublicKeyTypeMap + PublicKeyEncoding, + PublicKeyTypeMap, + PublicKeyWithEncoding } from "@agentcommercekit/keys" import type { VerificationMethod } from "did-resolver" @@ -29,27 +29,19 @@ export const keyConfig = { } } as const -export type PublicKeyWithFormat = { - [K in PublicKeyFormat]: { - format: K - algorithm: KeypairAlgorithm - value: PublicKeyTypeMap[K] - } -}[PublicKeyFormat] - -type LegacyPublicKeyFormat = "hex" | "base58" | "base64" +type LegacyPublicKeyEncoding = "hex" | "base58" type DidDocumentPublicKey = { - [K in Exclude]: { - format: K + [E in Exclude]: { + encoding: E algorithm: KeypairAlgorithm - value: PublicKeyTypeMap[K] + value: PublicKeyTypeMap[E] } -}[Exclude] +}[Exclude] interface CreateVerificationMethodOptions { did: DidUri - publicKey: PublicKeyWithFormat + publicKey: PublicKeyWithEncoding } /** @@ -59,17 +51,17 @@ export function createVerificationMethod({ did, publicKey }: CreateVerificationMethodOptions): VerificationMethod { - const { format, algorithm, value } = + const { encoding, algorithm, value } = convertLegacyPublicKeyToMultibase(publicKey) const verificationMethod: VerificationMethod = { - id: `${did}#${format}-1`, + id: `${did}#${encoding}-1`, type: keyConfig[algorithm].type, controller: did } // Add public key in the requested format - switch (format) { + switch (encoding) { case "jwk": verificationMethod.publicKeyJwk = value break @@ -82,27 +74,21 @@ export function createVerificationMethod({ } function convertLegacyPublicKeyToMultibase( - publicKey: PublicKeyWithFormat + publicKey: PublicKeyWithEncoding ): DidDocumentPublicKey { - switch (publicKey.format) { + switch (publicKey.encoding) { case "hex": return { - format: "multibase", + encoding: "multibase", algorithm: publicKey.algorithm, value: bytesToMultibase(hexStringToBytes(publicKey.value)) } case "base58": return { - format: "multibase", + encoding: "multibase", algorithm: publicKey.algorithm, value: bytesToMultibase(base58ToBytes(publicKey.value)) } - case "base64": - return { - format: "multibase", - algorithm: publicKey.algorithm, - value: bytesToMultibase(base64ToBytes(publicKey.value)) - } default: return publicKey } @@ -119,7 +105,7 @@ export interface CreateDidDocumentOptions { /** * The public key to include in the DID document */ - publicKey: PublicKeyWithFormat + publicKey: PublicKeyWithEncoding /** * Additional URIs that are equivalent to this DID @@ -201,9 +187,9 @@ export type CreateDidDocumentFromKeypairOptions = Omit< */ keypair: Keypair /** - * The format of the public key + * The encoding of the public key */ - format?: PublicKeyFormat + encoding?: PublicKeyEncoding } /** @@ -214,56 +200,11 @@ export type CreateDidDocumentFromKeypairOptions = Omit< */ export function createDidDocumentFromKeypair({ keypair, - format = "jwk", + encoding = "jwk", ...options }: CreateDidDocumentFromKeypairOptions): DidDocument { - switch (format) { - case "hex": - return createDidDocument({ - ...options, - publicKey: { - format: "hex", - algorithm: keypair.algorithm, - value: formatPublicKey(keypair, "hex") - } - }) - case "jwk": - return createDidDocument({ - ...options, - publicKey: { - format: "jwk", - algorithm: keypair.algorithm, - value: formatPublicKey(keypair, "jwk") - } - }) - case "multibase": - return createDidDocument({ - ...options, - publicKey: { - format: "multibase", - algorithm: keypair.algorithm, - value: formatPublicKey(keypair, "multibase") - } - }) - case "base58": - return createDidDocument({ - ...options, - publicKey: { - format: "base58", - algorithm: keypair.algorithm, - value: formatPublicKey(keypair, "base58") - } - }) - case "base64": - return createDidDocument({ - ...options, - publicKey: { - format: "base64", - algorithm: keypair.algorithm, - value: formatPublicKey(keypair, "base64") - } - }) - default: - throw new Error(`Invalid format`) - } + return createDidDocument({ + ...options, + publicKey: encodePublicKeyFromKeypair(encoding, keypair) + }) } diff --git a/packages/did/src/did-resolvers/did-resolver.test.ts b/packages/did/src/did-resolvers/did-resolver.test.ts index 513a5d5..abddd9f 100644 --- a/packages/did/src/did-resolvers/did-resolver.test.ts +++ b/packages/did/src/did-resolvers/did-resolver.test.ts @@ -8,7 +8,7 @@ describe("DidResolver", () => { const didDocument = createDidDocument({ did, publicKey: { - format: "hex", + encoding: "hex", algorithm: "secp256k1", value: "0xc0ffee254729296a45a3885639AC7E10F9d54979" } diff --git a/packages/did/src/methods/did-key.test.ts b/packages/did/src/methods/did-key.test.ts index 41c57f0..c279c23 100644 --- a/packages/did/src/methods/did-key.test.ts +++ b/packages/did/src/methods/did-key.test.ts @@ -1,34 +1,38 @@ -import { generateKeypair, keypairFromBase58 } from "@agentcommercekit/keys" +/* eslint-disable @cspell/spellchecker */ +import { generateKeypair, jwkToKeypair } from "@agentcommercekit/keys" import { describe, expect, it } from "vitest" import { createDidKeyUri, isDidKeyUri } from "./did-key" import { getDidResolver } from "../did-resolvers/get-did-resolver" import type { DidUri } from "../did-uri" -import type { KeypairBase58 } from "@agentcommercekit/keys" +import type { PrivateKeyJwk } from "@agentcommercekit/keys/encoding" -const KNOWN_DID_KEYS: { did: DidUri; keypairBase58: KeypairBase58 }[] = [ +const KNOWN_DID_KEYS: { did: DidUri; jwk: PrivateKeyJwk }[] = [ { - did: "did:key:zQ3shuYfyvoY2K83PxQrDr7wS8anFwjVGoKsnEDNjgrn19Udv", - keypairBase58: { - publicKey: "29bWy5f4YzUpWiKAoU34PJzQpHyJJWmqFJHcwPX6svznC", - privateKey: "9Y3KWxCfMccnzGm3ztk8VxgpVpfn5Cf4WJUYtFYs5Fte", - algorithm: "secp256k1" + did: "did:key:zQ3shNCcRrVT3tm43o6JNjSjQaiBXvSb8kHtFhoNGR8eimFZs", + jwk: { + kty: "EC", + crv: "secp256k1", + x: "C70KP9BXCdKBTfjtkQA9xH7uzd7R8hYC6cSpE6CUpro", + y: "2kmbH7YTM-RJ2G596UidxkB3SiG66gi9htsriop766g", + d: "ca66hYvhFSAbm_5YsBTydV2R_hDal-ISv3trPNCFYWg" } }, { - did: "did:key:z6MkqknXmyEp9pQt6cQJLib7KkrSMvFQEKaDJsmw3amDQwzh", - keypairBase58: { - publicKey: "CJXVBizNpGvQz7Zbf9dGUfJSYLyYpSKrcrs1DJoCVjDK", - privateKey: "GyoKth6SiFGEbsSxT7D9dhF9jSCk7W6MLP7Z5815Rkbg", - algorithm: "Ed25519" + did: "did:key:z6MknEES6VA14awWdV27ab5r1jtz3d6ct2wULmvU4YgE1wQ8", + jwk: { + kty: "OKP", + crv: "Ed25519", + x: "c4cfSJSiOFUJffpI06i5Q20X8qc8Vdcw5gCxvcZy9kU", + d: "FIQVa6NvaYXJdRCoI-pNl_ScNKgf_jVjGZf7bHDxEBw" } } ] describe("createDidKeyUri()", () => { it.each(KNOWN_DID_KEYS)( - `generates a valid did:key from a $keypairBase58.algorithm public key`, - async ({ did, keypairBase58 }) => { - const keypair = keypairFromBase58(keypairBase58) + `generates a valid did:key from a $jwk.crv public key`, + async ({ did, jwk }) => { + const keypair = jwkToKeypair(jwk) const didKey = createDidKeyUri(keypair) expect(didKey).toMatch(/^did:key:z[1-9A-HJ-NP-Za-km-z]+$/) diff --git a/packages/did/src/methods/did-pkh.ts b/packages/did/src/methods/did-pkh.ts index a4d498c..a9e0101 100644 --- a/packages/did/src/methods/did-pkh.ts +++ b/packages/did/src/methods/did-pkh.ts @@ -213,7 +213,7 @@ export function createDidPkhDocument({ did, keypair, controller, - format: "hex", + encoding: "hex", additionalContexts, verificationMethod: { id: `${did}#blockchainAccountId`, @@ -230,7 +230,7 @@ export function createDidPkhDocument({ did, keypair, controller, - format: "jwk", + encoding: "jwk", additionalContexts }) diff --git a/packages/did/src/methods/did-web.test.ts b/packages/did/src/methods/did-web.test.ts index 9af87ec..ac13069 100644 --- a/packages/did/src/methods/did-web.test.ts +++ b/packages/did/src/methods/did-web.test.ts @@ -34,7 +34,7 @@ describe("createDidWebDocument", () => { const { did, didDocument } = createDidWebDocument({ publicKey: { - format: "jwk", + encoding: "jwk", value: publicKeyJwk, algorithm: keypair.algorithm }, @@ -69,7 +69,7 @@ describe("createDidWebDocument", () => { const { did, didDocument } = createDidWebDocument({ publicKey: { - format: "hex", + encoding: "hex", value: publicKeyHex, algorithm: keypair.algorithm }, diff --git a/packages/did/src/resolve-did.test.ts b/packages/did/src/resolve-did.test.ts index 96f395f..bbf971d 100644 --- a/packages/did/src/resolve-did.test.ts +++ b/packages/did/src/resolve-did.test.ts @@ -12,7 +12,7 @@ async function generateDid(baseUrl: string, controller?: DidUri) { return createDidWebDocument({ publicKey: { - format: "hex", + encoding: "hex", value: bytesToHexString(keypair.publicKey), algorithm: keypair.algorithm }, diff --git a/packages/keys/README.md b/packages/keys/README.md index 683b6cd..fe0d2fb 100644 --- a/packages/keys/README.md +++ b/packages/keys/README.md @@ -17,21 +17,19 @@ pnpm add @agentcommercekit/keys ```ts import { generateKeypair, - keypairToBase58, keypairToJwk, - formatPublicKey + encodePublicKeyFromKeypair } from "@agentcommercekit/keys" // Generate and format keypairs const keypair = await generateKeypair("secp256k1") -const base58Keypair = keypairToBase58(keypair) const jwkKeypair = keypairToJwk(keypair) // Format public keys -const hexPublicKey = formatPublicKey(keypair, "hex") -const jwkPublicKey = formatPublicKey(keypair, "jwk") -const multibasePublicKey = formatPublicKey(keypair, "multibase") -const base58PublicKey = formatPublicKey(keypair, "base58") +const hexPublicKey = encodePublicKeyFromKeypair("hex", keypair) +const jwkPublicKey = encodePublicKeyFromKeypair("jwk", keypair) +const multibasePublicKey = encodePublicKeyFromKeypair("multibase", keypair) +const base58PublicKey = encodePublicKeyFromKeypair("base58", keypair) ``` ## API @@ -39,18 +37,12 @@ const base58PublicKey = formatPublicKey(keypair, "base58") ### Keypair Operations - `generateKeypair(algorithm: KeypairAlgorithm, privateKeyBytes?: Uint8Array): Promise` -- `keypairToBase58(keypair: Keypair): KeypairBase58` -- `keypairFromBase58(base58: KeypairBase58): Keypair` - `keypairToJwk(keypair: Keypair): PrivateKeyJwk` - `jwkToKeypair(jwk: PrivateKeyJwk): Keypair` ### Public Key Formatting -- `formatPublicKey(keypair: Keypair, format: T): PublicKeyTypeMap[T]` -- `formatPublicKeyHex(keypair: Keypair): string` -- `formatPublicKeyJwk(keypair: Keypair): PublicKeyJwk` -- `formatPublicKeyMultibase(keypair: Keypair): string` -- `formatPublicKeyBase58(keypair: Keypair): string` +- `encodePublicKeyFromKeypair(encoding: T, keypair: Keypair): PublicKeyTypeMap[T]` - `getCompressedPublicKey(keypair: Keypair): Uint8Array` ### Additional Exports diff --git a/packages/keys/src/encoding/base64.test.ts b/packages/keys/src/encoding/base64.test.ts index 6bbf9f5..3eb75e9 100644 --- a/packages/keys/src/encoding/base64.test.ts +++ b/packages/keys/src/encoding/base64.test.ts @@ -1,36 +1,36 @@ import { describe, expect, test } from "vitest" -import { base64ToBytes, bytesToBase64, isBase64 } from "./base64" +import { base64urlToBytes, bytesToBase64url, isBase64url } from "./base64" describe("base64 encoding and decoding", () => { test("converts bytes to base64 string", () => { const bytes = new Uint8Array([1, 2, 3, 4]) - const base64 = bytesToBase64(bytes) + const base64 = bytesToBase64url(bytes) expect(base64).toBe("AQIDBA") }) test("converts base64 string to bytes", () => { const base64 = "AQIDBA" - const bytes = base64ToBytes(base64) + const bytes = base64urlToBytes(base64) expect(bytes).toEqual(new Uint8Array([1, 2, 3, 4])) }) test("roundtrip base64 encoding", () => { const original = new Uint8Array([1, 2, 3, 4]) - const base64 = bytesToBase64(original) - const bytes = base64ToBytes(base64) + const base64 = bytesToBase64url(original) + const bytes = base64urlToBytes(base64) expect(bytes).toEqual(original) }) }) describe("isBase64", () => { test("returns true for valid base64 strings", () => { - expect(isBase64("AQIDBA")).toBe(true) - expect(isBase64("SGVsbG8sIFdvcmxkIQ")).toBe(true) // "Hello, World!" + expect(isBase64url("AQIDBA")).toBe(true) + expect(isBase64url("SGVsbG8sIFdvcmxkIQ")).toBe(true) // "Hello, World!" }) test("returns false for invalid base64 strings", () => { - expect(isBase64("not base64")).toBe(false) - expect(isBase64("AQIDBA!")).toBe(false) - expect(isBase64(123)).toBe(false) + expect(isBase64url("not base64")).toBe(false) + expect(isBase64url("AQIDBA!")).toBe(false) + expect(isBase64url(123)).toBe(false) }) }) diff --git a/packages/keys/src/encoding/base64.ts b/packages/keys/src/encoding/base64.ts index fe986da..1e4c6e6 100644 --- a/packages/keys/src/encoding/base64.ts +++ b/packages/keys/src/encoding/base64.ts @@ -4,21 +4,21 @@ import { toString } from "uint8arrays/to-string" /** * Convert bytes to a base64url string */ -export function bytesToBase64(bytes: Uint8Array): string { +export function bytesToBase64url(bytes: Uint8Array): string { return toString(bytes, "base64url") } /** * Convert a base64url string to bytes */ -export function base64ToBytes(base64: string): Uint8Array { +export function base64urlToBytes(base64: string): Uint8Array { return fromString(base64, "base64url") } /** * Check if a string is valid base64url encoded */ -export function isBase64(str: unknown): str is string { +export function isBase64url(str: unknown): str is string { if (typeof str !== "string") { return false } diff --git a/packages/keys/src/encoding/jwk.test.ts b/packages/keys/src/encoding/jwk.test.ts index 0db8432..db955da 100644 --- a/packages/keys/src/encoding/jwk.test.ts +++ b/packages/keys/src/encoding/jwk.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "vitest" -import { bytesToBase64, isBase64 } from "./base64" +import { bytesToBase64url, isBase64url } from "./base64" import { bytesToJwk, isPrivateKeyJwk, isPublicKeyJwk, jwkToBytes } from "./jwk" import type { PublicKeyJwkEd25519, PublicKeyJwkSecp256k1 } from "./jwk" @@ -12,8 +12,8 @@ describe("JWK encoding", () => { secp256k1Bytes.fill(2, 33) // y-coordinate (32 bytes) // Generate the actual base64url encoded strings from our test data - const base64String = bytesToBase64(Ed25519Bytes) // base64url of 32 bytes of 1s - const base64String2 = bytesToBase64(secp256k1Bytes.slice(33)) // base64url of 32 bytes of 2s + const base64String = bytesToBase64url(Ed25519Bytes) // base64url of 32 bytes of 1s + const base64String2 = bytesToBase64url(secp256k1Bytes.slice(33)) // base64url of 32 bytes of 2s describe("bytesToJwk", () => { test("converts Ed25519 public key to JWK", () => { @@ -23,7 +23,7 @@ describe("JWK encoding", () => { crv: "Ed25519", x: expect.any(String) as unknown }) - expect(isBase64(jwk.x)).toBe(true) + expect(isBase64url(jwk.x)).toBe(true) }) test("converts secp256k1 public key to JWK", () => { @@ -37,8 +37,8 @@ describe("JWK encoding", () => { x: expect.any(String) as unknown, y: expect.any(String) as unknown }) - expect(isBase64(jwk.x)).toBe(true) - expect(isBase64(jwk.y)).toBe(true) + expect(isBase64url(jwk.x)).toBe(true) + expect(isBase64url(jwk.y)).toBe(true) }) }) diff --git a/packages/keys/src/encoding/jwk.ts b/packages/keys/src/encoding/jwk.ts index 1bfc1e7..f42ceca 100644 --- a/packages/keys/src/encoding/jwk.ts +++ b/packages/keys/src/encoding/jwk.ts @@ -1,4 +1,4 @@ -import { base64ToBytes, bytesToBase64 } from "./base64" +import { base64urlToBytes, bytesToBase64url } from "./base64" import type { KeypairAlgorithm } from "../types" /** @@ -26,43 +26,61 @@ export type PrivateKeyJwk = PublicKeyJwk & { d: string // base64url encoded private key } -/** - * Check if an object is a valid public key JWK - */ -export function isPublicKeyJwk(jwk: unknown): jwk is PublicKeyJwk { +export function isPublicKeyJwkSecp256k1( + jwk: unknown +): jwk is PublicKeyJwkSecp256k1 { if (typeof jwk !== "object" || jwk === null) { return false } const obj = jwk as Record - if (obj.kty !== "EC" && obj.kty !== "OKP") { + if (obj.kty !== "EC" || obj.crv !== "secp256k1") { return false } - if (obj.crv !== "secp256k1" && obj.crv !== "Ed25519") { + if (typeof obj.x !== "string" || obj.x.length === 0) { return false } - if (typeof obj.x !== "string" || obj.x.length === 0) { + if (typeof obj.y !== "string" || obj.y.length === 0) { + return false + } + + return true +} + +export function isPublicKeyJwkEd25519( + jwk: unknown +): jwk is PublicKeyJwkEd25519 { + if (typeof jwk !== "object" || jwk === null) { return false } - // For secp256k1, y coordinate is required - if (obj.crv === "secp256k1") { - if (typeof obj.y !== "string" || obj.y.length === 0) { - return false - } + const obj = jwk as Record + + if (obj.kty !== "OKP" || obj.crv !== "Ed25519") { + return false + } + + if (typeof obj.x !== "string" || obj.x.length === 0) { + return false } - // For Ed25519, y coordinate should not be present - if (obj.crv === "Ed25519" && "y" in obj) { + if ("y" in obj) { return false } return true } +/** + * Check if an object is a valid public key JWK + */ +export function isPublicKeyJwk(jwk: unknown): jwk is PublicKeyJwk { + return isPublicKeyJwkSecp256k1(jwk) || isPublicKeyJwkEd25519(jwk) +} + /** * Check if an object is a valid private key JWK */ @@ -86,7 +104,7 @@ export function bytesToJwk( return { kty: "OKP", crv: "Ed25519", - x: bytesToBase64(bytes) + x: bytesToBase64url(bytes) } as const } @@ -100,8 +118,8 @@ export function bytesToJwk( return { kty: "EC", crv: "secp256k1", - x: bytesToBase64(xBytes), - y: bytesToBase64(yBytes) + x: bytesToBase64url(xBytes), + y: bytesToBase64url(yBytes) } as const } @@ -109,14 +127,14 @@ export function bytesToJwk( * Convert a JWK to public key bytes */ export function jwkToBytes(jwk: PublicKeyJwk): Uint8Array { - const xBytes = base64ToBytes(jwk.x) + const xBytes = base64urlToBytes(jwk.x) // For secp256k1, we need to reconstruct the full public key if (jwk.crv === "secp256k1") { const fullKey = new Uint8Array(65) fullKey[0] = 0x04 // Add the prefix byte fullKey.set(xBytes, 1) // Add the x-coordinate - fullKey.set(base64ToBytes(jwk.y), 33) // Add the y-coordinate + fullKey.set(base64urlToBytes(jwk.y), 33) // Add the y-coordinate return fullKey } diff --git a/packages/keys/src/index.ts b/packages/keys/src/index.ts index 0be37af..87ec31a 100644 --- a/packages/keys/src/index.ts +++ b/packages/keys/src/index.ts @@ -1,3 +1,3 @@ -export * from "./types" export * from "./keypair" export * from "./public-key" +export * from "./types" diff --git a/packages/keys/src/keypair.ts b/packages/keys/src/keypair.ts index fbded2d..fc896d8 100644 --- a/packages/keys/src/keypair.ts +++ b/packages/keys/src/keypair.ts @@ -1,10 +1,9 @@ import { generateKeypair as ed25519 } from "./curves/ed25519" import { generateKeypair as secp256k1 } from "./curves/secp256k1" -import { base58ToBytes, bytesToBase58 } from "./encoding/base58" -import { base64ToBytes, bytesToBase64 } from "./encoding/base64" +import { base64urlToBytes, bytesToBase64url } from "./encoding/base64" import { bytesToJwk, jwkToBytes } from "./encoding/jwk" import type { PrivateKeyJwk } from "./encoding/jwk" -import type { Keypair, KeypairAlgorithm, KeypairBase58 } from "./types" +import type { Keypair, KeypairAlgorithm } from "./types" /** * Generate a Keypair for a given algorithm @@ -24,34 +23,6 @@ export async function generateKeypair( return ed25519(privateKeyBytes) } -/** - * Convert a Keypair to a base58-encoded string format - * - * @param keypair - The Keypair to convert - * @returns A base58-encoded string representation of the Keypair - */ -export function keypairToBase58(keypair: Keypair): KeypairBase58 { - return { - publicKey: bytesToBase58(keypair.publicKey), - privateKey: bytesToBase58(keypair.privateKey), - algorithm: keypair.algorithm - } -} - -/** - * Convert a base58-encoded string format back to a Keypair - * - * @param base58 - The base58-encoded Keypair - * @returns The reconstructed Keypair - */ -export function keypairFromBase58(base58: KeypairBase58): Keypair { - return { - publicKey: base58ToBytes(base58.publicKey), - privateKey: base58ToBytes(base58.privateKey), - algorithm: base58.algorithm - } -} - /** * Convert a Keypair to a JWK format * @@ -62,7 +33,7 @@ export function keypairToJwk(keypair: Keypair): PrivateKeyJwk { const publicKeyJwk = bytesToJwk(keypair.publicKey, keypair.algorithm) return { ...publicKeyJwk, - d: bytesToBase64(keypair.privateKey) + d: bytesToBase64url(keypair.privateKey) } } @@ -75,7 +46,7 @@ export function keypairToJwk(keypair: Keypair): PrivateKeyJwk { export function jwkToKeypair(jwk: PrivateKeyJwk): Keypair { return { publicKey: jwkToBytes(jwk), - privateKey: base64ToBytes(jwk.d), + privateKey: base64urlToBytes(jwk.d), algorithm: jwk.crv } } diff --git a/packages/keys/src/public-key.test.ts b/packages/keys/src/public-key.test.ts index 12c17f5..16f5f83 100644 --- a/packages/keys/src/public-key.test.ts +++ b/packages/keys/src/public-key.test.ts @@ -1,68 +1,64 @@ import { describe, expect, test } from "vitest" import { isBase58 } from "./encoding/base58" -import { base64ToBytes, isBase64 } from "./encoding/base64" +import { base64urlToBytes, isBase64url } from "./encoding/base64" import { isHexString } from "./encoding/hex" +import { isPublicKeyJwkEd25519, isPublicKeyJwkSecp256k1 } from "./encoding/jwk" import { isMultibase } from "./encoding/multibase" import { generateKeypair } from "./keypair" -import { - formatPublicKey, - formatPublicKeyBase58, - formatPublicKeyBase64, - formatPublicKeyHex, - formatPublicKeyJwk, - formatPublicKeyMultibase, - publicKeyFormats -} from "./public-key" -import type { PublicKeyJwkEd25519, PublicKeyJwkSecp256k1 } from "./encoding/jwk" +import { encodePublicKeyFromKeypair, publicKeyEncodings } from "./public-key" const keypairAlgorithms = ["secp256k1", "Ed25519"] as const describe("public key encoding", () => { describe.each(keypairAlgorithms)("algorithm: %s", (algorithm) => { - describe.each(publicKeyFormats)("format: %s", (format) => { - test("formats public key correctly", async () => { + describe.each(publicKeyEncodings)("format: %s", (format) => { + test("encodes public key correctly", async () => { const keypair = await generateKeypair(algorithm) - const result = formatPublicKey(keypair, format) + const publicKey = encodePublicKeyFromKeypair(format, keypair) + const publicKeyValue = publicKey.value switch (format) { case "hex": - expect(isHexString(result)).toBe(true) + expect(isHexString(publicKeyValue)).toBe(true) break case "jwk": if (algorithm === "secp256k1") { - const jwk = result as PublicKeyJwkSecp256k1 - expect(jwk).toEqual({ + if (!isPublicKeyJwkSecp256k1(publicKeyValue)) { + throw new Error("Invalid JWK") + } + + expect(publicKeyValue).toEqual({ kty: "EC", crv: "secp256k1", x: expect.any(String) as unknown, y: expect.any(String) as unknown }) - expect(isBase64(jwk.x)).toBe(true) - expect(isBase64(jwk.y)).toBe(true) - const xBytes = base64ToBytes(jwk.x) + expect(isBase64url(publicKeyValue.x)).toBe(true) + expect(isBase64url(publicKeyValue.y)).toBe(true) + const xBytes = base64urlToBytes(publicKeyValue.x) expect(xBytes.length).toBe(32) - const yBytes = base64ToBytes(jwk.y) + const yBytes = base64urlToBytes(publicKeyValue.y) expect(yBytes.length).toBe(32) } else { - const jwk = result as PublicKeyJwkEd25519 - expect(jwk).toEqual({ + if (!isPublicKeyJwkEd25519(publicKeyValue)) { + throw new Error("Invalid JWK") + } + + expect(publicKeyValue).toEqual({ kty: "OKP", crv: "Ed25519", x: expect.any(String) as unknown }) - expect(isBase64(jwk.x)).toBe(true) - const xBytes = base64ToBytes(jwk.x) + expect(isBase64url(publicKeyValue.x)).toBe(true) + const xBytes = base64urlToBytes(publicKeyValue.x) expect(xBytes.length).toBe(32) } break case "multibase": - expect(isMultibase(result)).toBe(true) + expect(isMultibase(publicKeyValue)).toBe(true) break case "base58": - expect(isBase58(result)).toBe(true) - break - case "base64": - expect(isBase64(result)).toBe(true) + expect(isBase58(publicKeyValue)).toBe(true) break } }) @@ -73,22 +69,22 @@ describe("public key encoding", () => { describe.each(keypairAlgorithms)("algorithm: %s", (algorithm) => { test("formats to multibase", async () => { const keypair = await generateKeypair(algorithm) - const multibase = formatPublicKeyMultibase(keypair) - expect(isMultibase(multibase)).toBe(true) + const multibase = encodePublicKeyFromKeypair("multibase", keypair) + expect(isMultibase(multibase.value)).toBe(true) }) test("formats to JWK", async () => { const keypair = await generateKeypair(algorithm) - const jwk = formatPublicKeyJwk(keypair) + const jwk = encodePublicKeyFromKeypair("jwk", keypair) if (algorithm === "secp256k1") { - expect(jwk).toEqual({ + expect(jwk.value).toEqual({ kty: "EC", crv: "secp256k1", x: expect.any(String) as unknown, y: expect.any(String) as unknown }) } else { - expect(jwk).toEqual({ + expect(jwk.value).toEqual({ kty: "OKP", crv: "Ed25519", x: expect.any(String) as unknown @@ -98,20 +94,14 @@ describe("public key encoding", () => { test("formats to hex", async () => { const keypair = await generateKeypair(algorithm) - const hex = formatPublicKeyHex(keypair) - expect(isHexString(hex)).toBe(true) + const hex = encodePublicKeyFromKeypair("hex", keypair) + expect(isHexString(hex.value)).toBe(true) }) test("formats to base58", async () => { const keypair = await generateKeypair(algorithm) - const base58 = formatPublicKeyBase58(keypair) - expect(isBase58(base58)).toBe(true) - }) - - test("formats to base64", async () => { - const keypair = await generateKeypair(algorithm) - const base64 = formatPublicKeyBase64(keypair) - expect(isBase64(base64)).toBe(true) + const base58 = encodePublicKeyFromKeypair("base58", keypair) + expect(isBase58(base58.value)).toBe(true) }) }) }) diff --git a/packages/keys/src/public-key.ts b/packages/keys/src/public-key.ts index 483ed28..e9a0856 100644 --- a/packages/keys/src/public-key.ts +++ b/packages/keys/src/public-key.ts @@ -1,31 +1,39 @@ import { compressPublicKey } from "./curves/secp256k1" import { bytesToBase58 } from "./encoding/base58" -import { bytesToBase64 } from "./encoding/base64" import { bytesToHexString } from "./encoding/hex" import { bytesToJwk } from "./encoding/jwk" import { bytesToMultibase } from "./encoding/multibase" import type { PublicKeyJwk } from "./encoding/jwk" -import type { Keypair } from "./types" +import type { Keypair, KeypairAlgorithm } from "./types" /** * Public key format types */ -export const publicKeyFormats = [ - "hex", - "jwk", - "multibase", - "base58", - "base64" -] as const -export type PublicKeyFormat = (typeof publicKeyFormats)[number] +export const publicKeyEncodings = ["hex", "jwk", "multibase", "base58"] as const +export type PublicKeyEncoding = (typeof publicKeyEncodings)[number] export type PublicKeyTypeMap = { hex: string jwk: PublicKeyJwk multibase: string base58: string - base64: string } +/** + * A type that represents a PublicKey with its encoding format and algorithm + */ +export type PublicKeyWithEncoding = { + [K in PublicKeyEncoding]: { + encoding: K + algorithm: KeypairAlgorithm + value: PublicKeyTypeMap[K] + } +}[PublicKeyEncoding] + +/** + * Get the compressed public key for a given keypair + * @param keypair - The keypair to get the compressed public key for + * @returns The compressed public key + */ export function getCompressedPublicKey(keypair: Keypair): Uint8Array { if (keypair.algorithm === "secp256k1") { return compressPublicKey(keypair) @@ -36,65 +44,92 @@ export function getCompressedPublicKey(keypair: Keypair): Uint8Array { /** * Convert a public key to a multibase string (used for DID:key) - * @param keypair - The Keypair containing the public key - * @returns A multibase string representation of the public key */ -export function formatPublicKeyMultibase(keypair: Keypair): string { - return bytesToMultibase(keypair.publicKey) +function encodePublicKeyMultibase( + publicKey: Uint8Array, + algorithm: KeypairAlgorithm +): PublicKeyWithEncoding & { encoding: "multibase" } { + return { + encoding: "multibase", + algorithm, + value: bytesToMultibase(publicKey) + } } /** * Convert a public key to a JWK format - * @param keypair - The Keypair containing the public key - * @returns A JSON Web Key representation of the public key */ -export function formatPublicKeyJwk(keypair: Keypair): PublicKeyJwk { - return bytesToJwk(keypair.publicKey, keypair.algorithm) +function encodePublicKeyJwk( + publicKey: Uint8Array, + algorithm: KeypairAlgorithm +): PublicKeyWithEncoding & { encoding: "jwk" } { + return { + encoding: "jwk", + algorithm, + value: bytesToJwk(publicKey, algorithm) + } } /** * Convert a public key to a hex string - * @param keypair - The Keypair containing the public key - * @returns A hex string representation of the public key */ -export function formatPublicKeyHex(keypair: Keypair): string { - return bytesToHexString(keypair.publicKey) +function encodePublicKeyHex( + publicKey: Uint8Array, + algorithm: KeypairAlgorithm +): PublicKeyWithEncoding & { encoding: "hex" } { + return { + encoding: "hex", + algorithm, + value: bytesToHexString(publicKey) + } } /** * Convert a public key to a base58 string - * @param keypair - The Keypair containing the public key - * @returns A base58 string representation of the public key */ -export function formatPublicKeyBase58(keypair: Keypair): string { - return bytesToBase58(keypair.publicKey) +function encodePublicKeyBase58( + publicKey: Uint8Array, + algorithm: KeypairAlgorithm +): PublicKeyWithEncoding & { encoding: "base58" } { + return { + encoding: "base58", + algorithm, + value: bytesToBase58(publicKey) + } } /** - * Convert a public key to a base64 string - * @param keypair - The Keypair containing the public key - * @returns A base64 string representation of the public key + * A map of public key encoders */ -export function formatPublicKeyBase64(keypair: Keypair): string { - return bytesToBase64(keypair.publicKey) -} - -export const publicKeyFormatters = { - hex: (keypair: Keypair) => formatPublicKeyHex(keypair), - jwk: (keypair: Keypair) => formatPublicKeyJwk(keypair), - multibase: (keypair: Keypair) => formatPublicKeyMultibase(keypair), - base58: (keypair: Keypair) => formatPublicKeyBase58(keypair), - base64: (keypair: Keypair) => formatPublicKeyBase64(keypair) +const publicKeyEncoders: { + [K in PublicKeyEncoding]: ( + publicKey: Uint8Array, + algorithm: KeypairAlgorithm + ) => PublicKeyWithEncoding & { encoding: K } +} = { + hex: encodePublicKeyHex, + jwk: encodePublicKeyJwk, + multibase: encodePublicKeyMultibase, + base58: encodePublicKeyBase58 } as const /** - * Convert a public key to the specified format with correct type inference + * Encode a raw public key to the specified format + */ +export function encodePublicKey( + encoding: T, + publicKey: Uint8Array, + algorithm: KeypairAlgorithm +): PublicKeyWithEncoding & { encoding: T } { + return publicKeyEncoders[encoding](publicKey, algorithm) +} + +/** + * Encode a public key from a keypair to the specified format */ -export function formatPublicKey( - keypair: Keypair, - format: T -): ReturnType<(typeof publicKeyFormatters)[T]> { - return publicKeyFormatters[format](keypair) as ReturnType< - (typeof publicKeyFormatters)[T] - > +export function encodePublicKeyFromKeypair( + encoding: T, + keypair: Keypair +): PublicKeyWithEncoding & { encoding: T } { + return encodePublicKey(encoding, keypair.publicKey, keypair.algorithm) } From cd347e7237e7bd28e1b149a8505f8dd4e98f8dc4 Mon Sep 17 00:00:00 2001 From: Matt Venables Date: Thu, 29 May 2025 06:05:41 -0400 Subject: [PATCH 5/5] Add changeset --- .changeset/smart-buckets-end.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .changeset/smart-buckets-end.md diff --git a/.changeset/smart-buckets-end.md b/.changeset/smart-buckets-end.md new file mode 100644 index 0000000..57d3f5d --- /dev/null +++ b/.changeset/smart-buckets-end.md @@ -0,0 +1,9 @@ +--- +"agentcommercekit": minor +"@agentcommercekit/keys": minor +"@agentcommercekit/did": minor +--- + +- Upgrade legacy public key formats to use multibase in DID Documents +- Update base64 methods to be explicit that they use `base64url` encoding +- Simplify interface for public key encoding methods