Skip to content

Instantly share code, notes, and snippets.

@dangdennis
Forked from miguelmota/KmsSigner.js
Last active October 21, 2024 20:19
Show Gist options
  • Save dangdennis/3383b554f3c3ffe05356988b91a0d769 to your computer and use it in GitHub Desktop.
Save dangdennis/3383b554f3c3ffe05356988b91a0d769 to your computer and use it in GitHub Desktop.
Typescripts ethers Signer using AWS KMS
// 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();
}
}
@dangdennis
Copy link
Author

a terribly incorrect but happy type declaration for asn1.js

// These types are entirely wrong but makes Typescript happy.
declare module "asn1.js" {
    type T = {
        seq(): T
        obj(arg1: any, arg2: any): T
        key(arg: any): T
        objid(): T
        bitstr(): string
        int(): T
    }
    
    function define(type: string, cb: function(this: T): void)
}

@0xtrou
Copy link

0xtrou commented Sep 8, 2024

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.');
  }
}

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