Skip to content

Instantly share code, notes, and snippets.

@10thfloor
Forked from MaritalWheat/flow-google-kms.ts
Created November 8, 2022 17:54
Show Gist options
  • Save 10thfloor/394488a478704fe2040daccf899c83a0 to your computer and use it in GitHub Desktop.
Save 10thfloor/394488a478704fe2040daccf899c83a0 to your computer and use it in GitHub Desktop.
@onflow GC KMS tx signing
import * as fcl from "@onflow/fcl"
import { KeyManagementServiceClient } from "@google-cloud/kms"
import * as crypto from "crypto"
import { fromBER, Sequence, Integer } from "asn1js"
var crc32c = require("fast-crc32c")
/////////////////////////////////////////////////////////////////////////////////////////////////
// This gist shows a quick example of using a Google Cloud managed key to sign a Flow transaction
// This would be called in a pattern similar to the below:
//
// const authorization = this.flowService.authorizeSigner()
// const transaction = [some code for constructing your transaction]
//
// return this.flowService.sendTx({
// transaction,
// args: [],
// authorizations: [authorization],
// payer: authorization,
// proposer: authorization,
// })
/////////////////////////////////////////////////////////////////////////////////////////////////
type Transaction = {
transaction: string
args: any
proposer: any
authorizations: any
payer: any
}
// Enter your Google Cloud configuration details here
const projectId = "your-value"
const locationId = "your-value"
const keyRingId = "your-value"
const keyId = "your-value"
const versionId = "your-value"
class FlowService {
constructor(
private readonly signerFlowAddress: string,
private readonly signerAccountIndex: string | number
) {}
// Create the proper authorization, utilizing a Google Cloud KMS-stored key
authorizeSigner = () => {
return async (account: any = {}) => {
const user = await this.getAccount(this.signerFlowAddress)
const key = user.keys[this.signerAccountIndex]
let sequenceNum
if (account.role.proposer) {
sequenceNum = key.sequenceNumber
}
const signingFunction = async (data: { message: string }) => {
const kmsSignature = await this.signAsymmetric(data.message)
return {
addr: user.address,
keyId: key.index,
signature: kmsSignature,
}
}
return {
...account,
tempId: user.address,
addr: user.address,
keyId: key.index,
sequenceNum,
signature: account.signature || null,
signingFunction,
resolve: null,
roles: account.roles,
}
}
}
getAccount = async (addr: string) => {
const { account } = await fcl.send([fcl.getAccount(addr)])
return account
}
sendTx = async ({
transaction,
args,
proposer,
authorizations,
payer,
}: Transaction): Promise<any> => {
const response = await fcl.send([
fcl.transaction`
${transaction}
`,
fcl.args(args),
fcl.proposer(proposer),
fcl.authorizations(authorizations),
fcl.payer(payer),
fcl.limit(9999),
])
return await fcl.tx(response).onceSealed()
}
// Where the Google KMS signing occurs
async signAsymmetric(message: string) {
const client = new KeyManagementServiceClient()
const versionName = client.cryptoKeyVersionPath(
projectId,
locationId,
keyRingId,
keyId,
versionId
)
// Create a digest of the message. The digest needs to match the digest
// configured for the Google Cloud KMS key (note: this appears to be SHA2-256)
const hash = crypto.createHash("sha256")
hash.update(Buffer.from(message, "hex"))
const digest = hash.digest()
// Optional but recommended: Compute digest's CRC32C.
const digestCrc32c = crc32c.calculate(digest)
// Sign the message with Google Cloud KMS
const [signResponse] = await client.asymmetricSign({
name: versionName,
digest: {
sha256: digest,
},
digestCrc32c: {
value: digestCrc32c,
},
})
// Optional, but recommended: perform integrity verification on signResponse.
// For more details on ensuring E2E in-transit integrity to and from Cloud KMS visit:
// https://cloud.google.com/kms/docs/data-integrity-guidelines
if (signResponse.name !== versionName) {
throw new Error("AsymmetricSign: request corrupted in-transit")
}
if (!signResponse.verifiedDigestCrc32c) {
throw new Error("AsymmetricSign: request corrupted in-transit")
}
if (crc32c.calculate(signResponse.signature) !== Number(signResponse.signatureCrc32c?.value)) {
throw new Error("AsymmetricSign: response corrupted in-transit")
}
// Because the signature is in a binary format, you need to encode the output before printing it to a
// console or displaying it on a screen.
const sig = signResponse.signature
const encoded = Buffer.concat([sig! as Uint8Array]).toString("base64")
console.log(`Signature: ${encoded}` + "\n")
// Convert the binary signature output to to format Flow network expects
const { r, s } = this.parseSignature(signResponse.signature! as Buffer)
return Buffer.concat([r, s]).toString("hex")
}
toArrayBuffer(buffer: Buffer) {
const ab = new ArrayBuffer(buffer.length)
const view = new Uint8Array(ab)
for (let i = 0; i < buffer.length; ++i) {
view[i] = buffer[i]
}
return ab
}
parseSignature(buf: Buffer) {
const { result } = fromBER(this.toArrayBuffer(buf))
const values = (result as Sequence).valueBlock.value
const getHex = (value: Integer) => {
const buf = Buffer.from(value.valueBlock.valueHex)
return buf.slice(Math.max(buf.length - 32, 0))
}
const r = getHex(values[0] as Integer)
const s = getHex(values[1] as Integer)
return { r, s }
}
}
export { FlowService }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment