From 3c50363a8dddb9553bd06582dac6ecb4c01d29ca Mon Sep 17 00:00:00 2001 From: Hanssen0 Date: Wed, 17 Sep 2025 20:58:57 +0800 Subject: [PATCH 1/2] feat(joy-id): address info in identity --- .changeset/late-vans-juggle.md | 7 +++ packages/core/src/signer/ckb/verifyJoyId.ts | 50 ++++++++++++++++----- packages/joy-id/src/ckb/index.ts | 1 + 3 files changed, 47 insertions(+), 11 deletions(-) create mode 100644 .changeset/late-vans-juggle.md diff --git a/.changeset/late-vans-juggle.md b/.changeset/late-vans-juggle.md new file mode 100644 index 00000000..4f88298b --- /dev/null +++ b/.changeset/late-vans-juggle.md @@ -0,0 +1,7 @@ +--- +"@ckb-ccc/core": major +"@ckb-ccc/joy-id": minor +--- + +feat(joy-id): address info in identity + \ No newline at end of file diff --git a/packages/core/src/signer/ckb/verifyJoyId.ts b/packages/core/src/signer/ckb/verifyJoyId.ts index eb908994..44eece34 100644 --- a/packages/core/src/signer/ckb/verifyJoyId.ts +++ b/packages/core/src/signer/ckb/verifyJoyId.ts @@ -1,27 +1,55 @@ -import { verifySignature } from "@joyid/ckb"; +import { + CredentialKeyType, + SigningAlg, + verifyCredential, + verifySignature, +} from "@joyid/ckb"; import { BytesLike } from "../../bytes/index.js"; import { hexFrom } from "../../hex/index.js"; /** * @public */ -export function verifyMessageJoyId( +export async function verifyMessageJoyId( message: string | BytesLike, signature: string, identity: string, ): Promise { const challenge = typeof message === "string" ? message : hexFrom(message).slice(2); - const { publicKey, keyType } = JSON.parse(identity) as { + const { address, publicKey, keyType } = JSON.parse(identity) as { + address: string; publicKey: string; - keyType: string; + keyType: CredentialKeyType; }; + const signatureObj = JSON.parse(signature) as { + alg: SigningAlg; + signature: string; + message: string; + }; + + if ( + !(await verifySignature({ + challenge, + pubkey: publicKey, + keyType, + ...signatureObj, + })) + ) { + return false; + } - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - return verifySignature({ - challenge, - pubkey: publicKey, - keyType, - ...JSON.parse(signature), - }); + // I sincerely hope one day we can get rid of the centralized registry + const registry = address.startsWith("ckb") + ? "https://api.joy.id/api/v1/" + : "https://api.testnet.joyid.dev/api/v1/"; + return verifyCredential( + { + pubkey: publicKey, + address, + keyType, + alg: signatureObj.alg, + }, + registry, + ); } diff --git a/packages/joy-id/src/ckb/index.ts b/packages/joy-id/src/ckb/index.ts index 2fa0851e..289e21d9 100644 --- a/packages/joy-id/src/ckb/index.ts +++ b/packages/joy-id/src/ckb/index.ts @@ -148,6 +148,7 @@ export class CkbSigner extends ccc.Signer { async getIdentity(): Promise { const connection = await this.assertConnection(); return JSON.stringify({ + address: connection.address, keyType: connection.keyType, publicKey: connection.publicKey.slice(2), }); From 32b18a7516b9b25f57a312dea56a33bac96f7018 Mon Sep 17 00:00:00 2001 From: Hanssen0 Date: Thu, 18 Sep 2025 19:28:47 +0800 Subject: [PATCH 2/2] feat(core): `Signer.fromSignature` --- .changeset/fifty-planes-fetch.md | 6 ++ packages/core/src/signer/index.ts | 17 +++++ packages/core/src/signer/signer/index.ts | 11 ++++ .../core/src/signer/signerFromSignature.ts | 65 +++++++++++++++++++ 4 files changed, 99 insertions(+) create mode 100644 .changeset/fifty-planes-fetch.md create mode 100644 packages/core/src/signer/signerFromSignature.ts diff --git a/.changeset/fifty-planes-fetch.md b/.changeset/fifty-planes-fetch.md new file mode 100644 index 00000000..bcd19d3f --- /dev/null +++ b/.changeset/fifty-planes-fetch.md @@ -0,0 +1,6 @@ +--- +"@ckb-ccc/core": minor +--- + +feat(core): `Signer.fromSignature` + \ No newline at end of file diff --git a/packages/core/src/signer/index.ts b/packages/core/src/signer/index.ts index 350b487e..052248f1 100644 --- a/packages/core/src/signer/index.ts +++ b/packages/core/src/signer/index.ts @@ -5,3 +5,20 @@ export * from "./dummy/index.js"; export * from "./evm/index.js"; export * from "./nostr/index.js"; export * from "./signer/index.js"; +export * from "./signerFromSignature.js"; + +import { BytesLike } from "../bytes/index.js"; +import { Client } from "../client/index.js"; +import { Signer as BaseSigner, Signature } from "./signer/index.js"; +import { signerFromSignature } from "./signerFromSignature.js"; + +export abstract class Signer extends BaseSigner { + static fromSignature( + client: Client, + signature: Signature, + message?: string | BytesLike | null, + ...addresses: (string | string[])[] + ): Promise { + return signerFromSignature(client, signature, message, ...addresses); + } +} diff --git a/packages/core/src/signer/signer/index.ts b/packages/core/src/signer/signer/index.ts index 1522b335..a25733f8 100644 --- a/packages/core/src/signer/signer/index.ts +++ b/packages/core/src/signer/signer/index.ts @@ -157,6 +157,17 @@ export abstract class Signer { } } + static async fromSignature( + _client: Client, + _signature: Signature, + _message?: string | BytesLike | null, + ..._addresses: (string | string[])[] + ): Promise { + throw Error( + "Signer.fromSignature should be override to avoid circular references", + ); + } + /** * Connects to the signer. * diff --git a/packages/core/src/signer/signerFromSignature.ts b/packages/core/src/signer/signerFromSignature.ts new file mode 100644 index 00000000..7f296f82 --- /dev/null +++ b/packages/core/src/signer/signerFromSignature.ts @@ -0,0 +1,65 @@ +import { Address } from "../address/index.js"; +import { BytesLike } from "../bytes/index.js"; +import { Client } from "../client/index.js"; +import { SignerBtcPublicKeyReadonly } from "./btc/index.js"; +import { SignerCkbPublicKey, SignerCkbScriptReadonly } from "./ckb/index.js"; +import { SignerDogeAddressReadonly } from "./doge/index.js"; +import { SignerEvmAddressReadonly } from "./evm/index.js"; +import { SignerNostrPublicKeyReadonly } from "./nostr/index.js"; +import { Signature, Signer, SignerSignType } from "./signer/index.js"; + +/** + * Creates a signer from a signature. + * + * @param client - The client instance. + * @param signature - The signature to create the signer from. + * @param message - The message that was signed. + * @param addresses - The addresses to check against the signer. + * @returns The signer if the signature is valid and the addresses match, otherwise undefined. + * @throws Error if the signature sign type is unknown. + */ +export async function signerFromSignature( + client: Client, + signature: Signature, + message?: string | BytesLike | null, + ...addresses: (string | string[])[] +): Promise { + if ( + message != undefined && + !(await Signer.verifyMessage(message, signature)) + ) { + return; + } + + const signer = await (async () => { + switch (signature.signType) { + case SignerSignType.EvmPersonal: + return new SignerEvmAddressReadonly(client, signature.identity); + case SignerSignType.BtcEcdsa: + return new SignerBtcPublicKeyReadonly(client, "", signature.identity); + case SignerSignType.JoyId: { + const { address } = JSON.parse(signature.identity) as { + address: string; + }; + return new SignerCkbScriptReadonly( + client, + (await Address.fromString(address, client)).script, + ); + } + case SignerSignType.NostrEvent: + return new SignerNostrPublicKeyReadonly(client, signature.identity); + case SignerSignType.CkbSecp256k1: + return new SignerCkbPublicKey(client, signature.identity); + case SignerSignType.DogeEcdsa: + return new SignerDogeAddressReadonly(client, signature.identity); + case SignerSignType.Unknown: + throw new Error("Unknown signer sign type"); + } + })(); + const signerAddresses = await signer.getAddresses(); + if (!addresses.flat().every((addr) => signerAddresses.includes(addr))) { + return; + } + + return signer; +}