Signing Transactions on Flow with KMS

Hey everyone,
it was recommended to us not to store the private keys in ENV vars on our backend - instead, we should store the keys in a KMS on AWS or Google. (note: I’m not talking about a custodian solution - we would use this to store our own admin account keys). Now, how do you actually get this to work (get signed) with an outside service like AWS KMS?
If you look at the Transaction script like this one:
tx := flow.NewTransaction().
SetScript([]byte(transactionString)).
SetGasLimit(9999).
SetPayer(senderAddr).
AddAuthorizer(senderAddr).
SetReferenceBlockID(referenceBlockID).
SetProposalKey(senderAddr, senderAccountKey.Index, senderAccountKey.SequenceNumber)

and also:
tx.SignEnvelope(senderAddr, senderAccountKey.Index, signer)

→ they require the actual Private key in a string format. How should I get this transaction signed without my backend actually getting the private key in the string format?

Tnx!

1 Like

Hello,

You can check out this project https://github.com/doublejumptokyo/fcl-kms-authorizer that signs transactions with AWS KMS. The idea with a KMS is instead of fetching the private key and sign, it sends the data to AWS and get a signed version back. Here’s how it’s done in the above library: https://github.com/doublejumptokyo/fcl-kms-authorizer/blob/9718e4719994ea1a33f2895400a43f433d074b4f/src/signer.ts#L60

2 Likes

Thank you @navid - that is the Typescript implementation. We really need the Golang version. It seems like there is an implementation in the official Flow Go SDK: https://github.com/onflow/flow-go-sdk/blob/573abf0bffa1334267386f3960a56c65516996cc/crypto/cloudkms/signer.go but I can’t find a good example how this Signer can be used instead of the crypto.NewInMemorySigner(…). @sideninja Any help would be appreciated :wink:

1 Like

Hello,

Does this example help?

1 Like

Tnx @navid - looks very promising. I will test it and let you know :wink:

@navid Also, I wonder what would it take to use to work with AWS KMS instead Google?

You need to implement Sign for AWS. Check out the GCP implementation here
Other signing functions use signer like this
There are two concepts to be aware of:

  • Parsing signatures requires padding: link
  • Append the domain tag to serialized transaction before signing: link

The second point won’t be needed if you implement Sign only.

1 Like

Hey @navid - I was able to set up the Google KMS (service account and asymmetric key). I used the
flow keys decode pem --from-file new-key2.pub
to convert the public key into a proper format and then used that key to create an account on Testnet via faucet.
Finally, I integrated it into our Golang backend like this:

func TestKMS() {
ctx := context.Background()

node := "access.devnet.nodes.onflow.org:9000" // NODE

flowClient, err := client.New(node, grpc.WithInsecure())
examples.Handle(err)

senderAddr := flow.HexToAddress("XXXXXXXXXX") // ADMIN ADDRESS
account, err := flowClient.GetAccount(ctx, senderAddr)

examples.Handle(err)

senderAccountKey := account.Keys[0]

accountKMSKey := cloudkms.Key{
	ProjectID:  "GOOGLE_KMS_PROJECT_ID",
	LocationID: "GOOGLE_KMS_LOCATION_ID",
	KeyRingID:  "GOOGLE_KMS_KEY_RING_ID",
	KeyID:      "GOOGLE_KMS_KEY_ID",
	KeyVersion: "GOOGLE_KMS_KEY_VERSION",
}
fmt.Print("accountKMSKey: ", accountKMSKey)

kmsClient, err := cloudkms.NewClient(ctx, option.WithCredentialsFile("Credentials.json"))
if err != nil {
	panic(err)
}

accountKMSSigner, err := kmsClient.SignerForKey(
	ctx,
	senderAddr,
	accountKMSKey,
)
if err != nil {
	panic(err)
}

latestBlock, err := flowClient.GetLatestBlockHeader(ctx, true)
if err != nil {
	panic(err)
}

var testTransactionString string = `
transaction() {
prepare(signer: AuthAccount) {
    // Get a key from an auth account.
    let keyA = signer.keys.get(0)
}

}
`
tx := flow.NewTransaction().
SetScript([]byte(testTransactionString)).
SetGasLimit(1000).
SetPayer(senderAddr).
SetReferenceBlockID(latestBlock.ID).
SetProposalKey(senderAddr, senderAccountKey.Index, senderAccountKey.SequenceNumber)

err = tx.SignEnvelope(senderAddr, senderAccountKey.Index, accountKMSSigner)
examples.Handle(err)

err = flowClient.SendTransaction(ctx, *tx)
examples.Handle(err)

result := examples.WaitForSeal(ctx, flowClient, tx.ID())
examples.Handle(result.Error)

fmt.Println("Transaction was Successful")

}

------> Unfortunately, whatever we do, we get the:
[Error Code: 1006] invalid proposal key: public key 0 on account XXXXXXXXXXXX does not have a valid signature: [Error Code: 1009] invalid envelope key: public key 0 on account XXXXXXXXXXXX does not have a valid signature: signature is not valid

Google KMS is not returning any errors so I assume the key signing process is done properly.
Before implementing Google KMS, we were using the Private key directly from Env Vars and it was working perfectly fine.

Any help would be appreciated…

Hello,

Have you created the account with the public key from the Google KMS? One potential problem is that the default hash algorithm for Google keys is SHA2_256.

Yes, the account was created with public key from Google KMS (downloaded and decoded with flow keys decode pem --from-file new-key2.pub ). Check attached screenshot.


Additional resources: How to authorize Flow Blockchain transactions with Google Cloud Key Management Service – Agency04

Can you share the transaction you created the account on Flow?

Tnx @navid You actually helped earlier. I was able to resolve the issue - I double checked all steps and realized that it was indeed SHA3_256 that I have chosen from the dropdown on the Faucet instead of the SHA2_256 during the account creation. It’s confusing because Google simply says SHA256. Tnx for the tip! I wonder if there’s a way to get better error response for cases like this…