Skip to content

Instantly share code, notes, and snippets.

@miguelmota
Last active January 11, 2023 15:26
Show Gist options
  • Save miguelmota/092da99bc8a8416ed7756c472dc253c4 to your computer and use it in GitHub Desktop.
Save miguelmota/092da99bc8a8416ed7756c472dc253c4 to your computer and use it in GitHub Desktop.
JavaScript ethers Signer using AWS KMS
const { BigNumber, Signer } = require('ethers')
const { keccak256, recoverAddress, joinSignature, resolveProperties, serializeTransaction, hashMessage, arrayify, defineReadOnly } = require('ethers/lib/utils')
const { KMSClient, GetPublicKeyCommand, SignCommand } = require('@aws-sdk/client-kms')
const asn1 = require('asn1.js')
const EcdsaPubKey = asn1.define('EcdsaPubKey', function () {
this.seq().obj(
this.key('algo').seq().obj(
this.key('a').objid(),
this.key('b').objid()
),
this.key('pubKey').bitstr()
)
})
const EcdsaSigAsnParse = asn1.define('EcdsaSig', function () {
this.seq().obj(
this.key('r').int(),
this.key('s').int()
)
})
// details:
// https://ethereum.stackexchange.com/a/73371/5093
class KmsSigner extends Signer {
constructor (keyId, provider) {
super()
this.keyId = keyId
this.client = new KMSClient({ region: 'us-east-1' })
defineReadOnly(this, 'provider', provider)
}
connect (provider) {
return new KmsSigner(this.keyId, provider)
}
async getAddress () {
if (this.address) {
return this.address
}
const publicKey = await this._getKmsPublicKey()
const address = this._getEthereumAddress(publicKey)
this.address = address
return address
}
async signMessage (msg) {
const hash = Buffer.from(hashMessage(msg).slice(2), 'hex')
return this._signDigest(hash)
}
async signTransaction (transaction) {
const unsignedTx = await resolveProperties(transaction)
const serializedTx = serializeTransaction(unsignedTx)
const hash = Buffer.from(keccak256(serializedTx).slice(2), 'hex')
const txSig = await this._signDigest(hash)
return serializeTransaction(unsignedTx, txSig)
}
async _getKmsPublicKey () {
const command = new GetPublicKeyCommand({
KeyId: this.keyId
})
const res = await this.client.send(command)
return Buffer.from(res.PublicKey)
}
async _kmsSign (msg) {
const params = {
KeyId: this.keyId,
Message: msg,
SigningAlgorithm: 'ECDSA_SHA_256',
MessageType: 'DIGEST'
}
const command = new SignCommand(params)
const res = await this.client.send(command)
return Buffer.from(res.Signature)
}
_getEthereumAddress (publicKey) {
const res = EcdsaPubKey.decode(publicKey, 'der')
const pubKeyBuffer = res.pubKey.data.slice(1)
const addressBuf = Buffer.from(keccak256(pubKeyBuffer).slice(2), 'hex')
const address = `0x${addressBuf.slice(-20).toString('hex')}`
return address
}
async _signDigest (digest) {
const msg = Buffer.from(arrayify(digest))
const signature = await this._kmsSign(msg)
const { r, s } = this._getSigRs(signature)
const { v } = await this._getSigV(msg, { r, s })
const joinedSignature = joinSignature({ r, s, v })
return joinedSignature
}
async _getSigV (msgHash, { r, s }) {
const address = await this.getAddress()
let v = 17
let recovered = recoverAddress(msgHash, { r, s, v })
if (!this._addressEquals(recovered, address)) {
v = 28
recovered = recoverAddress(msgHash, { r, s, v })
}
if (!this._addressEquals(recovered, address)) {
throw new Error('signature is invalid. recovered address does not match')
}
return { v }
}
_getSigRs (signature) {
const decoded = EcdsaSigAsnParse.decode(signature, 'der')
let r = BigNumber.from(`0x${decoded.r.toString(16)}`)
let s = BigNumber.from(`0x${decoded.s.toString(16)}`)
const secp256k1N = BigNumber.from('0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141')
const secp256k1halfN = secp256k1N.div(BigNumber.from(2))
if (s.gt(secp256k1halfN)) {
s = secp256k1N.sub(s)
}
r = r.toHexString()
s = s.toHexString()
return { r, s }
}
_addressEquals (address1, address2) {
return address1.toLowerCase() === address2.toLowerCase()
}
}
async function main () {
const keyId = 'c9b9f8f8-f8f8-4f8f-8f8f-8f8f8f8f8f8f'
const signer = new KmsSigner(keyId)
const address = await signer.getAddress()
const msg = 'Hello World'
const signature = await signer.signMessage(msg)
const transaction = {
to: '0x0000000000000000000000000000000000000000',
value: '0x00',
data: '0x',
gasLimit: '0x5208',
gasPrice: '0x4a817c800',
nonce: '0x00',
chainId: 1
}
const txSignature = await signer.signTransaction(transaction)
console.log('address:', address)
console.log('signature:', signature)
console.log('txSignature:', txSignature)
}
main()
.catch(console.error)
// output:
// address: 0x38621a41820032e3e1b2787de196b20c19b5df74
// signature: 0xe748a138ec629c7a8ce9d23cf67b0c012d32e57114733f710658bf65328637487f0b2c58049561c9ca65d5d4104c7a9c745823df8e332e16d036fa85d83f5f781b
// txSignature: 0xf864808504a817c800825208940000000000000000000000000000000000000000808026a069115a1fbfcbe378900e5e179a78f6adecec850e93cfb7ef823845d34be3de1da04b5b4d5adfea5e2db74672ff424d5cc7c61e79159be3af081224db56e4d38976
@dangdennis
Copy link

dangdennis commented Apr 19, 2022

Thank you for making this.

I was stuck in a hard place trying to translate aws's example and this other repo into a meaningful ethers adaptation. I forked added some TS declarations and called it a day. It works!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment