-
-
Save 10thfloor/394488a478704fe2040daccf899c83a0 to your computer and use it in GitHub Desktop.
@onflow GC KMS tx signing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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