Sign Messages
Sign and verify messages across multiple wallet types with CCC's unified signing interface.
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
Signatureobject and howsignTypeenables 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
| Value | Wallet / scheme |
|---|---|
SignerSignType.CkbSecp256k1 | CKB native secp256k1 wallets |
SignerSignType.EvmPersonal | EVM wallets (MetaMask, OKX EVM, etc.) |
SignerSignType.BtcEcdsa | Bitcoin wallets (UniSat, OKX BTC, etc.) |
SignerSignType.JoyId | JoyID passkey wallet |
SignerSignType.NostrEvent | Nostr clients |
SignerSignType.DogeEcdsa | Dogecoin wallets |
SignerSignType.Unknown | Unknown / 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 matchThis 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
- Compose transactions — send CKB or tokens on-chain.
- Node.js backend — verify signatures and send transactions from the server.