How to manage a million dollars on Solana with Cloud KMS
This post shows how to set up a keypair in google kms to securely sign transactions and messages on Solana.
The good thing about using a KMS solution to handle keys, is that you never see the actual key material. The private key will never be shown. This means you can safely build a system to manage a million dollars, or a system to do other sensitive signing operations. (sorry for the clickbaity title).
Solana uses the Elliptic Curve ED25519 signing standard. AWS doesn’t support this at the moment, but thankfully, Google Cloud does!
Set up Google Cloud KMS
I won’t go into detail about how to set up Google Cloud KMS, but the main points are:
- “Enable” Cloud Key Management Service (KMS) API in the Console
- Add a keyring in Key Manager
- Create a service account that has “Cloud KMS CryptoKey Signer/Verifier” access on the keyring
In this post I’ll use the javascript and the nodeJS api to interact with kms, but this is easily transferable to other languages and frameworks.
Creating a keypair
You can create a key through the Console, or in code. The important thing is to select an asymmetric key and use the ED25519
signing scheme.
const parentKeyRingName = kms.keyRingPath(
projectId,
"global",
keyRingId,
)
const myKeyId = "some-key-id"
const [key] = await kms.createCryptoKey({
parent: keyRingName,
cryptoKeyId: myKeyId,
cryptoKey: {
purpose: "ASYMMETRIC_SIGN",
versionTemplate: {
algorithm: "EC_SIGN_ED25519",
},
},
})
Getting the Solana public key
When fetching the public key from the KMS service, it will look something like this:
const [publicKey] = await kms.getPublicKey({
name: versionName,
})
console.log(publicKey.pem)
-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEAEh/PETwwliU2EE1FH1NFnF7REHX2mJ4wRboPtCup2ug=
-----END PUBLIC KEY-----
We’re only interested in the last part of the public key (stripping away the first 12 chars). This is base64 encoded.
Eh/PETwwliU2EE1FH1NFnF7REHX2mJ4wRboPtCup2ug=
const [publicKey] = await kms.getPublicKey({
name: versionName,
})
if (!publicKey.pem) {
throw new Error("Public key not found")
}
const rawPk = publicKey.pem
.replace("-----BEGIN PUBLIC KEY-----\n", "")
.replace("\n-----END PUBLIC KEY-----\n", "")
const pkBufferForSolana = Buffer.from(rawPk, "base64").slice(12, 44)
To get the public address in a recognizable format, we need to re-encode it to base58.
const [solanaAddress] = base58.deserialize(pkBufferForSolana)
Signing a transaction
First of all, you need to build the transaction without signing it. You can do this with Umi like this:
const transaction = transactionBuilder().add(
// example: transfer token
transferTokens(umi, {
source: sourceTokenAccount,
destination: destinationTokenAccount,
amount: 1n,
}),
)
const builtTransaction = await transaction.buildWithLatestBlockhash(umi)
const serializedMessage = builtTransaction.serializedMessage
Now sign the serializedMessage
with KMS:
const [asymmetricSignResponse] = await kms.asymmetricSign({
name: versionName,
data: builtTransaction.serializedMessage,
})
const signature = asymmetricSignResponse.signature
Congratulations! You have now successfully signed the transaction.
In order to submit this to chain, we can do this:
await umi.rpc.sendTransaction({
signatures: [signature],
message: builtTransaction.message,
serializedMessage: builtTransaction.serializedMessage,
})
const result = await umi.rpc.sendTransaction(signedTransaction)
If we want to get the familiar base58 format seen on the explorer:
const [sigAsB58] = base58.deserialize(signature)
console.log("sigAsB58: ", sigAsB58)
This gives us the signature we can use to create this URL: https://explorer.solana.com/tx/3tTvJKpim4YqBtQBVcSJNmPD2Tb1nzV7XGRiUVW8cc9rgJmPZx2eDNUMcp5YpQ3V9US4uxRkwDZGxfJ58ci58fLo?cluster=devnet.
Tadaa!