-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 03fe0ae
Showing
10 changed files
with
3,908 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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>; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
} | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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')}`; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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/*"], | ||
} |
Oops, something went wrong.