-
-
Save dangdennis/3383b554f3c3ffe05356988b91a0d769 to your computer and use it in GitHub Desktop.
Typescripts ethers Signer using AWS KMS
This file contains hidden or 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
// Code taken from https://gist.github.com/miguelmota/092da99bc8a8416ed7756c472dc253c4 | |
import { | |
GetPublicKeyCommand, | |
KMSClient, | |
SignCommand, | |
} from "@aws-sdk/client-kms"; | |
import { BigNumber, ethers, Signer, UnsignedTransaction } from "ethers"; | |
import * as asn1 from "asn1.js"; | |
import { AlchemyProvider } from "@ethersproject/providers"; | |
const EcdsaSigAsnParse = asn1.define("EcdsaSig", function (this) { | |
// parsing this according to https://tools.ietf.org/html/rfc3279#section-2.2.3 | |
this.seq().obj(this.key("r").int(), this.key("s").int()); | |
}); | |
const EcdsaPubKey = asn1.define("EcdsaPubKey", function (this) { | |
// copied from https://github.com/rjchow/ethers-aws-kms-signer/blob/8e3a4812b542e86ac9d3b6da02b794eb1b5be86d/src/util/aws-kms-utils.ts#L46 | |
// parsing this according to https://tools.ietf.org/html/rfc5480#section-2 | |
this.seq().obj( | |
this.key("algo").seq().obj(this.key("a").objid(), this.key("b").objid()), | |
this.key("pubKey").bitstr() | |
); | |
}); | |
// details: | |
// https://ethereum.stackexchange.com/a/73371/5093 | |
export class KmsSigner extends Signer { | |
keyId: string; | |
kms: KMSClient; | |
address?: string; | |
constructor(kms: KMSClient, keyId: string, provider: AlchemyProvider) { | |
super(); | |
this.keyId = keyId; | |
this.kms = kms; | |
ethers.utils.defineReadOnly(this, "provider", provider); | |
} | |
connect(provider: AlchemyProvider) { | |
return new KmsSigner(this.kms, 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: string) { | |
const hash = Buffer.from(ethers.utils.hashMessage(msg).slice(2), "hex"); | |
return this.#signDigest(hash); | |
} | |
async signTransaction( | |
transaction: ethers.utils.Deferrable<ethers.providers.TransactionRequest> | |
) { | |
const unsignedTx = await ethers.utils.resolveProperties(transaction); | |
const serializedTx = ethers.utils.serializeTransaction( | |
unsignedTx as UnsignedTransaction | |
); | |
const hash = Buffer.from( | |
ethers.utils.keccak256(serializedTx).slice(2), | |
"hex" | |
); | |
const txSig = await this.#signDigest(hash); | |
return ethers.utils.serializeTransaction( | |
unsignedTx as UnsignedTransaction, | |
txSig | |
); | |
} | |
async #getKmsPublicKey() { | |
const command = new GetPublicKeyCommand({ | |
KeyId: this.keyId, | |
}); | |
const res = await this.kms.send(command); | |
if (!res.PublicKey) { | |
throw new Error("Missing public key"); | |
} | |
return Buffer.from(res.PublicKey); | |
} | |
async #kmsSign(msg: Buffer) { | |
const command = new SignCommand({ | |
KeyId: this.keyId, | |
Message: msg, | |
SigningAlgorithm: "ECDSA_SHA_256", | |
MessageType: "DIGEST", | |
}); | |
const res = await this.kms.send(command); | |
if (!res.Signature) { | |
throw new Error("Missing signature"); | |
} | |
return Buffer.from(res.Signature); | |
} | |
#getEthereumAddress(publicKey: Buffer) { | |
const res = EcdsaPubKey.decode(publicKey, "der"); | |
const pubKeyBuffer = res.pubKey.data.slice(1); | |
const addressBuf = Buffer.from( | |
ethers.utils.keccak256(pubKeyBuffer).slice(2), | |
"hex" | |
); | |
const address = `0x${addressBuf.slice(-20).toString("hex")}`; | |
return address; | |
} | |
async #signDigest(digest: Buffer) { | |
const msg = Buffer.from(ethers.utils.arrayify(digest)); | |
const signature = await this.#kmsSign(msg); | |
const { r, s } = this.#getSigRs(signature); | |
const { v } = await this.#getSigV(msg, { r, s }); | |
const joinedSignature = ethers.utils.joinSignature({ r, s, v }); | |
return joinedSignature; | |
} | |
async #getSigV(msgHash: Buffer, { r, s }: { r: string; s: string }) { | |
const address = await this.getAddress(); | |
let v = 27; | |
let recovered = ethers.utils.recoverAddress(msgHash, { r, s, v }); | |
if (!this.#addressEquals(recovered, address)) { | |
v = 28; | |
recovered = ethers.utils.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: Buffer) { | |
const decoded = EcdsaSigAsnParse.decode(signature, "der"); | |
let r: BigNumber | string = BigNumber.from(`0x${decoded.r.toString(16)}`); | |
let s: BigNumber | string = 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: string, address2: string) { | |
return address1.toLowerCase() === address2.toLowerCase(); | |
} | |
} |
In the
#getSigV
method you definev
as either 17 or 28. Is this correct, other examples mention 27 and 28? Or am I missing something here?Sources: https://ethereum.stackexchange.com/questions/97971/aws-kms-sigining-using-the-sign-verify-method#:~:text=the%20value%20of%20v%20could%20be%20either%2027%20or%2028 https://luhenning.medium.com/the-dark-side-of-the-elliptic-curve-signing-ethereum-transactions-with-aws-kms-in-javascript-83610d9a6f81#:~:text=recovery%20id%20and%20it%20can%20be%20one%20of%20two%20possible%20values%3A%2027%20or%2028
Yes you’re correct! I’ll update for posterity.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
In the
#getSigV
method you definev
as either 17 or 28.Is this correct, other examples mention 27 and 28?
Or am I missing something here?
Sources:
https://ethereum.stackexchange.com/questions/97971/aws-kms-sigining-using-the-sign-verify-method#:~:text=the%20value%20of%20v%20could%20be%20either%2027%20or%2028
https://luhenning.medium.com/the-dark-side-of-the-elliptic-curve-signing-ethereum-transactions-with-aws-kms-in-javascript-83610d9a6f81#:~:text=recovery%20id%20and%20it%20can%20be%20one%20of%20two%20possible%20values%3A%2027%20or%2028