Last active
January 11, 2023 15:26
-
-
Save miguelmota/092da99bc8a8416ed7756c472dc253c4 to your computer and use it in GitHub Desktop.
JavaScript ethers Signer using AWS KMS
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
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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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!