Skip to main content
A Cloud KMS signer uses a signing key generated and stored inside a cloud key management service — AWS KMS, Azure Key Vault, or GCP Cloud HSM. The key is non-extractable by design: no employee at your company (or at the cloud provider) can ever retrieve the raw key material. Cloud KMS signers support advanced enterprise security controls natively, including IP allowlisting, cloud IAM-based access policies, rate limiting, circuit breakers, and detailed audit logs and alerting. For a conceptual overview, see Cloud KMS in the Wallet Signers guide. To learn how to register additional operational signers on an existing wallet, see Registering a signer.

How It Works

  1. You create a signing key inside Google Cloud KMS — the private key never leaves Google’s hardware.
  2. A Cloud Run function sits in front of KMS and signs transaction digests on demand.
  3. The Crossmint SDK calls your Cloud Run function whenever the wallet needs to sign something.
  4. The signature is converted from Google’s format to Ethereum’s format, and the Crossmint SDK uses it to complete the transaction.
That is it — your key stays in the HSM, and Crossmint never sees it.

What You Will Build

  • An asymmetric signing key in Google Cloud KMS (secp256k1)
  • A Cloud Run function that signs digests via KMS
  • A Crossmint server wallet using your KMS key as its signer

Prerequisites


Set Up Google Cloud KMS

1

Enable the Cloud KMS API

Navigate to Cloud Key Management in the Google Cloud Marketplace and click Enable.
2

Create a Key Ring

Go to Security > Key Management in the Google Cloud Console and click Create Key Ring.Create a key ring
  • Key ring name: crossmint-keys (or your preferred name)
  • Location: Select a region, e.g. us-west1
3

Create an Asymmetric Signing Key

Inside your key ring, click Create Key with the following settings:Create a key
SettingValue
Key namemy-crossmint-wallet-key
Protection levelHSM (Hardware Security Module)
Key materialHSM-generated key
PurposeAsymmetric Sign
AlgorithmElliptic Curve secp256k1 - SHA256 Digest
You must use the secp256k1 curve for EVM-compatible wallets. Other curves (e.g. P-256) will not produce valid Ethereum signatures.
4

Download the Public Key

Once the key is created, click on the key version, select Get Public Key, and download it in PEM format.Download the public keySave the contents — you will need this PEM string in later steps. It looks like:
-----BEGIN PUBLIC KEY-----
MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAE...
-----END PUBLIC KEY-----

Set Up IAM Permissions

1

Create a Service Account

Navigate to IAM & Admin > Service Accounts and create a new service account (e.g. kms-signer-sa).
2

Grant KMS Permissions

Assign the following role to the service account:
  • Role: Cloud KMS CryptoKey Signer/Verifier (roles/cloudkms.signerVerifier)
This follows the principle of least privilege — the service account can only sign and verify, not manage or destroy keys.

Deploy a Cloud Run Signing Function

This function receives a digest, signs it with your KMS key, and returns the DER-encoded signature.
1

Create a Cloud Run Function

Go to Cloud Run Functions and create a new function:
  • Name: kms-signer-function
  • Region: Same region as your key ring (e.g. us-west1)
  • Runtime: Node.js 18+
  • Authentication: Configure based on your security requirements
For production, restrict access to your Cloud Run function using IAM authentication or VPC Service Controls. Allowing unauthenticated access is only suitable for development.
2

Set Environment Variables

In the Cloud Run function settings, go to the Containers tab, expand Edit Container, then select the Variables & Secrets tab. Add the following environment variables. To get these values, use Copy resource name from the same key version actions menu where you downloaded the public key — it gives you a path like projects/PROJECT_ID/locations/LOCATION/keyRings/KEY_RING/cryptoKeys/KEY_NAME/cryptoKeyVersions/VERSION.
VariableExample Value
PROJECT_IDcrossmint-test-462521
LOCATIONus-west1
KEY_RINGcrossmint-keys
KEY_NAMEmy-crossmint-wallet-key
VERSION1
Add environment variables to container
3

Add the Function Code

Set the Function entry point to signDigest, then paste the following code:
index.js
const { KeyManagementServiceClient } = require("@google-cloud/kms");
const client = new KeyManagementServiceClient();

exports.signDigest = async (req, res) => {
    try {
        const { digest } = req.body;

        const keyName = client.cryptoKeyVersionPath(
            process.env.PROJECT_ID,
            process.env.LOCATION,
            process.env.KEY_RING,
            process.env.KEY_NAME,
            process.env.VERSION || "1"
        );

        const [signResponse] = await client.asymmetricSign({
            name: keyName,
            digest: { sha256: Buffer.from(digest, "hex") },
        });

        res.status(200).send({
            derSignature: Buffer.from(signResponse.signature).toString("hex"),
        });
    } catch (error) {
        res.status(500).send({ error: error.message });
    }
};
Add @google-cloud/kms to your function’s package.json dependencies:Add the google kms dependency
4

Deploy and Test

Deploy the function and note the URL — you will need it in the next step.Test with a sample request:
curl -X POST https://YOUR_FUNCTION_URL \
  -H "Content-Type: application/json" \
  -d '{"digest": "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"}'
You should receive a JSON response containing a derSignature field.

Create and Use the Crossmint Wallet

Now wire everything together — convert your KMS public key to an Ethereum address, create a Crossmint wallet, and sign transactions through your Cloud Run function.

Install Dependencies

npm i @crossmint/wallets-sdk viem @peculiar/asn1-schema @peculiar/asn1-x509 buffer

Convert the KMS Public Key to an Ethereum Address

Google Cloud KMS returns public keys in PEM/DER format. This helper extracts the raw public key bytes and converts them to an Ethereum address using viem.
kms-utils.ts
import { publicKeyToAddress } from "viem/accounts";
import { AsnParser } from "@peculiar/asn1-schema";
import { SubjectPublicKeyInfo } from "@peculiar/asn1-x509";
import { Buffer } from "buffer";

export const publicKeyPem = `-----BEGIN PUBLIC KEY-----
YOUR_PUBLIC_KEY_HERE
-----END PUBLIC KEY-----`;

export function kmsPemToEthAddress(pem: string): `0x${string}` {
  const b64 = pem.replace(
    /-----BEGIN PUBLIC KEY-----|-----END PUBLIC KEY-----|\s+/g,
    ""
  );
  const der = Uint8Array.from(Buffer.from(b64, "base64"));
  const spki = AsnParser.parse(der, SubjectPublicKeyInfo);
  const publicKeyHex = `0x${Buffer.from(
    new Uint8Array(spki.subjectPublicKey)
  ).toString("hex")}` as `0x${string}`;
  return publicKeyToAddress(publicKeyHex);
}

Build the KMS Signing Function

Cloud KMS returns DER-encoded signatures. Ethereum requires r, s, v format with EIP-2 low-s normalization. This function handles the full conversion:
kms-signer.ts
import { hashMessage, recoverPublicKey } from "viem";
import { publicKeyToAddress } from "viem/accounts";
import { Buffer } from "buffer";
import { kmsPemToEthAddress, publicKeyPem } from "./kms-utils";

const KMS_SIGNER_URL = "https://YOUR_CLOUD_FUNCTION_URL";

const SECP256K1_N =
    0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141n;
const SECP256K1_HALF_N = SECP256K1_N / 2n;

function derToRS(derHex: string): { r: bigint; s: bigint } {
    const bytes = Buffer.from(derHex, "hex");
    let offset = 2; // skip 0x30 + length byte

    offset++; // skip 0x02 tag for r
    const rLen = bytes[offset++];
    const r = BigInt("0x" + bytes.slice(offset, offset + rLen).toString("hex"));
    offset += rLen;

    offset++; // skip 0x02 tag for s
    const sLen = bytes[offset++];
    const s = BigInt("0x" + bytes.slice(offset, offset + sLen).toString("hex"));

    return { r, s };
}

function bigintToHex(value: bigint): `0x${string}` {
    return `0x${value.toString(16).padStart(64, "0")}`;
}

export async function signWithKms(message: `0x${string}`): Promise<`0x${string}`> {
    const digest = hashMessage({ raw: message });

    const response = await fetch(KMS_SIGNER_URL, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ digest: digest.slice(2) }),
    });

    if (!response.ok) {
        throw new Error(`KMS signing failed: ${response.statusText}`);
    }

    const { derSignature } = await response.json();
    let { r, s } = derToRS(derSignature);

    // EIP-2 low-s normalization
    if (s > SECP256K1_HALF_N) {
        s = SECP256K1_N - s;
    }

    const rHex = bigintToHex(r);
    const sHex = bigintToHex(s);
    const expectedAddress = kmsPemToEthAddress(publicKeyPem);

    // Recover v by trying both 27 and 28
    for (const v of [27n, 28n]) {
        const sig = `${rHex}${sHex.slice(2)}${
            v === 27n ? "1b" : "1c"
        }` as `0x${string}`;
        const recovered = publicKeyToAddress(
            await recoverPublicKey({ hash: digest, signature: sig })
        );
        if (recovered.toLowerCase() === expectedAddress.toLowerCase()) {
            return sig;
        }
    }

    throw new Error("Could not recover valid signature");
}

Create the Wallet

Use the KMS-derived Ethereum address as the wallet’s recovery signer. The recovery signer is used for wallet recovery and adding new signers — ideal for a server-held KMS key.
create-wallet.ts
import { CrossmintWallets, createCrossmint } from "@crossmint/wallets-sdk";
import { kmsPemToEthAddress, publicKeyPem } from "./kms-utils";
import { signWithKms } from "./kms-signer";

const crossmint = createCrossmint({
    apiKey: "YOUR_CROSSMINT_SERVER_API_KEY",
});
export const crossmintWallets = CrossmintWallets.from(crossmint);

const wallet = await crossmintWallets.createWallet({
    chain: "base-sepolia",
    recovery: {
        type: "external-wallet",
        address: kmsPemToEthAddress(publicKeyPem),
        onSign: async (message) => {
            return signWithKms(message as `0x${string}`);
        },
    },
    alias: "my-crossmint-wallet",
});

Use the Wallet

Once created, retrieve the wallet and use it to sign transactions. Here are a couple of examples:
use-wallet.ts
import { EVMWallet } from "@crossmint/wallets-sdk";
import { kmsPemToEthAddress, publicKeyPem } from "./kms-utils";
import { signWithKms } from "./kms-signer";
import { crossmintWallets } from "./create-wallet";

const wallet = await crossmintWallets.getWallet(
    "evm:alias:my-crossmint-wallet",
    { chain: "base-sepolia" }
);

await wallet.useSigner({
    type: "external-wallet",
    address: kmsPemToEthAddress(publicKeyPem),
    onSign: async (message) => {
        return signWithKms(message as `0x${string}`);
    },
});

// Example: Send tokens
await wallet.send("0xRecipientAddress", "usdxm", "1");

// Example: Send a raw EVM transaction
const evmWallet = EVMWallet.from(wallet);
await evmWallet.sendTransaction({
    to: "0xRecipientAddress",
    value: BigInt(0),
    data: "0x",
});

Security Best Practices

Key rotation caveat: While Cloud KMS supports automatic key rotation, rotating an asymmetric signing key generates a new key pair — which means a new Ethereum address. For wallets, manage key versions carefully and plan migrations accordingly.
  • Restrict Cloud Run access: Use IAM authentication or VPC Service Controls to ensure only authorized services can call your signing function.
  • Enable audit logs: Turn on Cloud KMS “Data Access” audit logs to track every signing operation.
  • Principle of least privilege: Only grant the signerVerifier role — never grant admin or encrypterDecrypter roles to your signing service account.
  • Environment variables: Never hardcode your Crossmint API key or GCP credentials. Use environment variables or a secrets manager.