├── .eslintignore ├── .prettierrc ├── .releaserc.yaml ├── .gitignore ├── .yo-rc.json ├── commitlint.config.js ├── jest.config.js ├── .babelrc ├── .editorconfig ├── .circleci └── config.yml ├── tsconfig.json ├── src ├── util │ ├── logger.ts │ ├── aws-kms-utils.test.ts │ └── aws-kms-utils.ts └── index.ts ├── .github └── workflows │ ├── release.yml │ └── test.yml ├── .eslintrc ├── LICENSE ├── README.md ├── package.json └── CHANGELOG.md /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage 2 | dist 3 | node_modules -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120 3 | } -------------------------------------------------------------------------------- /.releaserc.yaml: -------------------------------------------------------------------------------- 1 | branch: master 2 | extends: "semantic-release-npm-github-publish" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | coverage 3 | dist 4 | node_modules 5 | *.log 6 | .vscode 7 | -------------------------------------------------------------------------------- /.yo-rc.json: -------------------------------------------------------------------------------- 1 | { 2 | "generator-semantic-module": { 3 | "packager": "npm", 4 | "commitizen-adapter": "@commitlint/prompt", 5 | "commitlint-config": "@commitlint/config-conventional" 6 | } 7 | } -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 3 | module.exports = { 4 | extends: ["@commitlint/config-conventional"], 5 | 6 | // Add your own rules. See http://marionebl.github.io/commitlint 7 | rules: {}, 8 | }; 9 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const debug = require("debug"); 2 | 3 | // Jest swallows stderr from debug, so if process is called with DEBUG then redirect debug to console.log 4 | if (process.env.DEBUG) { 5 | /* eslint-disable no-console */ 6 | debug.log = console.log.bind(console); 7 | } 8 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "targets": { 7 | "node": 6 8 | } 9 | } 10 | ], 11 | "@babel/preset-typescript" 12 | ], 13 | "plugins": [ 14 | "@babel/plugin-proposal-class-properties", 15 | "@babel/plugin-proposal-optional-chaining", 16 | "@babel/plugin-proposal-nullish-coalescing-operator" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | 9 | # Change these settings to your own preference 10 | indent_style = space 11 | indent_size = 2 12 | 13 | # We recommend you to keep these unchanged 14 | end_of_line = lf 15 | charset = utf-8 16 | trim_trailing_whitespace = true 17 | insert_final_newline = true 18 | 19 | [*.md] 20 | trim_trailing_whitespace = false 21 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: 'circleci/node:latest' 6 | steps: 7 | - checkout 8 | - run: 9 | name: install 10 | command: npm install 11 | - run: 12 | name: lint 13 | command: npm run lint 14 | - run: 15 | name: build 16 | command: npm run build 17 | - run: npm run commitlint-circle 18 | - run: 19 | name: release 20 | command: npm run semantic-release 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "resolveJsonModule": true, 4 | "outDir": "dist/ts", 5 | "target": "esnext", 6 | "module": "esnext", 7 | "moduleResolution": "node", 8 | "jsx": "react", 9 | "strict": false, 10 | "declaration": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "noImplicitReturns": true, 13 | "noUnusedLocals": true, 14 | "noUnusedParameters": true, 15 | "stripInternal": true, 16 | "allowSyntheticDefaultImports": true 17 | } 18 | } -------------------------------------------------------------------------------- /src/util/logger.ts: -------------------------------------------------------------------------------- 1 | import debug from "debug"; 2 | import { name } from "../../package.json"; // automatically set logger namespace to package.json name 3 | 4 | export const LOGGER_NAMESPACE = name; 5 | 6 | const logger = debug(LOGGER_NAMESPACE); 7 | 8 | export const trace = (namespace: string) => logger.extend(`trace:${namespace}`); 9 | export const info = (namespace: string) => logger.extend(`info:${namespace}`); 10 | export const error = (namespace: string) => logger.extend(`error:${namespace}`); 11 | 12 | export const getLogger = (namespace: string) => ({ 13 | trace: trace(namespace), 14 | info: info(namespace), 15 | error: error(namespace), 16 | }); 17 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | branches: 5 | - master 6 | jobs: 7 | release: 8 | name: Release 9 | runs-on: ubuntu-latest 10 | environment: 11 | name: 'NPM Deploy' 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v2 15 | with: 16 | fetch-depth: 0 17 | - name: Setup Node.js 18 | uses: actions/setup-node@v1 19 | with: 20 | node-version: 12 21 | - name: Install dependencies 22 | run: npm ci 23 | - name: Release 24 | env: 25 | GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} 26 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 27 | run: npx semantic-release -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": ["airbnb-base", "plugin:prettier/recommended"], 4 | "env": { 5 | "jest": true 6 | }, 7 | "settings": { 8 | "import/resolver": { 9 | "node": { 10 | "extensions": [".js", ".jsx", ".ts", ".tsx"] 11 | } 12 | } 13 | }, 14 | "overrides": [ 15 | { 16 | "files": ["**/*.ts", "**/*.tsx"], 17 | "parser": "@typescript-eslint/parser", 18 | "parserOptions": { 19 | "project": "./tsconfig.json" 20 | }, 21 | "plugins": ["@typescript-eslint"], 22 | "rules": { 23 | "no-undef": "off", 24 | "no-unused-vars": "off", 25 | "no-restricted-globals": "off" 26 | } 27 | } 28 | ], 29 | "rules": { 30 | "import/extensions": "off", 31 | "import/prefer-default-export": "off", 32 | "import/no-default-export": "error", 33 | "no-underscore-dangle": [2, { "allowAfterThis": true }] 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 RJ Chow 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | test: 7 | name: Lint & Test 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - uses: actions/setup-node@v1 12 | with: 13 | node-version: 12.x 14 | - name: Cache Node Modules 15 | uses: actions/cache@v2 16 | env: 17 | cache-name: cache-node-modules 18 | with: 19 | # npm cache files are stored in `~/.npm` on Linux/macOS 20 | path: ~/.npm 21 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} 22 | restore-keys: | 23 | ${{ runner.os }}-build-${{ env.cache-name }}- 24 | ${{ runner.os }}-build- 25 | ${{ runner.os }}- 26 | - name: Install Packages 27 | # there's a problem with ganache-core using a very old version of ethereumjs-abi which fails on git checkout with ssh reasons 28 | run: | 29 | git config --global url."https://".insteadOf git:// 30 | git config --global url."https://".insteadOf git+https:// 31 | git config --global url."https://".insteadOf ssh://git 32 | npm ci 33 | - name: Check Lint 34 | run: npm run lint 35 | - name: Test 36 | run: npm run test 37 | - name: Build 38 | run: npm run build -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { ethers, UnsignedTransaction } from "ethers"; 2 | import { getPublicKey, getEthereumAddress, requestKmsSignature, determineCorrectV } from "./util/aws-kms-utils"; 3 | 4 | export interface AwsKmsSignerCredentials { 5 | accessKeyId?: string; 6 | secretAccessKey?: string; 7 | sessionToken?: string; 8 | region: string; 9 | keyId: string; 10 | } 11 | export class AwsKmsSigner extends ethers.Signer { 12 | kmsCredentials: AwsKmsSignerCredentials; 13 | 14 | ethereumAddress: string; 15 | 16 | constructor(kmsCredentials: AwsKmsSignerCredentials, provider?: ethers.providers.Provider) { 17 | super(); 18 | ethers.utils.defineReadOnly(this, "provider", provider || null); 19 | ethers.utils.defineReadOnly(this, "kmsCredentials", kmsCredentials); 20 | } 21 | 22 | async getAddress(): Promise { 23 | if (this.ethereumAddress === undefined) { 24 | const key = await getPublicKey(this.kmsCredentials); 25 | this.ethereumAddress = getEthereumAddress(key.PublicKey as Buffer); 26 | } 27 | return Promise.resolve(this.ethereumAddress); 28 | } 29 | 30 | async _signDigest(digestString: string): Promise { 31 | const digestBuffer = Buffer.from(ethers.utils.arrayify(digestString)); 32 | const sig = await requestKmsSignature(digestBuffer, this.kmsCredentials); 33 | const ethAddr = await this.getAddress(); 34 | const { v } = determineCorrectV(digestBuffer, sig.r, sig.s, ethAddr); 35 | return ethers.utils.joinSignature({ 36 | v, 37 | r: `0x${sig.r.toString("hex")}`, 38 | s: `0x${sig.s.toString("hex")}`, 39 | }); 40 | } 41 | 42 | async signMessage(message: string | ethers.utils.Bytes): Promise { 43 | return this._signDigest(ethers.utils.hashMessage(message)); 44 | } 45 | 46 | async signTransaction(transaction: ethers.utils.Deferrable): Promise { 47 | const unsignedTx = await ethers.utils.resolveProperties(transaction); 48 | const serializedTx = ethers.utils.serializeTransaction(unsignedTx); 49 | const transactionSignature = await this._signDigest(ethers.utils.keccak256(serializedTx)); 50 | return ethers.utils.serializeTransaction(unsignedTx, transactionSignature); 51 | } 52 | 53 | connect(provider: ethers.providers.Provider): AwsKmsSigner { 54 | return new AwsKmsSigner(this.kmsCredentials, provider); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/util/aws-kms-utils.test.ts: -------------------------------------------------------------------------------- 1 | import BN from "bn.js"; 2 | import { getEthereumAddress, determineCorrectV, findEthereumSig } from "./aws-kms-utils"; 3 | 4 | describe("getEthereumAddress", () => { 5 | test("should work correctly", () => { 6 | const samplePubKey = Buffer.from( 7 | "3056301006072a8648ce3d020106052b8104000a03420004f2de8ae7a9f594fb0d399abfb58639f43fb80960a1ed7c6e257c11e764d4759e1773a2c7ec7b913bec5d0e3a12bd7acd199f62e86de3f83b35bf6749fc1144ba", 8 | "hex" 9 | ); 10 | expect(getEthereumAddress(samplePubKey)).toBe("0xe94e130546485b928c9c9b9a5e69eb787172952e"); 11 | }); 12 | test("should fail on truncated key", () => { 13 | const samplePubKey = Buffer.from( 14 | "3056301006072a8648ce3d020106052b8104000a03420004f2de8ae7a9f594fb0d399abfb58639f43fb80960a1ed7c6e257c11", 15 | "hex" 16 | ); 17 | expect(() => getEthereumAddress(samplePubKey)).toThrow(); 18 | }); 19 | }); 20 | 21 | describe("findEthereumSig", () => { 22 | test("should work correctly", () => { 23 | const sampleSignature = Buffer.from( 24 | "304502203f25afdb7ed67094101cd71109261886db9abbf1ba20cc53aec20ba01c2e6baa022100ab0de6d40f8960c252fc6f21e35e8369126fb19033f10953c42a61766635df82", 25 | "hex" 26 | ); 27 | expect(JSON.stringify(findEthereumSig(sampleSignature))).toBe( 28 | '{"r":"3f25afdb7ed67094101cd71109261886db9abbf1ba20cc53aec20ba01c2e6baa","s":"54f2192bf0769f3dad0390de1ca17c95a83f2b567b5796e7fba7fd166a0061bf"}' 29 | ); 30 | }); 31 | }); 32 | 33 | describe("determineCorrectV", () => { 34 | test("should get correct V if it is 28", () => { 35 | const sampleMsg = Buffer.from("a1de988600a42c4b4ab089b619297c17d53cffae5d5120d82d8a92d0bb3b78f2", "hex"); 36 | const sampleR = new BN("fa754063b93a288b9a96883fc365efb9aee7ecaf632009baa04fe429e706d50e", 16); 37 | const sampleS = new BN("6a8971b06cd37b3da4ad04bb1298fda152a41e5c1104fd5d974d5c0a060a5e62", 16); 38 | const expectedAddr = "0xe94e130546485b928c9c9b9a5e69eb787172952e"; 39 | expect(determineCorrectV(sampleMsg, sampleR, sampleS, expectedAddr)).toMatchObject({ 40 | pubKey: "0xE94E130546485b928C9C9b9A5e69EB787172952e", 41 | v: 28, 42 | }); 43 | }); 44 | test("should get correct V if it is 27", () => { 45 | const sampleMsg = Buffer.from("a1de988600a42c4b4ab089b619297c17d53cffae5d5120d82d8a92d0bb3b78f2", "hex"); 46 | const sampleR = new BN("904d320777ceae0232282cbf6da3809a678541cdef7f4f3328242641ceecb0dc", 16); 47 | const sampleS = new BN("5b7f7afe18221049a1e176a89a60b6c10df8c0e838edb9b2f11ae1fb50a28271", 16); 48 | const expectedAddr = "0xe94e130546485b928c9c9b9a5e69eb787172952e"; 49 | expect(determineCorrectV(sampleMsg, sampleR, sampleS, expectedAddr)).toMatchObject({ 50 | pubKey: "0xE94E130546485b928C9C9b9A5e69EB787172952e", 51 | v: 27, 52 | }); 53 | }); 54 | 55 | test("should ??? if somethings are invalid", () => { 56 | const sampleMsg = Buffer.from("8600a42c4b4ab089b619297c17d53cffae5d5120d82d8a92d0bb3b78f2", "hex"); 57 | const sampleR = new BN("777ceae0232282cbf6da3809a678541cdef7f4f3328242641ceecb0dc", 16); 58 | const sampleS = new BN("5b7f7afe18221049a1e176a89a60b6c10df8c0e838edb9b2f11ae1fb50a28271", 16); 59 | const expectedAddr = "0xe94e130546485b928c9c9b9a5e69eb787172952e"; 60 | expect(() => determineCorrectV(sampleMsg, sampleR, sampleS, expectedAddr)).toThrowError(); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ethers-aws-kms-signer 2 | 3 | This is a wallet or signer that can be used together with [Ethers.js](https://github.com/ethers-io/ethers.js/) applications, using AWS KMS as the key storage. 4 | For GCP KMS look [here](https://github.com/openlawteam/ethers-gcp-kms-signer) 5 | 6 | ## Getting Started 7 | 8 | ```sh 9 | npm i ethers-aws-kms-signer 10 | ``` 11 | 12 | You can provide the AWS Credentials using the various ways listed [here](https://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/setting-credentials-node.html) depending on how you are using this library. You can also explicitly specify them when invoking the `AwsKmsSigner` constructor as shown below. 13 | 14 | ```js 15 | import { AwsKmsSigner } from "ethers-aws-kms-signer"; 16 | 17 | const kmsCredentials = { 18 | accessKeyId: "AKIAxxxxxxxxxxxxxxxx", // credentials for your IAM user with KMS access 19 | secretAccessKey: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", // credentials for your IAM user with KMS access 20 | region: "ap-southeast-1", 21 | keyId: "arn:aws:kms:ap-southeast-1:123456789012:key/123a1234-1234-4111-a1ab-a1abc1a12b12", 22 | }; 23 | 24 | const provider = ethers.providers.getDefaultProvider("ropsten"); 25 | let signer = new AwsKmsSigner(kmsCredentials); 26 | signer = signer.connect(provider); 27 | 28 | const tx = await signer.sendTransaction({ to: "0xE94E130546485b928C9C9b9A5e69EB787172952e", value: 1 }); 29 | console.log(tx); 30 | 31 | ``` 32 | 33 | # Developers 34 | ## Install 35 | 36 | `git clone` this repo 37 | 38 | ```sh 39 | $ git clone https://github.com/rjchow/nod my-module 40 | $ cd my-module 41 | $ rm -rf .git 42 | $ npm install # or yarn 43 | ``` 44 | 45 | Just make sure to edit `package.json`, `README.md` and `LICENSE` files accordingly with your module's info. 46 | 47 | ## Commands 48 | 49 | ```sh 50 | $ npm test # run tests with Jest 51 | $ npm run coverage # run tests with coverage and open it on browser 52 | $ npm run lint # lint code 53 | $ npm run build # generate docs and transpile code 54 | ``` 55 | 56 | ## Logging 57 | 58 | Turn on debugging by using the DEBUG environment variable for Node.js and using localStorage.debug in the browser. 59 | 60 | E.g: 61 | 62 | ```bash 63 | DEBUG="PLACEHOLDER_PROJECT_NAME:*" npm run dev 64 | ``` 65 | 66 | ## Commit message format 67 | 68 | This boiler plate uses the **semantic-release** package to manage versioning. Once it has been set up, version numbers and Github release changelogs will be automatically managed. **semantic-release** uses the commit messages to determine the type of changes in the codebase. Following formalized conventions for commit messages, **semantic-release** automatically determines the next [semantic version](https://semver.org) number, generates a changelog and publishes the release. 69 | 70 | Use `npm run commit` instead of `git commit` in order to invoke Commitizen commit helper that helps with writing properly formatted commit messages. 71 | 72 | 73 | ## License 74 | 75 | MIT © [RJ Chow](https://github.com/rjchow) 76 | 77 | 78 | # Credits 79 | 80 | Utmost credit goes to Lucas Henning for doing the legwork on parsing the AWS KMS signature and public key asn formats: https://luhenning.medium.com/the-dark-side-of-the-elliptic-curve-signing-ethereum-transactions-with-aws-kms-in-javascript-83610d9a6f81 81 | 82 | A significant portion of code was inspired by the work he published at https://github.com/lucashenning/aws-kms-ethereum-signing 83 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ethers-aws-kms-signer", 3 | "version": "1.3.2", 4 | "description": "An Ethers.js compatible signer that connects to AWS KMS", 5 | "license": "MIT", 6 | "repository": "rjchow/ethers-aws-kms-signer", 7 | "main": "dist/index.js", 8 | "author": { 9 | "name": "RJ Chow", 10 | "email": "me@rjchow.com", 11 | "url": "https://github.com/rjchow" 12 | }, 13 | "files": [ 14 | "dist", 15 | "src" 16 | ], 17 | "scripts": { 18 | "type-check": "tsc --noEmit", 19 | "test": "jest", 20 | "coverage": "npm test -- --coverage", 21 | "postcoverage": "open-cli coverage/lcov-report/index.html", 22 | "lint": "eslint . --ext js,ts,tsx", 23 | "lint:fix": "npm run lint -- --fix", 24 | "postdocs": "git add README.md", 25 | "clean": "rimraf dist", 26 | "prebuild": "npm run clean", 27 | "build": "tsc --emitDeclarationOnly && babel src -d dist --ignore src/**/*.spec.ts,src/**/*.test.ts -x .js,.ts,.tsx", 28 | "preversion": "npm run lint && npm test && npm run build", 29 | "semantic-release": "semantic-release", 30 | "commit": "git-cz", 31 | "commit:retry": "git-cz --retry", 32 | "commitmsg": "commitlint -e", 33 | "commitlint-circle": "commitlint-circle", 34 | "upgrade-deps": "npx updtr" 35 | }, 36 | "types": "dist/ts/src/index.d.ts", 37 | "husky": { 38 | "hooks": { 39 | "pre-commit": "lint-staged" 40 | } 41 | }, 42 | "lint-staged": { 43 | "*.{js,ts,tsx}": [ 44 | "eslint --fix --ext js,ts,tsx", 45 | "git add" 46 | ] 47 | }, 48 | "keywords": [ 49 | "generator-nod" 50 | ], 51 | "dependencies": { 52 | "asn1.js": "^5.4.1", 53 | "aws-sdk": "^2.922.0", 54 | "bn.js": "^5.2.0", 55 | "debug": "^4.3.1", 56 | "ethers": "^5.4.1" 57 | }, 58 | "devDependencies": { 59 | "@babel/cli": "^7.14.3", 60 | "@babel/core": "^7.14.3", 61 | "@babel/plugin-proposal-class-properties": "^7.13.0", 62 | "@babel/plugin-proposal-nullish-coalescing-operator": "^7.14.2", 63 | "@babel/plugin-proposal-optional-chaining": "^7.14.2", 64 | "@babel/preset-env": "^7.14.4", 65 | "@babel/preset-typescript": "^7.13.0", 66 | "@commitlint/cli": "^12.1.4", 67 | "@commitlint/config-conventional": "^12.1.4", 68 | "@commitlint/prompt": "^12.1.4", 69 | "@ls-age/commitlint-circle": "^1.0.0", 70 | "@types/asn1": "^0.2.0", 71 | "@types/debug": "^4.1.5", 72 | "@types/jest": "^26.0.23", 73 | "@typescript-eslint/eslint-plugin": "^4.26.0", 74 | "@typescript-eslint/parser": "^4.26.0", 75 | "babel-eslint": "^10.1.0", 76 | "babel-jest": "^27.0.2", 77 | "commitizen": "^4.2.4", 78 | "eslint": "^7.28.0", 79 | "eslint-config-airbnb-base": "^14.2.1", 80 | "eslint-config-prettier": "^8.3.0", 81 | "eslint-plugin-import": "^2.23.4", 82 | "eslint-plugin-prettier": "^3.4.0", 83 | "git-cz": "^4.7.6", 84 | "husky": "^6.0.0", 85 | "jest": "^27.0.4", 86 | "lint-staged": "^11.0.0", 87 | "open-cli": "^6.0.1", 88 | "prettier": "^2.3.1", 89 | "rimraf": "^3.0.2", 90 | "semantic-release": "^17.4.3", 91 | "semantic-release-npm-github-publish": "^1.4.0", 92 | "typescript": "^4.3.2" 93 | }, 94 | "publishConfig": { 95 | "access": "public" 96 | }, 97 | "config": { 98 | "commitizen": { 99 | "path": "node_modules/@commitlint/prompt" 100 | } 101 | }, 102 | "engines": { 103 | "node": ">= 10.18" 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/util/aws-kms-utils.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "ethers"; 2 | import { KMS } from "aws-sdk"; 3 | import * as asn1 from "asn1.js"; 4 | import BN from "bn.js"; 5 | import { AwsKmsSignerCredentials } from "../index"; 6 | 7 | /* this asn1.js library has some funky things going on */ 8 | /* eslint-disable func-names */ 9 | 10 | const EcdsaSigAsnParse: { decode: (asnStringBuffer: Buffer, format: "der") => { r: BN; s: BN } } = asn1.define( 11 | "EcdsaSig", 12 | function (this: any) { 13 | // parsing this according to https://tools.ietf.org/html/rfc3279#section-2.2.3 14 | this.seq().obj(this.key("r").int(), this.key("s").int()); 15 | } 16 | ); 17 | const EcdsaPubKey = asn1.define("EcdsaPubKey", function (this: any) { 18 | // parsing this according to https://tools.ietf.org/html/rfc5480#section-2 19 | this.seq().obj(this.key("algo").seq().obj(this.key("a").objid(), this.key("b").objid()), this.key("pubKey").bitstr()); 20 | }); 21 | /* eslint-enable func-names */ 22 | 23 | export async function sign(digest: Buffer, kmsCredentials: AwsKmsSignerCredentials) { 24 | const kms = new KMS(kmsCredentials); 25 | const params: KMS.SignRequest = { 26 | // key id or 'Alias/' 27 | KeyId: kmsCredentials.keyId, 28 | Message: digest, 29 | // 'ECDSA_SHA_256' is the one compatible with ECC_SECG_P256K1. 30 | SigningAlgorithm: "ECDSA_SHA_256", 31 | MessageType: "DIGEST", 32 | }; 33 | const res = await kms.sign(params).promise(); 34 | return res; 35 | } 36 | 37 | export async function getPublicKey(kmsCredentials: AwsKmsSignerCredentials) { 38 | const kms = new KMS(kmsCredentials); 39 | return kms 40 | .getPublicKey({ 41 | KeyId: kmsCredentials.keyId, 42 | }) 43 | .promise(); 44 | } 45 | 46 | export function getEthereumAddress(publicKey: Buffer): string { 47 | // The public key is ASN1 encoded in a format according to 48 | // https://tools.ietf.org/html/rfc5480#section-2 49 | // I used https://lapo.it/asn1js to figure out how to parse this 50 | // and defined the schema in the EcdsaPubKey object 51 | const res = EcdsaPubKey.decode(publicKey, "der"); 52 | let pubKeyBuffer: Buffer = res.pubKey.data; 53 | 54 | // The public key starts with a 0x04 prefix that needs to be removed 55 | // more info: https://www.oreilly.com/library/view/mastering-ethereum/9781491971932/ch04.html 56 | pubKeyBuffer = pubKeyBuffer.slice(1, pubKeyBuffer.length); 57 | 58 | const address = ethers.utils.keccak256(pubKeyBuffer); // keccak256 hash of publicKey 59 | const EthAddr = `0x${address.slice(-40)}`; // take last 20 bytes as ethereum adress 60 | return EthAddr; 61 | } 62 | 63 | export function findEthereumSig(signature: Buffer) { 64 | const decoded = EcdsaSigAsnParse.decode(signature, "der"); 65 | const { r, s } = decoded; 66 | 67 | const secp256k1N = new BN("fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141", 16); // max value on the curve 68 | const secp256k1halfN = secp256k1N.div(new BN(2)); // half of the curve 69 | // Because of EIP-2 not all elliptic curve signatures are accepted 70 | // the value of s needs to be SMALLER than half of the curve 71 | // i.e. we need to flip s if it's greater than half of the curve 72 | // if s is less than half of the curve, we're on the "good" side of the curve, we can just return 73 | return { r, s: s.gt(secp256k1halfN) ? secp256k1N.sub(s) : s }; 74 | } 75 | 76 | export async function requestKmsSignature(plaintext: Buffer, kmsCredentials: AwsKmsSignerCredentials) { 77 | const signature = await sign(plaintext, kmsCredentials); 78 | if (signature.$response.error || signature.Signature === undefined) { 79 | throw new Error(`AWS KMS call failed with: ${signature.$response.error}`); 80 | } 81 | return findEthereumSig(signature.Signature as Buffer); 82 | } 83 | 84 | function recoverPubKeyFromSig(msg: Buffer, r: BN, s: BN, v: number) { 85 | return ethers.utils.recoverAddress(`0x${msg.toString("hex")}`, { 86 | r: `0x${r.toString("hex")}`, 87 | s: `0x${s.toString("hex")}`, 88 | v, 89 | }); 90 | } 91 | 92 | export function determineCorrectV(msg: Buffer, r: BN, s: BN, expectedEthAddr: string) { 93 | // This is the wrapper function to find the right v value 94 | // There are two matching signatues on the elliptic curve 95 | // we need to find the one that matches to our public key 96 | // it can be v = 27 or v = 28 97 | let v = 27; 98 | let pubKey = recoverPubKeyFromSig(msg, r, s, v); 99 | if (pubKey.toLowerCase() !== expectedEthAddr.toLowerCase()) { 100 | // if the pub key for v = 27 does not match 101 | // it has to be v = 28 102 | v = 28; 103 | pubKey = recoverPubKeyFromSig(msg, r, s, v); 104 | } 105 | return { pubKey, v }; 106 | } 107 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [1.3.2](https://github.com/rjchow/ethers-aws-kms-signer/compare/v1.3.1...v1.3.2) (2021-07-14) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * ethers v5.4.1 ([a529e69](https://github.com/rjchow/ethers-aws-kms-signer/commit/a529e694fc904a7a6cdc2f680d45a6e65bdbc302)) 7 | 8 | 9 | ### Miscellaneous 10 | 11 | * EthersJS 5.4.0 releases support for EIP1559 new transaction format ([3975790](https://github.com/rjchow/ethers-aws-kms-signer/commit/397579071830940e93a0093f1dfdeddf5f6e5e0b)) 12 | 13 | ## [1.3.1](https://github.com/rjchow/ethers-aws-kms-signer/compare/v1.3.0...v1.3.1) (2021-07-12) 14 | 15 | 16 | ### Bug Fixes 17 | 18 | * fixing ethers version to 5.3.1 because it breaks this library with 5.4.1 ([71ce09b](https://github.com/rjchow/ethers-aws-kms-signer/commit/71ce09bf1a7092afb06cc5c71c561130d30321e9)) 19 | * fixing ethers version to 5.3.1 because it breaks this library with 5.4.1 ([bef444c](https://github.com/rjchow/ethers-aws-kms-signer/commit/bef444c31c80db1404d0bb7f155a9f49d8d7687b)) 20 | 21 | # [1.3.0](https://github.com/rjchow/ethers-aws-kms-signer/compare/v1.2.0...v1.3.0) (2021-06-07) 22 | 23 | 24 | ### Features 25 | 26 | * use carat for dependencies ([a5618da](https://github.com/rjchow/ethers-aws-kms-signer/commit/a5618dab2076275518d95900780ff04d904293a7)) 27 | * use carat for dependencies ([54a143a](https://github.com/rjchow/ethers-aws-kms-signer/commit/54a143ab07af80e1a7c59922d5fccb8e52659144)) 28 | 29 | # [1.2.0](https://github.com/rjchow/ethers-aws-kms-signer/compare/v1.1.0...v1.2.0) (2021-06-04) 30 | 31 | 32 | ### Features 33 | 34 | * allow aws credentials to be provided in other ways ([a08ed1f](https://github.com/rjchow/ethers-aws-kms-signer/commit/a08ed1f7b84b97df3bb484f3206e5e10ecd49dae)) 35 | * allow aws credentials to be provided in other ways ([40ef566](https://github.com/rjchow/ethers-aws-kms-signer/commit/40ef566f7a5099e5e8b00d7a7c1e51a62285286b)) 36 | 37 | # [1.1.0](https://github.com/rjchow/ethers-aws-kms-signer/compare/v1.0.0...v1.1.0) (2021-03-03) 38 | 39 | 40 | ### Features 41 | 42 | * fixed exported ts declaration path ([69d29b6](https://github.com/rjchow/ethers-aws-kms-signer/commit/69d29b66a99f9bc1c14157a3fd5b63cc420e84a2)) 43 | * force release ([01b44a4](https://github.com/rjchow/ethers-aws-kms-signer/commit/01b44a408e3dd77708717419c681ad1230bfb3f2)) 44 | 45 | # 1.0.0 (2021-02-03) 46 | 47 | 48 | ### Features 49 | 50 | * release ([25d97cb](https://github.com/rjchow/ethers-aws-kms-signer/commit/25d97cb3a4e34f7c16e26798ffd889e6f953f758)) 51 | 52 | 53 | ### Miscellaneous 54 | 55 | * init ([d4bc063](https://github.com/rjchow/ethers-aws-kms-signer/commit/d4bc0631df3485f0b238fa1d45ff8062d7f78cbd)) 56 | 57 | ## [1.9.3](https://github.com/rjchow/nod/compare/v1.9.2...v1.9.3) (2021-01-25) 58 | 59 | 60 | ### Chores 61 | 62 | * update dependencies ([#346](https://github.com/rjchow/nod/issues/346)) ([0e54596](https://github.com/rjchow/nod/commit/0e54596741a9c41e0320dc6f52aaaa1a9f64ed64)) 63 | 64 | ## [1.9.2](https://github.com/rjchow/nod/compare/v1.9.1...v1.9.2) (2021-01-06) 65 | 66 | 67 | ### Chores 68 | 69 | * update dependencies ([#340](https://github.com/rjchow/nod/issues/340)) ([93f9dc4](https://github.com/rjchow/nod/commit/93f9dc47ec377f5b197348073c7d56a8fb5684f1)) 70 | 71 | ## [1.9.1](https://github.com/rjchow/nod/compare/v1.9.0...v1.9.1) (2020-12-29) 72 | 73 | 74 | ### Chores 75 | 76 | * update dependencies ([#333](https://github.com/rjchow/nod/issues/333)) ([dbf8daf](https://github.com/rjchow/nod/commit/dbf8daf553d5ec6d68d1ce06bbd05d5c4a3e2b8a)) 77 | 78 | # [1.9.0](https://github.com/rjchow/nod/compare/v1.8.3...v1.9.0) (2020-12-24) 79 | 80 | 81 | ### Features 82 | 83 | * 🎸 removed default export ([733bee7](https://github.com/rjchow/nod/commit/733bee790e339154e1d2dde34cd952934d7e8800)) 84 | 85 | ## [1.8.3](https://github.com/rjchow/nod/compare/v1.8.2...v1.8.3) (2020-12-22) 86 | 87 | 88 | ### Chores 89 | 90 | * fix update deps commit message ([412421a](https://github.com/rjchow/nod/commit/412421a8902c8c4672a7264d0abbad01763b4976)) 91 | 92 | ## [1.8.2](https://github.com/rjchow/nod/compare/v1.8.1...v1.8.2) (2020-12-15) 93 | 94 | 95 | ### Bug Fixes 96 | 97 | * changed updtr command to allow usage in ci ([4a92313](https://github.com/rjchow/nod/commit/4a9231377151055767c32179b57b9bd8aa52fc06)) 98 | 99 | 100 | ### Miscellaneous 101 | 102 | * Rename update-deps to update-deps.yml ([2d80e61](https://github.com/rjchow/nod/commit/2d80e61881aac084be065cba057854a5599156e8)) 103 | * Create update-deps ([a7fdec3](https://github.com/rjchow/nod/commit/a7fdec34dd6a3f9ea09cb59a3ba9695291527deb)) 104 | 105 | ## [1.8.1](https://github.com/rjchow/nod/compare/v1.8.0...v1.8.1) (2020-10-18) 106 | 107 | 108 | ### Chores 109 | 110 | * update deps ([dd89919](https://github.com/rjchow/nod/commit/dd89919525e56f5eb605c8700c10e0e3f787e830)) 111 | 112 | # [1.8.0](https://github.com/rjchow/nod/compare/v1.7.0...v1.8.0) (2020-05-18) 113 | 114 | 115 | ### Features 116 | 117 | * added command to upgrade deps ([#223](https://github.com/rjchow/nod/issues/223)) ([a052392](https://github.com/rjchow/nod/commit/a0523927832b8d34c5b9ae77adabe3a445aec0e7)) 118 | 119 | # [1.7.0](https://github.com/rjchow/nod/compare/v1.6.3...v1.7.0) (2020-05-18) 120 | 121 | 122 | ### Features 123 | 124 | * major versions upgrade ([#222](https://github.com/rjchow/nod/issues/222)) ([de8c831](https://github.com/rjchow/nod/commit/de8c831e599488c64bede2d3c709f1b04c3a4aba)) 125 | 126 | ## [1.6.3](https://github.com/rjchow/nod/compare/v1.6.2...v1.6.3) (2020-01-06) 127 | 128 | 129 | ### Bug Fixes 130 | 131 | * 🐛 corrected exported types path in package.json ([#134](https://github.com/rjchow/nod/issues/134)) ([829d7b3](https://github.com/rjchow/nod/commit/829d7b3abf3ccf8f9b37f7c11dbf38ee807a66d3)) 132 | 133 | ## [1.6.2](https://github.com/rjchow/nod/compare/v1.6.1...v1.6.2) (2019-12-31) 134 | 135 | 136 | ### Chores 137 | 138 | * upgrade dependencies ([#139](https://github.com/rjchow/nod/issues/139)) ([3f6654e](https://github.com/rjchow/nod/commit/3f6654e66aa71f9e69578f683fb47bf0c279fcda)) 139 | 140 | ## [1.6.1](https://github.com/rjchow/nod/compare/v1.6.0...v1.6.1) (2019-12-05) 141 | 142 | 143 | ### Chores 144 | 145 | * **deps-dev:** bump prettier from 1.18.2 to 1.19.1 ([#121](https://github.com/rjchow/nod/issues/121)) ([f9a51a9](https://github.com/rjchow/nod/commit/f9a51a93139dbb61152b3568550443279967e7a3)) 146 | 147 | # [1.6.0](https://github.com/rjchow/nod/compare/v1.5.0...v1.6.0) (2019-12-05) 148 | 149 | 150 | ### Chores 151 | 152 | * **deps:** [security] bump eslint-utils from 1.3.1 to 1.4.3 ([#102](https://github.com/rjchow/nod/issues/102)) ([e8982ed](https://github.com/rjchow/nod/commit/e8982edb1ec0c3cb2e5a971bd033737a637b31e9)) 153 | * **deps:** [security] bump mixin-deep from 1.3.1 to 1.3.2 ([#79](https://github.com/rjchow/nod/issues/79)) ([1da45f7](https://github.com/rjchow/nod/commit/1da45f74ba2de4d425a5a57c97f83c41cac6bb30)) 154 | * **deps:** [security] bump tar from 2.2.1 to 2.2.2 ([#68](https://github.com/rjchow/nod/issues/68)) ([f5b1123](https://github.com/rjchow/nod/commit/f5b1123031eea56e31ca5d1b082c7515906488b3)) 155 | * **deps-dev:** bump @babel/core from 7.4.5 to 7.7.2 ([#112](https://github.com/rjchow/nod/issues/112)) ([f0e8e28](https://github.com/rjchow/nod/commit/f0e8e28c34430de2f52771e2e25c2cfe5a1b7d9b)) 156 | * **deps-dev:** bump @babel/preset-env from 7.4.5 to 7.7.1 ([#110](https://github.com/rjchow/nod/issues/110)) ([4ef4bbe](https://github.com/rjchow/nod/commit/4ef4bbe622f2062fb2480f4cd83863e676e89e51)) 157 | * **deps-dev:** bump @types/jest from 24.0.15 to 24.0.23 ([#115](https://github.com/rjchow/nod/issues/115)) ([686f2f2](https://github.com/rjchow/nod/commit/686f2f29d521ff27ab9e9cee12e982433bacf50b)) 158 | * **deps-dev:** bump eslint from 6.0.0 to 6.6.0 ([39be3df](https://github.com/rjchow/nod/commit/39be3dff033552907838cbd2a658ae1dcc0ef68e)) 159 | * **deps-dev:** bump eslint from 6.0.0 to 6.6.0 ([f379f3f](https://github.com/rjchow/nod/commit/f379f3f9553e2a4024641be5746d30b2653f5092)) 160 | * **deps-dev:** bump eslint-plugin-import from 2.17.3 to 2.18.2 ([#58](https://github.com/rjchow/nod/issues/58)) ([cf990fa](https://github.com/rjchow/nod/commit/cf990fa505d98c19765165487f5df3da40f2275c)) 161 | * **deps-dev:** bump husky from 2.4.1 to 3.1.0 ([#118](https://github.com/rjchow/nod/issues/118)) ([4d8abce](https://github.com/rjchow/nod/commit/4d8abce43d646c7b41740699fa7a597d02c15d30)) 162 | * **deps-dev:** bump lint-staged from 9.2.0 to 9.4.3 ([a35c989](https://github.com/rjchow/nod/commit/a35c98935577da520d96bdef20f98bb740c1fff0)) 163 | * **deps-dev:** bump lint-staged from 9.2.0 to 9.4.3 ([fc12f18](https://github.com/rjchow/nod/commit/fc12f189a3a7d01de90de80c2d0c609aa6274c1d)) 164 | * **deps-dev:** bump semantic-release from 15.13.18 to 15.13.31 ([2b55c94](https://github.com/rjchow/nod/commit/2b55c9474e19f7ea23df95a21f185429371c435b)) 165 | * **deps-dev:** bump semantic-release from 15.13.18 to 15.13.31 ([264472e](https://github.com/rjchow/nod/commit/264472e74d7ed5ac76cc82e5485ed796cddb0f99)) 166 | * **dev-deps:** npm audit ([a5a7dfc](https://github.com/rjchow/nod/commit/a5a7dfcd3d825bb9c110c26e31391777a14a74b1)) 167 | 168 | 169 | ### Features 170 | 171 | * 🎸 Added semantic release configuration for npm release ([#131](https://github.com/rjchow/nod/issues/131)) ([cee2d64](https://github.com/rjchow/nod/commit/cee2d645cabff1f1b6ba50635e8fc2e993dd5ef8)) 172 | --------------------------------------------------------------------------------