Skip to content

Commit

Permalink
init
Browse files Browse the repository at this point in the history
  • Loading branch information
hara-re-engines committed Jan 31, 2023
0 parents commit 03fe0ae
Show file tree
Hide file tree
Showing 10 changed files with 3,908 additions and 0 deletions.
36 changes: 36 additions & 0 deletions .github/workflows/publish-package.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: publish package
on:
push:
branches:
- main

jobs:
publish:
runs-on: ubuntu-latest
steps:
- name: checkout
uses: actions/checkout@v3
- name: setup
id: node
uses: actions/setup-node@v3
with:
node-version: "18"
registry-url: https://npm.pkg.github.com
scope: "@microverse-dev"
cache: yarn
cache-dependency-path: "**/yarn.lock"
- name: yarn install
run: yarn --frozen-lockfile
- name: lint
uses: reviewdog/action-eslint@v1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
eslint_flags: '--max-warnings=0 src/**/{*.js,*.jsx,*.ts,*.tsx,}'
fail_on_error: 'true'
reporter: github-pr-review
- name: build
run: yarn build
- name: publish
run: yarn publish
env:
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
11 changes: 11 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
node_modules
dist

### macOS ###
# General
.DS_Store
.AppleDouble
.LSOverride

# Ignores the whole .idea folder and all .iml files
.idea/
26 changes: 26 additions & 0 deletions .prettierrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"overrides": [
{
"files": "*.ts",
"options": {
"parser": "typescript",
"singleQuote": true,
"trailingComma": "all",
"tabWidth": 2,
"printWidth": 100,
"arrowParens": "avoid"
}
},
{
"files": "*.sol",
"options": {
"printWidth": 100,
"tabWidth": 2,
"useTabs": false,
"singleQuote": false,
"bracketSpacing": false,
"compiler": "0.8.17"
}
}
]
}
7 changes: 7 additions & 0 deletions @types/asn1.js/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
declare module 'asn1.js' {
export class AsnObject<T> {
public decode(body: Uint8Array | string | Buffer, type: string): T;
}

function define<T>(name: string, creator: () => void): AsnObject<T>;
}
42 changes: 42 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"name": "@microverse-dev/hardhat-gcp-kms-signer",
"version": "0.1.0",
"description": "Hardhat plugin for GCP KMS Signer",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"author": "[email protected]",
"license": "MIT",
"repository": {
"type": "git",
"url": "[email protected]:microverse-dev/hardhat-gcp-kms-signer.git"
},
"publishConfig": {
"registry": "https://npm.pkg.github.com/",
"access": "restricted"
},
"files": [
"dist"
],
"dependencies": {
"@google-cloud/kms": "3.3.0",
"asn1.js": "5.4.1",
"bn.js": "5.2.1",
"ethers": "5.7.2",
"key-encoder": "2.0.3"
},
"devDependencies": {
"@trivago/prettier-plugin-sort-imports": "4.0.0",
"@types/node": "18.11.18",
"hardhat": "2.12.6",
"prettier": "2.8.3",
"ts-node": "10.9.1",
"typescript": "4.9.5"
},
"peerDependencies": {
"@google-cloud/kms": "3.0.1",
"hardhat": "2.12.0"
},
"scripts": {
"build": "tsc"
}
}
56 changes: 56 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { KMSSigner } from './signer';
import './type-extensions';
import { extendConfig, extendEnvironment, HardhatUserConfig } from 'hardhat/config';
import { BackwardsCompatibilityProviderAdapter } from 'hardhat/internal/core/providers/backwards-compatibility';
import { ChainIdValidatorProvider } from 'hardhat/internal/core/providers/chainId';
import {
AutomaticGasPriceProvider,
AutomaticGasProvider,
} from 'hardhat/internal/core/providers/gas-providers';
import { HttpProvider } from 'hardhat/internal/core/providers/http';
import { lazyObject } from 'hardhat/plugins';
import {
EIP1193Provider,
HardhatConfig,
HardhatRuntimeEnvironment,
HttpNetworkUserConfig,
} from 'hardhat/types';

extendConfig((config: HardhatConfig, userConfig: Readonly<HardhatUserConfig>) => {
const userNetworks = userConfig.networks;
if (userNetworks == null) {
return;
}

for (const networkName in userNetworks) {
if (networkName === 'hardhat') {
continue;
}
const network = userNetworks[networkName]!;
if (network.kmsResourceName) {
config.networks[networkName].kmsResourceName = network.kmsResourceName;
}
}
});

extendEnvironment((hre: HardhatRuntimeEnvironment) => {
if (hre.network.name !== 'hardhat' && hre.network.config.kmsResourceName) {
hre.network.provider = lazyObject(() => {
const httpNetConfig = hre.network.config as HttpNetworkUserConfig;
const httpProvider = new HttpProvider(
httpNetConfig.url!,
hre.network.name,
httpNetConfig.httpHeaders,
httpNetConfig.timeout,
);

let wrappedProvider: EIP1193Provider;
wrappedProvider = new KMSSigner(httpProvider, hre.network.config.kmsResourceName!);
wrappedProvider = new AutomaticGasProvider(wrappedProvider, hre.network.config.gasMultiplier);
wrappedProvider = new AutomaticGasPriceProvider(wrappedProvider);
wrappedProvider = new ChainIdValidatorProvider(wrappedProvider, hre.network.config.chainId!);

return new BackwardsCompatibilityProviderAdapter(wrappedProvider);
});
}
});
149 changes: 149 additions & 0 deletions src/signer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import { KeyManagementServiceClient } from '@google-cloud/kms';
import * as asn1 from 'asn1.js';
import * as BN from 'bn.js';
import { BigNumber, utils } from 'ethers';
import { rpcTransactionRequest } from 'hardhat/internal/core/jsonrpc/types/input/transactionRequest';
import { validateParams } from 'hardhat/internal/core/jsonrpc/types/input/validation';
import { ProviderWrapperWithChainId } from 'hardhat/internal/core/providers/chainId';
import { EIP1193Provider, RequestArguments } from 'hardhat/types';
import KeyEncoder from 'key-encoder';

const EcdsaSigAsnParse: { decode: (asnStringBuffer: Buffer, format: 'der') => { r: BN; s: BN } } =
asn1.define('EcdsaSig', function (this: any) {
this.seq().obj(this.key('r').int(), this.key('s').int());
});

const EcdsaPubKey = asn1.define('EcdsaPubKey', function (this: any) {
this.seq().obj(
this.key('algo').seq().obj(this.key('a').objid(), this.key('b').objid()),
this.key('pubKey').bitstr(),
);
});

export class KMSSigner extends ProviderWrapperWithChainId {
public kmsResourceName: string;
public ethAddress?: string;

constructor(provider: EIP1193Provider, kmsResourceName: string) {
super(provider);
this.kmsResourceName = kmsResourceName;
}

public async request(args: RequestArguments) {
const method = args.method;
const params = this._getParams(args);
const senderAddress = await this._getSenderAddress();
if (method === 'eth_sendTransaction') {
const [txRequest] = validateParams(params, rpcTransactionRequest);
const baseTx = await utils.resolveProperties(txRequest);

const unsignedTx: utils.UnsignedTransaction = {
chainId: await this._getChainId(),
data: baseTx.data,
gasLimit: baseTx.gas,
gasPrice: baseTx.gasPrice,
nonce: Number(baseTx.nonce ? baseTx.nonce : await this._getNonce(senderAddress)),
type: 2,
to: baseTx.to && toHexString(baseTx.to),
value: baseTx.value && BigNumber.from(baseTx.value),
maxFeePerGas: baseTx.maxFeePerGas?.toString(),
maxPriorityFeePerGas: baseTx.maxPriorityFeePerGas?.toString(),
};

// XXX: eip1193には未対応のため、type=0を明示する。
if (unsignedTx.maxFeePerGas === undefined && unsignedTx.maxPriorityFeePerGas === undefined) {
unsignedTx.type = 0;
delete unsignedTx.maxFeePerGas;
delete unsignedTx.maxPriorityFeePerGas;
}

const serializedTx = utils.serializeTransaction(unsignedTx as utils.UnsignedTransaction);
const signature = await this._signDigest(utils.keccak256(serializedTx));

return this._wrappedProvider.request({
method: 'eth_sendRawTransaction',
params: [utils.serializeTransaction(unsignedTx as utils.UnsignedTransaction, signature)],
});
} else if (method === 'eth_accounts' || method === 'eth_requestAccounts') {
return [senderAddress];
}

return this._wrappedProvider.request(args);
}

private async _getSenderAddress(): Promise<string> {
if (!this.ethAddress) {
this.ethAddress = await requestAddressFromKMS(this.kmsResourceName);
}
return this.ethAddress;
}

private async _getNonce(address: string): Promise<bigint> {
const response = (await this._wrappedProvider.request({
method: 'eth_getTransactionCount',
params: [address, 'pending'],
})) as any;

return BigInt(response);
}

private async _signDigest(digestString: string): Promise<string> {
const digestBuffer = Buffer.from(utils.arrayify(digestString));
const { r, s } = await requestSignatureFromKMS(digestBuffer, this.kmsResourceName);
const senderAddress = await this._getSenderAddress();
for (let recoveryParam = 0; recoveryParam < 2; recoveryParam++) {
const address = utils.recoverAddress(digestBuffer, { r, s, recoveryParam }).toLowerCase();
if (address === senderAddress) {
return utils.joinSignature({ r, s, v: recoveryParam });
}
}

throw new Error(`Failed to calculate recovery param: ${senderAddress}`);
}
}

const secp256k1N = new BN('fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141', 16);
const secp256k1halfN = secp256k1N.div(new BN(2));

const keyEncoder = new KeyEncoder('secp256k1');

const requestAddressFromKMS = async (kmsResourceName: string) => {
const kms = new KeyManagementServiceClient();
const [kmsPublicKey] = await kms.getPublicKey({ name: kmsResourceName });

if (!kmsPublicKey || !kmsPublicKey.pem) throw new Error(`Can not find key: ${kmsResourceName}`);

const block = keyEncoder.encodePublic(kmsPublicKey.pem, 'pem', 'der');
const res = EcdsaPubKey.decode(Buffer.from(block, 'hex'), 'der') as any;
const pubKeyBuffer: Buffer = res.pubKey.data;

const publicKey = pubKeyBuffer.subarray(1, pubKeyBuffer.length);

return `0x${utils.keccak256(publicKey).slice(-40)}`;
};

export async function requestSignatureFromKMS(digest: Buffer, kmsResourceName: string) {
const kms = new KeyManagementServiceClient();
const [asymmetricSignResponse] = await kms.asymmetricSign({
name: kmsResourceName,
digest: {
sha256: digest,
},
});

if (!asymmetricSignResponse || !asymmetricSignResponse.signature) {
throw new Error(`GCP KMS call failed`);
}

const { r, s } = EcdsaSigAsnParse.decode(asymmetricSignResponse.signature as Buffer, 'der');

return { r: toHexString(r)!, s: toHexString(s.gt(secp256k1halfN) ? secp256k1N.sub(s) : s) };
}

export function toHexString(value: BN | Buffer | undefined): string | undefined {
if (value == null) {
return;
}

return `0x${value.toString('hex')}`;
}
19 changes: 19 additions & 0 deletions src/type-extensions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import 'hardhat/types/config';

declare module 'hardhat/types/config' {
export interface HttpNetworkUserConfig {
kmsResourceName?: string;
}

export interface HardhatNetworkUserConfig {
kmsResourceName?: string;
}

export interface HttpNetworkConfig {
kmsResourceName?: string;
}

export interface HardhatNetworkConfig {
kmsResourceName?: string;
}
}
16 changes: 16 additions & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "esnext",
"module": "commonjs",
"moduleResolution": "node",
"declaration": true,
"pretty": true,
"outDir": "./dist",
"strict": true,
"skipLibCheck": true,
"typeRoots": [
"@types"
]
},
"include": ["src/*"],
}
Loading

0 comments on commit 03fe0ae

Please sign in to comment.