Guides

Sign Messages

Sign and verify messages across multiple wallet types with CCC's unified signing interface.

Edit on GitHub

Sign an arbitrary message with the user's wallet and verify the signature later — useful for off-chain authentication ("Sign-In with Wallet"), proving address ownership, or creating verifiable attestations. The API is identical regardless of whether the user connected a CKB, EVM, BTC, JoyID, Nostr, or Doge wallet.

What you'll get

After this guide you'll be able to:

  • Sign a message with any connected wallet (one unified call)
  • Verify a signature statically — no connected wallet required
  • Understand the Signature object and how signType enables cross-chain verification

All examples assume you already have a connected signer. If you don't, see Connect Wallets first.

The Signature type

signer.signMessage returns a Signature object defined in packages/core/src/signer/signer/index.ts:

class Signature {
  signature: string;  // the raw signature hex
  identity: string;   // signer identity (usually their address)
  signType: SignerSignType;
}

The signType field tells verifiers which cryptographic scheme was used, enabling scheme-specific verification without the caller needing to know the wallet type ahead of time.

SignerSignType enum

ValueWallet / scheme
SignerSignType.CkbSecp256k1CKB native secp256k1 wallets
SignerSignType.EvmPersonalEVM wallets (MetaMask, OKX EVM, etc.)
SignerSignType.BtcEcdsaBitcoin wallets (UniSat, OKX BTC, etc.)
SignerSignType.JoyIdJoyID passkey wallet
SignerSignType.NostrEventNostr clients
SignerSignType.DogeEcdsaDogecoin wallets
SignerSignType.UnknownUnknown / unsupported type

Sign a message

One call — works with every wallet type CCC supports:

import { ccc } from "@ckb-ccc/ccc";

const message = "Hello world";
const signature = await signer.signMessage(message);

console.log(signature.signature);  // "0x..." — raw signature bytes as hex
console.log(signature.identity);   // signer's address or public key
console.log(signature.signType);   // e.g. "CkbSecp256k1", "EvmPersonal", "BtcEcdsa"

signMessage accepts either a plain string or a BytesLike (Uint8Array / hex string), so you can sign raw bytes when needed.

Verify a signature

Verification is a static method — you do not need a connected wallet. CCC dispatches to the correct cryptographic scheme automatically based on signature.signType:

const isValid = await ccc.Signer.verifyMessage(message, signature);
// true — message + signature match

const isFail = await ccc.Signer.verifyMessage("Wrong message", signature);
// false — message doesn't match

This means your backend can verify signatures from any CCC-supported wallet without knowing which wallet the user used.

Full sign + verify example

End-to-end example from packages/examples/src/sign.ts. It handles the playground's default SignerCkbPublicKey (which cannot sign messages) by substituting a private-key signer for demonstration:

import { ccc } from "@ckb-ccc/ccc";
import { client, signer as playgroundSigner } from "@ckb-ccc/playground";

// The default playground signer cannot sign messages.
// In a real app the connected wallet signer is always usable.
const signer: ccc.Signer =
  playgroundSigner instanceof ccc.SignerCkbPublicKey
    ? new ccc.SignerCkbPrivateKey(client, "01".repeat(32))
    : playgroundSigner;

const message = "Hello world";

// Sign
const signature = await signer.signMessage(message);
console.log(signature);

// Verify — passes
console.log(
  `Verification should pass: ${await ccc.Signer.verifyMessage(message, signature)}`,
);

// Verify — fails with wrong message
console.log(
  `Verification should fail: ${await ccc.Signer.verifyMessage("Wrong message", signature)}`,
);

Instance-level verification (identity check)

Use this when: you not only want to verify the signature is valid, but also confirm it was produced by the currently connected wallet. The instance method additionally checks that signature.identity matches the signer:

// Returns false if the signature was produced by a different signer
const ok = await signer.verifyMessage(message, signature);

Static vs. instance verification: ccc.Signer.verifyMessage() (static) verifies any Signature regardless of who produced it. signer.verifyMessage() (instance) additionally enforces that signature.identity matches the current signer's identity.

Sign raw bytes

Use this when: you need to sign arbitrary binary data (e.g. a hash, a serialized protobuf, or a binary payload):

const bytes = new Uint8Array([0xde, 0xad, 0xbe, 0xef]);
const signature = await signer.signMessage(bytes);

// Hex strings also work:
const sigFromHex = await signer.signMessage("0xdeadbeef");

Troubleshooting

signer.signMessage throws "not implemented" or "unsupported" Some signer types (e.g. SignerCkbPublicKey) represent a read-only public key with no signing capability. This happens in the CCC playground default signer. In production apps with real wallet connections, signMessage always works.

Verification returns false even though I signed the same message Make sure you're passing the exact same message value (including encoding). If you signed a string, verify with the same string — not a Uint8Array version of it, and vice versa.

I need to verify on a backend that doesn't have CCC The Signature object is JSON-serializable. Send it to your backend, install @ckb-ccc/shell, and call ccc.Signer.verifyMessage(message, signature) there.

Next steps

On this page