-
-
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(); | |
} | |
} |
thank you bruh, saved my day!, also want to give an ethers-v6 compatible version
import {
GetPublicKeyCommand,
KMSClient,
SignCommand,
} from '@aws-sdk/client-kms';
import {
AbstractSigner,
getBytes,
hashMessage,
JsonRpcProvider,
keccak256,
recoverAddress,
resolveProperties,
Signature,
Transaction,
TransactionRequest,
} from 'ethers';
import * as asn1 from 'asn1.js';
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(),
);
});
/**
* @dev KMS Signer
*/
export class KmsSigner extends AbstractSigner {
keyId: string;
kms: KMSClient;
address?: string;
provider: JsonRpcProvider;
/**
* @dev Constructor
* @param kms
* @param keyId
* @param provider
*/
constructor(kms: KMSClient, keyId: string, provider: JsonRpcProvider) {
super();
this.keyId = keyId;
this.kms = kms;
this.provider = provider;
}
/**
* @dev Connect to provider
* @param provider
*/
connect(provider: JsonRpcProvider) {
return new KmsSigner(this.kms, this.keyId, provider);
}
/**
* @dev Get ethereum address
*/
async getAddress() {
if (this.address) {
return this.address;
}
const publicKey = await this.#getKmsPublicKey();
const address = this.#getEthereumAddress(publicKey);
this.address = address;
return address;
}
/**
* @dev Sign message
* @param msg
*/
async signMessage(msg: string) {
const hash = Buffer.from(hashMessage(msg).slice(2), 'hex');
return this.#signDigest(hash);
}
/**
* @dev Sign transaction
* @param request
*/
async signTransaction(request: TransactionRequest) {
const unsignedTx = await resolveProperties(request);
const tx = Transaction.from({
...unsignedTx,
from: unsignedTx.from as string,
to: unsignedTx.to as string,
});
const hash = Buffer.from(
keccak256(Transaction.from(tx).unsignedSerialized).slice(2),
'hex',
);
tx.signature = await this.#signDigest(hash);
return Transaction.from(tx).serialized;
}
/**
* @dev Get public key from KMS
* @private
*/
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);
}
/**
* @dev Sign message using KMS
* @param msg
* @private
*/
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);
}
/**
* @dev Get ethereum address from public key
* @param publicKey
* @private
*/
#getEthereumAddress(publicKey: Buffer) {
const res = EcdsaPubKey.decode(publicKey, 'der');
const pubKeyBuffer = res.pubKey.data.slice(1);
const addressBuf = Buffer.from(keccak256(pubKeyBuffer).slice(2), 'hex');
return `0x${addressBuf.subarray(-20).toString('hex')}`;
}
/**
* @dev Sign digest
* @param digest
* @private
*/
async #signDigest(digest: Buffer) {
const msg = Buffer.from(getBytes(digest));
const signature = await this.#kmsSign(msg);
const { r, s } = this.#getSigRs(signature);
const { v } = await this.#getSigV(msg, { r, s });
return Signature.from({ r, s, v }).serialized;
}
/**
* @dev Get v value from signature
* @param msgHash
* @param r
* @param s
* @private
*/
async #getSigV(msgHash: Buffer, { r, s }: { r: string; s: string }) {
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 };
}
/**
* @dev Parse signature to get r and s
* @param signature
* @private
*/
#getSigRs(signature: Buffer) {
const decoded = EcdsaSigAsnParse.decode(signature, 'der');
let r: bigint | string = BigInt(`0x${decoded.r.toString(16)}`);
let s: bigint | string = BigInt(`0x${decoded.s.toString(16)}`);
const secp256k1N = BigInt(
'0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141',
);
const secp256k1halfN = secp256k1N / BigInt(2);
if (s > secp256k1halfN) {
s = secp256k1N - s;
}
r = `0x${r.toString(16)}`;
s = `0x${s.toString(16)}`;
return { r, s };
}
/**
* @dev Compare two addresses
* @param address1
* @param address2
* @private
*/
#addressEquals(address1: string, address2: string) {
return address1.toLowerCase() === address2.toLowerCase();
}
/**
* @dev This method is not implemented
* @param args
*/
signTypedData(
...args: Parameters<AbstractSigner['signTypedData']>
): Promise<string> {
console.log('signTypedData', args);
throw new Error('Method not implemented.');
}
}
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
#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.
a terribly incorrect but happy type declaration for asn1.js