-
-
Save dangdennis/3383b554f3c3ffe05356988b91a0d769 to your computer and use it in GitHub Desktop.
| // 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 define v 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
In the
#getSigVmethod you definevas 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.
thank you bruh, saved my day!, also want to give an ethers-v6 compatible version