evm-kms-signer 1.0.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +69 -10
- package/dist/account.d.ts +29 -0
- package/dist/account.d.ts.map +1 -1
- package/dist/account.js +40 -0
- package/dist/account.js.map +1 -1
- package/dist/gcp/client.d.ts +91 -0
- package/dist/gcp/client.d.ts.map +1 -0
- package/dist/gcp/client.js +179 -0
- package/dist/gcp/client.js.map +1 -0
- package/dist/gcp/signer.d.ts +212 -0
- package/dist/gcp/signer.d.ts.map +1 -0
- package/dist/gcp/signer.js +296 -0
- package/dist/gcp/signer.js.map +1 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/types/index.d.ts +30 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +8 -3
package/README.md
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
# evm-kms-signer
|
|
2
2
|
|
|
3
|
-
A TypeScript library that integrates AWS KMS (Key Management Service) with [viem](https://viem.sh) to create secure Ethereum signers. This allows you to sign Ethereum transactions and messages using keys stored in AWS KMS, providing enterprise-grade security for your Ethereum operations.
|
|
3
|
+
A TypeScript library that integrates AWS/GCP KMS (Key Management Service) with [viem](https://viem.sh) to create secure Ethereum signers. This allows you to sign Ethereum transactions and messages using keys stored in AWS or GCP KMS, providing enterprise-grade security for your Ethereum operations.
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
7
|
- **AWS KMS Integration**: Sign Ethereum transactions using keys securely stored in AWS KMS
|
|
8
|
+
- **GCP KMS Support**: Also supports Google Cloud Platform KMS for multi-cloud deployments
|
|
8
9
|
- **Full EIP Compliance**: Supports EIP-191 (personal messages), EIP-712 (typed data), EIP-155 (replay protection), EIP-2 (signature normalization)
|
|
9
10
|
- **Type-Safe**: Built with TypeScript in strict mode with comprehensive type definitions
|
|
10
11
|
- **viem Compatible**: Seamlessly integrates with viem's Account system via `toAccount`
|
|
11
|
-
- **DER Signature Parsing**: Automatically converts AWS KMS DER-encoded signatures to Ethereum format
|
|
12
|
+
- **DER Signature Parsing**: Automatically converts AWS/GCP KMS DER-encoded signatures to Ethereum format
|
|
12
13
|
- **Comprehensive Error Handling**: Custom error classes for better debugging
|
|
13
|
-
- **Well-Tested**:
|
|
14
|
+
- **Well-Tested**: 169 tests covering all functionality with 100% type safety
|
|
14
15
|
|
|
15
16
|
## Installation
|
|
16
17
|
|
|
@@ -30,9 +31,11 @@ Or with yarn:
|
|
|
30
31
|
yarn add evm-kms-signer
|
|
31
32
|
```
|
|
32
33
|
|
|
33
|
-
##
|
|
34
|
+
## Usage
|
|
34
35
|
|
|
35
|
-
### AWS KMS
|
|
36
|
+
### AWS KMS
|
|
37
|
+
|
|
38
|
+
#### Prerequisites
|
|
36
39
|
|
|
37
40
|
1. **Create an ECC Key in AWS KMS**:
|
|
38
41
|
- Go to AWS KMS Console
|
|
@@ -50,7 +53,7 @@ yarn add evm-kms-signer
|
|
|
50
53
|
3. **Note Your Key ID**:
|
|
51
54
|
Copy the Key ARN or Key ID for use in your application.
|
|
52
55
|
|
|
53
|
-
|
|
56
|
+
#### Environment Variables
|
|
54
57
|
|
|
55
58
|
Create a `.env` file in your project root:
|
|
56
59
|
|
|
@@ -63,9 +66,7 @@ AWS_ACCESS_KEY_ID=your_access_key
|
|
|
63
66
|
AWS_SECRET_ACCESS_KEY=your_secret_key
|
|
64
67
|
```
|
|
65
68
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
### Signing a Message
|
|
69
|
+
#### Basic Usage
|
|
69
70
|
|
|
70
71
|
```typescript
|
|
71
72
|
import 'dotenv/config'
|
|
@@ -93,7 +94,7 @@ async function main() {
|
|
|
93
94
|
main().catch(console.error)
|
|
94
95
|
```
|
|
95
96
|
|
|
96
|
-
|
|
97
|
+
#### Use with viem
|
|
97
98
|
|
|
98
99
|
```typescript
|
|
99
100
|
import 'dotenv/config'
|
|
@@ -130,6 +131,63 @@ async function main() {
|
|
|
130
131
|
main().catch(console.error)
|
|
131
132
|
```
|
|
132
133
|
|
|
134
|
+
### GCP KMS
|
|
135
|
+
|
|
136
|
+
#### Prerequisites
|
|
137
|
+
|
|
138
|
+
1. **Create a KMS key ring and crypto key in GCP Console**:
|
|
139
|
+
- Go to Google Cloud Console → Security → Key Management
|
|
140
|
+
- Create a new key ring in your desired location
|
|
141
|
+
- Create a crypto key with purpose "Asymmetric sign"
|
|
142
|
+
- Choose **Elliptic Curve P-256 - SHA256 Digest** algorithm (secp256k1 for Ethereum)
|
|
143
|
+
|
|
144
|
+
2. **Grant Permissions**:
|
|
145
|
+
Grant `roles/cloudkms.cryptoKeySignerVerifier` permission to your service account:
|
|
146
|
+
```bash
|
|
147
|
+
gcloud kms keys add-iam-policy-binding KEY_ID \
|
|
148
|
+
--location=LOCATION \
|
|
149
|
+
--keyring=KEYRING_ID \
|
|
150
|
+
--member=serviceAccount:SERVICE_ACCOUNT_EMAIL \
|
|
151
|
+
--role=roles/cloudkms.cryptoKeySignerVerifier
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
3. **Set up authentication**:
|
|
155
|
+
- Set `GOOGLE_APPLICATION_CREDENTIALS` environment variable pointing to your service account key file, or
|
|
156
|
+
- Pass `keyFilename` in config
|
|
157
|
+
|
|
158
|
+
#### Basic Usage
|
|
159
|
+
|
|
160
|
+
```typescript
|
|
161
|
+
import { GcpSigner } from 'evm-kms-signer';
|
|
162
|
+
|
|
163
|
+
const signer = new GcpSigner({
|
|
164
|
+
projectId: 'your-project-id',
|
|
165
|
+
locationId: 'global',
|
|
166
|
+
keyRingId: 'your-keyring-id',
|
|
167
|
+
keyId: 'your-key-id',
|
|
168
|
+
keyVersion: '1',
|
|
169
|
+
keyFilename: '/path/to/service-account-key.json', // optional
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
const address = await signer.getAddress();
|
|
173
|
+
const signature = await signer.signMessage({ message: 'Hello!' });
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
#### Use with viem
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
import { createWalletClient, http } from 'viem';
|
|
180
|
+
import { mainnet } from 'viem/chains';
|
|
181
|
+
import { toGcpKmsAccount } from 'evm-kms-signer';
|
|
182
|
+
|
|
183
|
+
const account = await toGcpKmsAccount(signer);
|
|
184
|
+
const client = createWalletClient({
|
|
185
|
+
account,
|
|
186
|
+
chain: mainnet,
|
|
187
|
+
transport: http(),
|
|
188
|
+
});
|
|
189
|
+
```
|
|
190
|
+
|
|
133
191
|
## API Documentation
|
|
134
192
|
|
|
135
193
|
### `KmsSigner`
|
|
@@ -302,4 +360,5 @@ Contributions are welcome! Please feel free to submit a Pull Request.
|
|
|
302
360
|
|
|
303
361
|
- Built with [viem](https://viem.sh) - Modern TypeScript Ethereum library
|
|
304
362
|
- Uses [AWS SDK for JavaScript v3](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/)
|
|
363
|
+
- Uses [Google Cloud KMS Client Library](https://cloud.google.com/nodejs/docs/reference/kms/latest)
|
|
305
364
|
- Inspired by the need for secure key management in Ethereum applications
|
package/dist/account.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { LocalAccount } from 'viem';
|
|
2
|
+
import type { GcpSigner } from './gcp/signer';
|
|
2
3
|
import type { KmsSigner } from './kms/signer';
|
|
3
4
|
/**
|
|
4
5
|
* Create a viem Account from KmsSigner.
|
|
@@ -22,4 +23,32 @@ import type { KmsSigner } from './kms/signer';
|
|
|
22
23
|
* ```
|
|
23
24
|
*/
|
|
24
25
|
export declare function toKmsAccount(signer: KmsSigner): Promise<LocalAccount>;
|
|
26
|
+
/**
|
|
27
|
+
* Create a viem Account from GcpSigner.
|
|
28
|
+
*
|
|
29
|
+
* Wraps GcpSigner methods to match viem's Account interface,
|
|
30
|
+
* enabling use with viem clients (walletClient, etc.)
|
|
31
|
+
*
|
|
32
|
+
* @param signer - GcpSigner instance
|
|
33
|
+
* @returns viem Account with GCP KMS signing capabilities
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```typescript
|
|
37
|
+
* const signer = new GcpSigner({
|
|
38
|
+
* projectId: 'my-project',
|
|
39
|
+
* locationId: 'global',
|
|
40
|
+
* keyRingId: 'my-keyring',
|
|
41
|
+
* keyId: 'my-key',
|
|
42
|
+
* keyVersion: '1'
|
|
43
|
+
* })
|
|
44
|
+
* const account = await toGcpKmsAccount(signer)
|
|
45
|
+
*
|
|
46
|
+
* const client = createWalletClient({
|
|
47
|
+
* account,
|
|
48
|
+
* chain: mainnet,
|
|
49
|
+
* transport: http()
|
|
50
|
+
* })
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
export declare function toGcpKmsAccount(signer: GcpSigner): Promise<LocalAccount>;
|
|
25
54
|
//# sourceMappingURL=account.d.ts.map
|
package/dist/account.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"account.d.ts","sourceRoot":"","sources":["../src/account.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,MAAM,CAAC;AAEzC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAE9C;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,YAAY,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,YAAY,CAAC,CAe3E"}
|
|
1
|
+
{"version":3,"file":"account.d.ts","sourceRoot":"","sources":["../src/account.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,MAAM,CAAC;AAEzC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAE9C;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,YAAY,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,YAAY,CAAC,CAe3E;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAsB,eAAe,CACpC,MAAM,EAAE,SAAS,GACf,OAAO,CAAC,YAAY,CAAC,CAevB"}
|
package/dist/account.js
CHANGED
|
@@ -33,4 +33,44 @@ export async function toKmsAccount(signer) {
|
|
|
33
33
|
signTypedData: async (typedData) => signer.signTypedData(typedData),
|
|
34
34
|
});
|
|
35
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Create a viem Account from GcpSigner.
|
|
38
|
+
*
|
|
39
|
+
* Wraps GcpSigner methods to match viem's Account interface,
|
|
40
|
+
* enabling use with viem clients (walletClient, etc.)
|
|
41
|
+
*
|
|
42
|
+
* @param signer - GcpSigner instance
|
|
43
|
+
* @returns viem Account with GCP KMS signing capabilities
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```typescript
|
|
47
|
+
* const signer = new GcpSigner({
|
|
48
|
+
* projectId: 'my-project',
|
|
49
|
+
* locationId: 'global',
|
|
50
|
+
* keyRingId: 'my-keyring',
|
|
51
|
+
* keyId: 'my-key',
|
|
52
|
+
* keyVersion: '1'
|
|
53
|
+
* })
|
|
54
|
+
* const account = await toGcpKmsAccount(signer)
|
|
55
|
+
*
|
|
56
|
+
* const client = createWalletClient({
|
|
57
|
+
* account,
|
|
58
|
+
* chain: mainnet,
|
|
59
|
+
* transport: http()
|
|
60
|
+
* })
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
export async function toGcpKmsAccount(signer) {
|
|
64
|
+
const address = await signer.getAddress();
|
|
65
|
+
return toAccount({
|
|
66
|
+
address,
|
|
67
|
+
signMessage: async ({ message }) => {
|
|
68
|
+
// Convert SignableMessage to string for GcpSigner
|
|
69
|
+
const messageStr = typeof message === 'string' ? message : message.raw.toString();
|
|
70
|
+
return signer.signMessage({ message: messageStr });
|
|
71
|
+
},
|
|
72
|
+
signTransaction: async (transaction, options) => signer.signTransaction(transaction, options),
|
|
73
|
+
signTypedData: async (typedData) => signer.signTypedData(typedData),
|
|
74
|
+
});
|
|
75
|
+
}
|
|
36
76
|
//# sourceMappingURL=account.js.map
|
package/dist/account.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"account.js","sourceRoot":"","sources":["../src/account.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"account.js","sourceRoot":"","sources":["../src/account.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAI1C;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,MAAiB;IACnD,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,UAAU,EAAE,CAAC;IAE1C,OAAO,SAAS,CAAC;QAChB,OAAO;QACP,WAAW,EAAE,KAAK,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE;YAClC,kDAAkD;YAClD,MAAM,UAAU,GACf,OAAO,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC;YAChE,OAAO,MAAM,CAAC,WAAW,CAAC,EAAE,OAAO,EAAE,UAAU,EAAE,CAAC,CAAC;QACpD,CAAC;QACD,eAAe,EAAE,KAAK,EAAE,WAAW,EAAE,OAAO,EAAE,EAAE,CAC/C,MAAM,CAAC,eAAe,CAAC,WAAW,EAAE,OAAO,CAAC;QAC7C,aAAa,EAAE,KAAK,EAAE,SAAS,EAAE,EAAE,CAAC,MAAM,CAAC,aAAa,CAAC,SAAS,CAAC;KACnE,CAAC,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACpC,MAAiB;IAEjB,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,UAAU,EAAE,CAAC;IAE1C,OAAO,SAAS,CAAC;QAChB,OAAO;QACP,WAAW,EAAE,KAAK,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE;YAClC,kDAAkD;YAClD,MAAM,UAAU,GACf,OAAO,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC;YAChE,OAAO,MAAM,CAAC,WAAW,CAAC,EAAE,OAAO,EAAE,UAAU,EAAE,CAAC,CAAC;QACpD,CAAC;QACD,eAAe,EAAE,KAAK,EAAE,WAAW,EAAE,OAAO,EAAE,EAAE,CAC/C,MAAM,CAAC,eAAe,CAAC,WAAW,EAAE,OAAO,CAAC;QAC7C,aAAa,EAAE,KAAK,EAAE,SAAS,EAAE,EAAE,CAAC,MAAM,CAAC,aAAa,CAAC,SAAS,CAAC;KACnE,CAAC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { GcpKmsConfig } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* GcpClient wraps GCP KMS SDK operations for key management and signing.
|
|
4
|
+
*
|
|
5
|
+
* This class provides a simplified interface to GCP KMS for:
|
|
6
|
+
* - Retrieving public keys from KMS
|
|
7
|
+
* - Signing message digests with KMS-stored private keys
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* const client = new GcpClient({
|
|
12
|
+
* projectId: 'my-project',
|
|
13
|
+
* locationId: 'global',
|
|
14
|
+
* keyRingId: 'my-keyring',
|
|
15
|
+
* keyId: 'my-key',
|
|
16
|
+
* keyVersion: '1'
|
|
17
|
+
* })
|
|
18
|
+
*
|
|
19
|
+
* const publicKey = await client.getPublicKey()
|
|
20
|
+
* const signature = await client.sign(messageHash)
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export declare class GcpClient {
|
|
24
|
+
private client;
|
|
25
|
+
private keyName;
|
|
26
|
+
/**
|
|
27
|
+
* Creates a new GCP KMS client instance.
|
|
28
|
+
*
|
|
29
|
+
* @param config - GCP KMS configuration including project, location, key ring, key, and version
|
|
30
|
+
*
|
|
31
|
+
* @remarks
|
|
32
|
+
* If keyFilename is not provided, the GCP SDK will use the default credential provider chain:
|
|
33
|
+
* - GOOGLE_APPLICATION_CREDENTIALS environment variable
|
|
34
|
+
* - Application Default Credentials (ADC)
|
|
35
|
+
* - Service account attached to the compute resource
|
|
36
|
+
*/
|
|
37
|
+
constructor(config: GcpKmsConfig);
|
|
38
|
+
/**
|
|
39
|
+
* Retrieves the public key from GCP KMS.
|
|
40
|
+
*
|
|
41
|
+
* @returns The public key in DER-encoded SubjectPublicKeyInfo (SPKI) format
|
|
42
|
+
* @throws {KmsClientError} If the KMS API call fails or returns no public key
|
|
43
|
+
*
|
|
44
|
+
* @remarks
|
|
45
|
+
* GCP KMS returns the public key in PEM format, which is converted to DER format.
|
|
46
|
+
* The returned public key is in SPKI format with a variable-length DER header.
|
|
47
|
+
* The last 65 bytes contain the uncompressed secp256k1 public key (0x04 + x + y).
|
|
48
|
+
*/
|
|
49
|
+
getPublicKey(): Promise<Uint8Array>;
|
|
50
|
+
/**
|
|
51
|
+
* Signs a message hash using the GCP KMS-stored private key.
|
|
52
|
+
*
|
|
53
|
+
* @param messageHash - The pre-hashed message to sign (32 bytes for keccak256)
|
|
54
|
+
* @returns The signature in DER-encoded format (SEQUENCE { INTEGER r, INTEGER s })
|
|
55
|
+
* @throws {KmsClientError} If the KMS API call fails or returns no signature
|
|
56
|
+
*
|
|
57
|
+
* @remarks
|
|
58
|
+
* - The messageHash should already be hashed (e.g., with keccak256)
|
|
59
|
+
* - GCP KMS uses EC_SIGN_SECP256K1_SHA256 algorithm for signing
|
|
60
|
+
* - The returned signature is DER-encoded and needs to be parsed to extract r and s values
|
|
61
|
+
*/
|
|
62
|
+
sign(messageHash: Uint8Array): Promise<Uint8Array>;
|
|
63
|
+
/**
|
|
64
|
+
* Converts PEM-encoded data to DER-encoded format.
|
|
65
|
+
*
|
|
66
|
+
* @param pem - PEM-encoded public key string
|
|
67
|
+
* @returns DER-encoded public key as Uint8Array
|
|
68
|
+
* @throws {KmsClientError} If PEM format is invalid
|
|
69
|
+
*
|
|
70
|
+
* @remarks
|
|
71
|
+
* PEM format consists of:
|
|
72
|
+
* - Header line: -----BEGIN PUBLIC KEY-----
|
|
73
|
+
* - Base64-encoded DER data
|
|
74
|
+
* - Footer line: -----END PUBLIC KEY-----
|
|
75
|
+
*
|
|
76
|
+
* This method extracts the Base64 data and decodes it to DER format.
|
|
77
|
+
*/
|
|
78
|
+
private pemToDer;
|
|
79
|
+
/**
|
|
80
|
+
* Calculates CRC32C checksum for data integrity verification.
|
|
81
|
+
*
|
|
82
|
+
* @param data - Data to calculate checksum for
|
|
83
|
+
* @returns CRC32C checksum as number
|
|
84
|
+
*
|
|
85
|
+
* @remarks
|
|
86
|
+
* GCP KMS requires CRC32C checksums for request/response integrity verification.
|
|
87
|
+
* This is a simple implementation using a lookup table.
|
|
88
|
+
*/
|
|
89
|
+
private calculateCrc32c;
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/gcp/client.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAE7C;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,qBAAa,SAAS;IACrB,OAAO,CAAC,MAAM,CAA6B;IAC3C,OAAO,CAAC,OAAO,CAAS;IAExB;;;;;;;;;;OAUG;gBACS,MAAM,EAAE,YAAY;IAUhC;;;;;;;;;;OAUG;IACG,YAAY,IAAI,OAAO,CAAC,UAAU,CAAC;IAqBzC;;;;;;;;;;;OAWG;IACG,IAAI,CAAC,WAAW,EAAE,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;IAqDxD;;;;;;;;;;;;;;OAcG;IACH,OAAO,CAAC,QAAQ;IAkBhB;;;;;;;;;OASG;IACH,OAAO,CAAC,eAAe;CAkBvB"}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { KeyManagementServiceClient } from '@google-cloud/kms';
|
|
2
|
+
import { KmsClientError } from '../errors';
|
|
3
|
+
/**
|
|
4
|
+
* GcpClient wraps GCP KMS SDK operations for key management and signing.
|
|
5
|
+
*
|
|
6
|
+
* This class provides a simplified interface to GCP KMS for:
|
|
7
|
+
* - Retrieving public keys from KMS
|
|
8
|
+
* - Signing message digests with KMS-stored private keys
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* const client = new GcpClient({
|
|
13
|
+
* projectId: 'my-project',
|
|
14
|
+
* locationId: 'global',
|
|
15
|
+
* keyRingId: 'my-keyring',
|
|
16
|
+
* keyId: 'my-key',
|
|
17
|
+
* keyVersion: '1'
|
|
18
|
+
* })
|
|
19
|
+
*
|
|
20
|
+
* const publicKey = await client.getPublicKey()
|
|
21
|
+
* const signature = await client.sign(messageHash)
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export class GcpClient {
|
|
25
|
+
/**
|
|
26
|
+
* Creates a new GCP KMS client instance.
|
|
27
|
+
*
|
|
28
|
+
* @param config - GCP KMS configuration including project, location, key ring, key, and version
|
|
29
|
+
*
|
|
30
|
+
* @remarks
|
|
31
|
+
* If keyFilename is not provided, the GCP SDK will use the default credential provider chain:
|
|
32
|
+
* - GOOGLE_APPLICATION_CREDENTIALS environment variable
|
|
33
|
+
* - Application Default Credentials (ADC)
|
|
34
|
+
* - Service account attached to the compute resource
|
|
35
|
+
*/
|
|
36
|
+
constructor(config) {
|
|
37
|
+
this.client = new KeyManagementServiceClient(config.keyFilename ? { keyFilename: config.keyFilename } : {});
|
|
38
|
+
// Construct the full key resource name
|
|
39
|
+
// Format: projects/{project}/locations/{location}/keyRings/{keyRing}/cryptoKeys/{key}/cryptoKeyVersions/{version}
|
|
40
|
+
this.keyName = `projects/${config.projectId}/locations/${config.locationId}/keyRings/${config.keyRingId}/cryptoKeys/${config.keyId}/cryptoKeyVersions/${config.keyVersion}`;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Retrieves the public key from GCP KMS.
|
|
44
|
+
*
|
|
45
|
+
* @returns The public key in DER-encoded SubjectPublicKeyInfo (SPKI) format
|
|
46
|
+
* @throws {KmsClientError} If the KMS API call fails or returns no public key
|
|
47
|
+
*
|
|
48
|
+
* @remarks
|
|
49
|
+
* GCP KMS returns the public key in PEM format, which is converted to DER format.
|
|
50
|
+
* The returned public key is in SPKI format with a variable-length DER header.
|
|
51
|
+
* The last 65 bytes contain the uncompressed secp256k1 public key (0x04 + x + y).
|
|
52
|
+
*/
|
|
53
|
+
async getPublicKey() {
|
|
54
|
+
try {
|
|
55
|
+
const [publicKey] = await this.client.getPublicKey({
|
|
56
|
+
name: this.keyName,
|
|
57
|
+
});
|
|
58
|
+
if (!publicKey.pem) {
|
|
59
|
+
throw new KmsClientError('No public key returned from GCP KMS');
|
|
60
|
+
}
|
|
61
|
+
// Convert PEM to DER format
|
|
62
|
+
return this.pemToDer(publicKey.pem);
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
if (error instanceof KmsClientError)
|
|
66
|
+
throw error;
|
|
67
|
+
throw new KmsClientError(`Failed to get public key from GCP KMS: ${error instanceof Error ? error.message : 'Unknown error'}`, error instanceof Error ? error : undefined);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Signs a message hash using the GCP KMS-stored private key.
|
|
72
|
+
*
|
|
73
|
+
* @param messageHash - The pre-hashed message to sign (32 bytes for keccak256)
|
|
74
|
+
* @returns The signature in DER-encoded format (SEQUENCE { INTEGER r, INTEGER s })
|
|
75
|
+
* @throws {KmsClientError} If the KMS API call fails or returns no signature
|
|
76
|
+
*
|
|
77
|
+
* @remarks
|
|
78
|
+
* - The messageHash should already be hashed (e.g., with keccak256)
|
|
79
|
+
* - GCP KMS uses EC_SIGN_SECP256K1_SHA256 algorithm for signing
|
|
80
|
+
* - The returned signature is DER-encoded and needs to be parsed to extract r and s values
|
|
81
|
+
*/
|
|
82
|
+
async sign(messageHash) {
|
|
83
|
+
try {
|
|
84
|
+
// Create CRC32C checksum for message integrity
|
|
85
|
+
const crc32c = this.calculateCrc32c(messageHash);
|
|
86
|
+
const [response] = await this.client.asymmetricSign({
|
|
87
|
+
name: this.keyName,
|
|
88
|
+
digest: {
|
|
89
|
+
sha256: messageHash,
|
|
90
|
+
},
|
|
91
|
+
digestCrc32c: {
|
|
92
|
+
value: crc32c,
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
if (!response.signature) {
|
|
96
|
+
throw new KmsClientError('No signature returned from GCP KMS');
|
|
97
|
+
}
|
|
98
|
+
// Convert signature to Uint8Array
|
|
99
|
+
const signatureBytes = typeof response.signature === 'string'
|
|
100
|
+
? Buffer.from(response.signature, 'base64')
|
|
101
|
+
: response.signature instanceof Buffer
|
|
102
|
+
? response.signature
|
|
103
|
+
: new Uint8Array(response.signature);
|
|
104
|
+
// Verify the signature CRC32C if provided
|
|
105
|
+
if (response.signatureCrc32c && response.verifiedDigestCrc32c) {
|
|
106
|
+
const signatureCrc32c = this.calculateCrc32c(signatureBytes instanceof Buffer
|
|
107
|
+
? new Uint8Array(signatureBytes)
|
|
108
|
+
: signatureBytes);
|
|
109
|
+
if (signatureCrc32c !== Number(response.signatureCrc32c.value)) {
|
|
110
|
+
throw new KmsClientError('Signature CRC32C verification failed - data may be corrupted');
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return signatureBytes instanceof Buffer
|
|
114
|
+
? new Uint8Array(signatureBytes)
|
|
115
|
+
: signatureBytes;
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
if (error instanceof KmsClientError)
|
|
119
|
+
throw error;
|
|
120
|
+
throw new KmsClientError(`Failed to sign with GCP KMS: ${error instanceof Error ? error.message : 'Unknown error'}`, error instanceof Error ? error : undefined);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Converts PEM-encoded data to DER-encoded format.
|
|
125
|
+
*
|
|
126
|
+
* @param pem - PEM-encoded public key string
|
|
127
|
+
* @returns DER-encoded public key as Uint8Array
|
|
128
|
+
* @throws {KmsClientError} If PEM format is invalid
|
|
129
|
+
*
|
|
130
|
+
* @remarks
|
|
131
|
+
* PEM format consists of:
|
|
132
|
+
* - Header line: -----BEGIN PUBLIC KEY-----
|
|
133
|
+
* - Base64-encoded DER data
|
|
134
|
+
* - Footer line: -----END PUBLIC KEY-----
|
|
135
|
+
*
|
|
136
|
+
* This method extracts the Base64 data and decodes it to DER format.
|
|
137
|
+
*/
|
|
138
|
+
pemToDer(pem) {
|
|
139
|
+
try {
|
|
140
|
+
// Remove PEM header, footer, and whitespace
|
|
141
|
+
const base64 = pem
|
|
142
|
+
.replace(/-----BEGIN PUBLIC KEY-----/, '')
|
|
143
|
+
.replace(/-----END PUBLIC KEY-----/, '')
|
|
144
|
+
.replace(/\s/g, '');
|
|
145
|
+
// Decode Base64 to DER
|
|
146
|
+
return Uint8Array.from(Buffer.from(base64, 'base64'));
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
throw new KmsClientError(`Failed to convert PEM to DER: ${error instanceof Error ? error.message : 'Unknown error'}`, error instanceof Error ? error : undefined);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Calculates CRC32C checksum for data integrity verification.
|
|
154
|
+
*
|
|
155
|
+
* @param data - Data to calculate checksum for
|
|
156
|
+
* @returns CRC32C checksum as number
|
|
157
|
+
*
|
|
158
|
+
* @remarks
|
|
159
|
+
* GCP KMS requires CRC32C checksums for request/response integrity verification.
|
|
160
|
+
* This is a simple implementation using a lookup table.
|
|
161
|
+
*/
|
|
162
|
+
calculateCrc32c(data) {
|
|
163
|
+
// CRC32C polynomial lookup table
|
|
164
|
+
const crc32cTable = new Int32Array(256);
|
|
165
|
+
for (let i = 0; i < 256; i++) {
|
|
166
|
+
let c = i;
|
|
167
|
+
for (let j = 0; j < 8; j++) {
|
|
168
|
+
c = c & 1 ? 0x82f63b78 ^ (c >>> 1) : c >>> 1;
|
|
169
|
+
}
|
|
170
|
+
crc32cTable[i] = c;
|
|
171
|
+
}
|
|
172
|
+
let crc = 0xffffffff;
|
|
173
|
+
for (let i = 0; i < data.length; i++) {
|
|
174
|
+
crc = crc32cTable[(crc ^ data[i]) & 0xff] ^ (crc >>> 8);
|
|
175
|
+
}
|
|
176
|
+
return (crc ^ 0xffffffff) >>> 0;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/gcp/client.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import type { TypedData } from 'abitype';
|
|
2
|
+
import type { Address, Hex, SerializeTransactionFn, TransactionSerializable, TypedDataDefinition } from 'viem';
|
|
3
|
+
import type { GcpKmsConfig } from '../types';
|
|
4
|
+
/**
|
|
5
|
+
* GcpSigner provides Ethereum signing capabilities using GCP KMS.
|
|
6
|
+
*
|
|
7
|
+
* This class manages the interaction with GCP KMS for cryptographic operations
|
|
8
|
+
* required by Ethereum accounts:
|
|
9
|
+
* - Public key retrieval and caching
|
|
10
|
+
* - Ethereum address derivation from KMS public key
|
|
11
|
+
* - Message and transaction signing
|
|
12
|
+
*
|
|
13
|
+
* The signer caches expensive operations (public key retrieval, address derivation)
|
|
14
|
+
* to avoid unnecessary KMS API calls.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```typescript
|
|
18
|
+
* const signer = new GcpSigner({
|
|
19
|
+
* projectId: 'my-project',
|
|
20
|
+
* locationId: 'global',
|
|
21
|
+
* keyRingId: 'my-keyring',
|
|
22
|
+
* keyId: 'my-key',
|
|
23
|
+
* keyVersion: '1'
|
|
24
|
+
* })
|
|
25
|
+
*
|
|
26
|
+
* const address = await signer.getAddress()
|
|
27
|
+
* console.log('Ethereum address:', address)
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export declare class GcpSigner {
|
|
31
|
+
private gcpClient;
|
|
32
|
+
private cachedAddress?;
|
|
33
|
+
private cachedPublicKey?;
|
|
34
|
+
/**
|
|
35
|
+
* Creates a new GCP KMS signer instance.
|
|
36
|
+
*
|
|
37
|
+
* @param config - GCP KMS configuration including project, location, key ring, key, and version
|
|
38
|
+
*
|
|
39
|
+
* @remarks
|
|
40
|
+
* The constructor initializes the GCP KMS client but does not make any API calls.
|
|
41
|
+
* Public key retrieval and address derivation happen lazily on first use.
|
|
42
|
+
*/
|
|
43
|
+
constructor(config: GcpKmsConfig);
|
|
44
|
+
/**
|
|
45
|
+
* Retrieves the uncompressed secp256k1 public key from GCP KMS.
|
|
46
|
+
*
|
|
47
|
+
* The public key is retrieved from KMS and extracted from the DER-encoded
|
|
48
|
+
* SubjectPublicKeyInfo format. The result is cached to avoid redundant KMS calls.
|
|
49
|
+
*
|
|
50
|
+
* @returns 65-byte uncompressed public key (0x04 + x coordinate + y coordinate)
|
|
51
|
+
* @throws {KmsClientError} If KMS API call fails
|
|
52
|
+
* @throws {DerParsingError} If public key format is invalid
|
|
53
|
+
*
|
|
54
|
+
* @remarks
|
|
55
|
+
* The public key format is:
|
|
56
|
+
* - Byte 0: 0x04 (uncompressed point indicator)
|
|
57
|
+
* - Bytes 1-32: x coordinate of the public key
|
|
58
|
+
* - Bytes 33-64: y coordinate of the public key
|
|
59
|
+
*/
|
|
60
|
+
getPublicKey(): Promise<Uint8Array>;
|
|
61
|
+
/**
|
|
62
|
+
* Derives the Ethereum address from the GCP KMS public key.
|
|
63
|
+
*
|
|
64
|
+
* The address is calculated by:
|
|
65
|
+
* 1. Retrieving the public key from KMS (cached if available)
|
|
66
|
+
* 2. Hashing the public key coordinates with keccak256
|
|
67
|
+
* 3. Taking the last 20 bytes as the address
|
|
68
|
+
*
|
|
69
|
+
* The result is cached to avoid redundant derivation.
|
|
70
|
+
*
|
|
71
|
+
* @returns Ethereum address (0x-prefixed, 40 hex characters)
|
|
72
|
+
* @throws {KmsClientError} If KMS API call fails
|
|
73
|
+
* @throws {DerParsingError} If public key format is invalid
|
|
74
|
+
*
|
|
75
|
+
* @remarks
|
|
76
|
+
* The returned address follows EIP-55 checksum encoding.
|
|
77
|
+
*/
|
|
78
|
+
getAddress(): Promise<Address>;
|
|
79
|
+
/**
|
|
80
|
+
* Signs a hash using the GCP KMS private key (internal helper method).
|
|
81
|
+
*
|
|
82
|
+
* This method is used internally by signMessage, signTransaction, and signTypedData.
|
|
83
|
+
* It converts the hash to bytes, signs with KMS, parses the DER signature,
|
|
84
|
+
* and normalizes the s value according to EIP-2.
|
|
85
|
+
*
|
|
86
|
+
* @param hash - The hash to sign (32 bytes, hex-encoded)
|
|
87
|
+
* @returns Object containing r and s as bigints
|
|
88
|
+
* @throws {KmsClientError} If KMS API call fails
|
|
89
|
+
* @throws {DerParsingError} If signature format is invalid
|
|
90
|
+
* @throws {SignatureNormalizationError} If s value is out of valid range
|
|
91
|
+
*
|
|
92
|
+
* @remarks
|
|
93
|
+
* The s value is automatically normalized to the lower half of the curve order (EIP-2)
|
|
94
|
+
* to prevent signature malleability attacks.
|
|
95
|
+
*/
|
|
96
|
+
private signHash;
|
|
97
|
+
/**
|
|
98
|
+
* Signs a message using EIP-191 personal_sign standard.
|
|
99
|
+
*
|
|
100
|
+
* This method:
|
|
101
|
+
* 1. Hashes the message with EIP-191 prefix: "\x19Ethereum Signed Message:\n" + len(message) + message
|
|
102
|
+
* 2. Signs the hash with GCP KMS
|
|
103
|
+
* 3. Calculates the recovery ID to enable public key recovery
|
|
104
|
+
* 4. Returns the signature in the standard format: r (32 bytes) + s (32 bytes) + v (1 byte)
|
|
105
|
+
*
|
|
106
|
+
* @param params - Object containing the message string
|
|
107
|
+
* @returns The signature as a hex string (0x-prefixed, 130 characters)
|
|
108
|
+
* @throws {KmsClientError} If KMS API call fails
|
|
109
|
+
* @throws {DerParsingError} If signature format is invalid
|
|
110
|
+
* @throws {RecoveryIdCalculationError} If recovery ID calculation fails
|
|
111
|
+
*
|
|
112
|
+
* @example
|
|
113
|
+
* ```typescript
|
|
114
|
+
* const signer = new GcpSigner({
|
|
115
|
+
* projectId: 'my-project',
|
|
116
|
+
* locationId: 'global',
|
|
117
|
+
* keyRingId: 'my-keyring',
|
|
118
|
+
* keyId: 'my-key',
|
|
119
|
+
* keyVersion: '1'
|
|
120
|
+
* })
|
|
121
|
+
* const signature = await signer.signMessage({ message: 'Hello, world!' })
|
|
122
|
+
* // signature: '0x...' (130 characters: 0x + 64 hex chars for r + 64 for s + 2 for v)
|
|
123
|
+
* ```
|
|
124
|
+
*/
|
|
125
|
+
signMessage({ message }: {
|
|
126
|
+
message: string;
|
|
127
|
+
}): Promise<Hex>;
|
|
128
|
+
/**
|
|
129
|
+
* Signs an Ethereum transaction.
|
|
130
|
+
*
|
|
131
|
+
* This method:
|
|
132
|
+
* 1. Serializes the transaction without signature fields (r, s, v)
|
|
133
|
+
* 2. Hashes the serialized transaction with keccak256
|
|
134
|
+
* 3. Signs the hash with GCP KMS
|
|
135
|
+
* 4. Calculates the recovery ID
|
|
136
|
+
* 5. Computes the v value (EIP-155 if chainId present, legacy otherwise)
|
|
137
|
+
* 6. Returns the fully serialized transaction with signature
|
|
138
|
+
*
|
|
139
|
+
* @param transaction - The transaction to sign
|
|
140
|
+
* @param options - Optional serializer function (defaults to viem's serializeTransaction)
|
|
141
|
+
* @returns The serialized signed transaction as a hex string
|
|
142
|
+
* @throws {KmsClientError} If KMS API call fails
|
|
143
|
+
* @throws {DerParsingError} If signature format is invalid
|
|
144
|
+
* @throws {RecoveryIdCalculationError} If recovery ID calculation fails
|
|
145
|
+
*
|
|
146
|
+
* @example
|
|
147
|
+
* ```typescript
|
|
148
|
+
* const signer = new GcpSigner({
|
|
149
|
+
* projectId: 'my-project',
|
|
150
|
+
* locationId: 'global',
|
|
151
|
+
* keyRingId: 'my-keyring',
|
|
152
|
+
* keyId: 'my-key',
|
|
153
|
+
* keyVersion: '1'
|
|
154
|
+
* })
|
|
155
|
+
* const signedTx = await signer.signTransaction({
|
|
156
|
+
* to: '0x...',
|
|
157
|
+
* value: parseEther('1'),
|
|
158
|
+
* chainId: 1
|
|
159
|
+
* })
|
|
160
|
+
* ```
|
|
161
|
+
*/
|
|
162
|
+
signTransaction(transaction: TransactionSerializable, { serializer, }?: {
|
|
163
|
+
serializer?: SerializeTransactionFn;
|
|
164
|
+
}): Promise<Hex>;
|
|
165
|
+
/**
|
|
166
|
+
* Signs typed data according to EIP-712.
|
|
167
|
+
*
|
|
168
|
+
* This method:
|
|
169
|
+
* 1. Hashes the typed data using EIP-712 (domain separator + type hash)
|
|
170
|
+
* 2. Signs the hash with GCP KMS
|
|
171
|
+
* 3. Calculates the recovery ID
|
|
172
|
+
* 4. Returns the signature in the standard format: r (32 bytes) + s (32 bytes) + v (1 byte)
|
|
173
|
+
*
|
|
174
|
+
* @param typedData - The EIP-712 typed data to sign
|
|
175
|
+
* @returns The signature as a hex string (0x-prefixed, 130 characters)
|
|
176
|
+
* @throws {KmsClientError} If KMS API call fails
|
|
177
|
+
* @throws {DerParsingError} If signature format is invalid
|
|
178
|
+
* @throws {RecoveryIdCalculationError} If recovery ID calculation fails
|
|
179
|
+
*
|
|
180
|
+
* @example
|
|
181
|
+
* ```typescript
|
|
182
|
+
* const signer = new GcpSigner({
|
|
183
|
+
* projectId: 'my-project',
|
|
184
|
+
* locationId: 'global',
|
|
185
|
+
* keyRingId: 'my-keyring',
|
|
186
|
+
* keyId: 'my-key',
|
|
187
|
+
* keyVersion: '1'
|
|
188
|
+
* })
|
|
189
|
+
* const signature = await signer.signTypedData({
|
|
190
|
+
* domain: {
|
|
191
|
+
* name: 'MyApp',
|
|
192
|
+
* version: '1',
|
|
193
|
+
* chainId: 1,
|
|
194
|
+
* verifyingContract: '0x...'
|
|
195
|
+
* },
|
|
196
|
+
* types: {
|
|
197
|
+
* Person: [
|
|
198
|
+
* { name: 'name', type: 'string' },
|
|
199
|
+
* { name: 'wallet', type: 'address' }
|
|
200
|
+
* ]
|
|
201
|
+
* },
|
|
202
|
+
* primaryType: 'Person',
|
|
203
|
+
* message: {
|
|
204
|
+
* name: 'Alice',
|
|
205
|
+
* wallet: '0x...'
|
|
206
|
+
* }
|
|
207
|
+
* })
|
|
208
|
+
* ```
|
|
209
|
+
*/
|
|
210
|
+
signTypedData<const TTypedData extends TypedData | Record<string, unknown>, TPrimaryType extends keyof TTypedData | 'EIP712Domain' = keyof TTypedData>(typedData: TypedDataDefinition<TTypedData, TPrimaryType>): Promise<Hex>;
|
|
211
|
+
}
|
|
212
|
+
//# sourceMappingURL=signer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"signer.d.ts","sourceRoot":"","sources":["../../src/gcp/signer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AACzC,OAAO,KAAK,EACX,OAAO,EACP,GAAG,EACH,sBAAsB,EACtB,uBAAuB,EACvB,mBAAmB,EACnB,MAAM,MAAM,CAAC;AAUd,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAU7C;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,qBAAa,SAAS;IACrB,OAAO,CAAC,SAAS,CAAY;IAC7B,OAAO,CAAC,aAAa,CAAC,CAAU;IAChC,OAAO,CAAC,eAAe,CAAC,CAAa;IAErC;;;;;;;;OAQG;gBACS,MAAM,EAAE,YAAY;IAIhC;;;;;;;;;;;;;;;OAeG;IACG,YAAY,IAAI,OAAO,CAAC,UAAU,CAAC;IAWzC;;;;;;;;;;;;;;;;OAgBG;IACG,UAAU,IAAI,OAAO,CAAC,OAAO,CAAC;IAWpC;;;;;;;;;;;;;;;;OAgBG;YACW,QAAQ;IAoBtB;;;;;;;;;;;;;;;;;;;;;;;;;;;OA2BG;IACG,WAAW,CAAC,EAAE,OAAO,EAAE,EAAE;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,GAAG,CAAC;IA2BjE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAiCG;IACG,eAAe,CACpB,WAAW,EAAE,uBAAuB,EACpC,EACC,UAAiC,GACjC,GAAE;QAAE,UAAU,CAAC,EAAE,sBAAsB,CAAA;KAAO,GAC7C,OAAO,CAAC,GAAG,CAAC;IAqCf;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA4CG;IACG,aAAa,CAClB,KAAK,CAAC,UAAU,SAAS,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC5D,YAAY,SAAS,MAAM,UAAU,GAAG,cAAc,GAAG,MAAM,UAAU,EACxE,SAAS,EAAE,mBAAmB,CAAC,UAAU,EAAE,YAAY,CAAC,GAAG,OAAO,CAAC,GAAG,CAAC;CA0BzE"}
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import { concat, fromHex, hashMessage, hashTypedData, keccak256, serializeTransaction, toHex, } from 'viem';
|
|
2
|
+
import { extractPublicKeyFromDer, publicKeyToAddress } from '../utils/address';
|
|
3
|
+
import { parseDerSignature } from '../utils/der';
|
|
4
|
+
import { calculateRecoveryId, normalizeS, uint8ArrayToBigInt, } from '../utils/signature';
|
|
5
|
+
import { GcpClient } from './client';
|
|
6
|
+
/**
|
|
7
|
+
* GcpSigner provides Ethereum signing capabilities using GCP KMS.
|
|
8
|
+
*
|
|
9
|
+
* This class manages the interaction with GCP KMS for cryptographic operations
|
|
10
|
+
* required by Ethereum accounts:
|
|
11
|
+
* - Public key retrieval and caching
|
|
12
|
+
* - Ethereum address derivation from KMS public key
|
|
13
|
+
* - Message and transaction signing
|
|
14
|
+
*
|
|
15
|
+
* The signer caches expensive operations (public key retrieval, address derivation)
|
|
16
|
+
* to avoid unnecessary KMS API calls.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```typescript
|
|
20
|
+
* const signer = new GcpSigner({
|
|
21
|
+
* projectId: 'my-project',
|
|
22
|
+
* locationId: 'global',
|
|
23
|
+
* keyRingId: 'my-keyring',
|
|
24
|
+
* keyId: 'my-key',
|
|
25
|
+
* keyVersion: '1'
|
|
26
|
+
* })
|
|
27
|
+
*
|
|
28
|
+
* const address = await signer.getAddress()
|
|
29
|
+
* console.log('Ethereum address:', address)
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export class GcpSigner {
|
|
33
|
+
/**
|
|
34
|
+
* Creates a new GCP KMS signer instance.
|
|
35
|
+
*
|
|
36
|
+
* @param config - GCP KMS configuration including project, location, key ring, key, and version
|
|
37
|
+
*
|
|
38
|
+
* @remarks
|
|
39
|
+
* The constructor initializes the GCP KMS client but does not make any API calls.
|
|
40
|
+
* Public key retrieval and address derivation happen lazily on first use.
|
|
41
|
+
*/
|
|
42
|
+
constructor(config) {
|
|
43
|
+
this.gcpClient = new GcpClient(config);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Retrieves the uncompressed secp256k1 public key from GCP KMS.
|
|
47
|
+
*
|
|
48
|
+
* The public key is retrieved from KMS and extracted from the DER-encoded
|
|
49
|
+
* SubjectPublicKeyInfo format. The result is cached to avoid redundant KMS calls.
|
|
50
|
+
*
|
|
51
|
+
* @returns 65-byte uncompressed public key (0x04 + x coordinate + y coordinate)
|
|
52
|
+
* @throws {KmsClientError} If KMS API call fails
|
|
53
|
+
* @throws {DerParsingError} If public key format is invalid
|
|
54
|
+
*
|
|
55
|
+
* @remarks
|
|
56
|
+
* The public key format is:
|
|
57
|
+
* - Byte 0: 0x04 (uncompressed point indicator)
|
|
58
|
+
* - Bytes 1-32: x coordinate of the public key
|
|
59
|
+
* - Bytes 33-64: y coordinate of the public key
|
|
60
|
+
*/
|
|
61
|
+
async getPublicKey() {
|
|
62
|
+
if (this.cachedPublicKey) {
|
|
63
|
+
return this.cachedPublicKey;
|
|
64
|
+
}
|
|
65
|
+
const derPublicKey = await this.gcpClient.getPublicKey();
|
|
66
|
+
const publicKey = extractPublicKeyFromDer(derPublicKey);
|
|
67
|
+
this.cachedPublicKey = publicKey;
|
|
68
|
+
return publicKey;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Derives the Ethereum address from the GCP KMS public key.
|
|
72
|
+
*
|
|
73
|
+
* The address is calculated by:
|
|
74
|
+
* 1. Retrieving the public key from KMS (cached if available)
|
|
75
|
+
* 2. Hashing the public key coordinates with keccak256
|
|
76
|
+
* 3. Taking the last 20 bytes as the address
|
|
77
|
+
*
|
|
78
|
+
* The result is cached to avoid redundant derivation.
|
|
79
|
+
*
|
|
80
|
+
* @returns Ethereum address (0x-prefixed, 40 hex characters)
|
|
81
|
+
* @throws {KmsClientError} If KMS API call fails
|
|
82
|
+
* @throws {DerParsingError} If public key format is invalid
|
|
83
|
+
*
|
|
84
|
+
* @remarks
|
|
85
|
+
* The returned address follows EIP-55 checksum encoding.
|
|
86
|
+
*/
|
|
87
|
+
async getAddress() {
|
|
88
|
+
if (this.cachedAddress) {
|
|
89
|
+
return this.cachedAddress;
|
|
90
|
+
}
|
|
91
|
+
const publicKey = await this.getPublicKey();
|
|
92
|
+
const address = publicKeyToAddress(publicKey);
|
|
93
|
+
this.cachedAddress = address;
|
|
94
|
+
return address;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Signs a hash using the GCP KMS private key (internal helper method).
|
|
98
|
+
*
|
|
99
|
+
* This method is used internally by signMessage, signTransaction, and signTypedData.
|
|
100
|
+
* It converts the hash to bytes, signs with KMS, parses the DER signature,
|
|
101
|
+
* and normalizes the s value according to EIP-2.
|
|
102
|
+
*
|
|
103
|
+
* @param hash - The hash to sign (32 bytes, hex-encoded)
|
|
104
|
+
* @returns Object containing r and s as bigints
|
|
105
|
+
* @throws {KmsClientError} If KMS API call fails
|
|
106
|
+
* @throws {DerParsingError} If signature format is invalid
|
|
107
|
+
* @throws {SignatureNormalizationError} If s value is out of valid range
|
|
108
|
+
*
|
|
109
|
+
* @remarks
|
|
110
|
+
* The s value is automatically normalized to the lower half of the curve order (EIP-2)
|
|
111
|
+
* to prevent signature malleability attacks.
|
|
112
|
+
*/
|
|
113
|
+
async signHash(hash) {
|
|
114
|
+
// Convert Hex to Uint8Array
|
|
115
|
+
const hashBytes = fromHex(hash, 'bytes');
|
|
116
|
+
// Sign with GCP KMS
|
|
117
|
+
const derSignature = await this.gcpClient.sign(hashBytes);
|
|
118
|
+
// Parse DER signature
|
|
119
|
+
const { r: rBytes, s: sBytes } = parseDerSignature(derSignature);
|
|
120
|
+
// Convert to bigint
|
|
121
|
+
const r = uint8ArrayToBigInt(rBytes);
|
|
122
|
+
let s = uint8ArrayToBigInt(sBytes);
|
|
123
|
+
// EIP-2 normalization
|
|
124
|
+
s = normalizeS(s);
|
|
125
|
+
return { r, s };
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Signs a message using EIP-191 personal_sign standard.
|
|
129
|
+
*
|
|
130
|
+
* This method:
|
|
131
|
+
* 1. Hashes the message with EIP-191 prefix: "\x19Ethereum Signed Message:\n" + len(message) + message
|
|
132
|
+
* 2. Signs the hash with GCP KMS
|
|
133
|
+
* 3. Calculates the recovery ID to enable public key recovery
|
|
134
|
+
* 4. Returns the signature in the standard format: r (32 bytes) + s (32 bytes) + v (1 byte)
|
|
135
|
+
*
|
|
136
|
+
* @param params - Object containing the message string
|
|
137
|
+
* @returns The signature as a hex string (0x-prefixed, 130 characters)
|
|
138
|
+
* @throws {KmsClientError} If KMS API call fails
|
|
139
|
+
* @throws {DerParsingError} If signature format is invalid
|
|
140
|
+
* @throws {RecoveryIdCalculationError} If recovery ID calculation fails
|
|
141
|
+
*
|
|
142
|
+
* @example
|
|
143
|
+
* ```typescript
|
|
144
|
+
* const signer = new GcpSigner({
|
|
145
|
+
* projectId: 'my-project',
|
|
146
|
+
* locationId: 'global',
|
|
147
|
+
* keyRingId: 'my-keyring',
|
|
148
|
+
* keyId: 'my-key',
|
|
149
|
+
* keyVersion: '1'
|
|
150
|
+
* })
|
|
151
|
+
* const signature = await signer.signMessage({ message: 'Hello, world!' })
|
|
152
|
+
* // signature: '0x...' (130 characters: 0x + 64 hex chars for r + 64 for s + 2 for v)
|
|
153
|
+
* ```
|
|
154
|
+
*/
|
|
155
|
+
async signMessage({ message }) {
|
|
156
|
+
// EIP-191 hashing (viem handles automatically)
|
|
157
|
+
const messageHash = hashMessage(message);
|
|
158
|
+
// Sign with GCP KMS
|
|
159
|
+
const { r, s } = await this.signHash(messageHash);
|
|
160
|
+
// Calculate recovery ID
|
|
161
|
+
const address = await this.getAddress();
|
|
162
|
+
const recoveryId = await calculateRecoveryId(messageHash, toHex(r, { size: 32 }), toHex(s, { size: 32 }), address);
|
|
163
|
+
// Calculate v value (Legacy, no chain)
|
|
164
|
+
const v = 27 + recoveryId;
|
|
165
|
+
// Serialize signature
|
|
166
|
+
return concat([
|
|
167
|
+
toHex(r, { size: 32 }),
|
|
168
|
+
toHex(s, { size: 32 }),
|
|
169
|
+
toHex(v, { size: 1 }),
|
|
170
|
+
]);
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Signs an Ethereum transaction.
|
|
174
|
+
*
|
|
175
|
+
* This method:
|
|
176
|
+
* 1. Serializes the transaction without signature fields (r, s, v)
|
|
177
|
+
* 2. Hashes the serialized transaction with keccak256
|
|
178
|
+
* 3. Signs the hash with GCP KMS
|
|
179
|
+
* 4. Calculates the recovery ID
|
|
180
|
+
* 5. Computes the v value (EIP-155 if chainId present, legacy otherwise)
|
|
181
|
+
* 6. Returns the fully serialized transaction with signature
|
|
182
|
+
*
|
|
183
|
+
* @param transaction - The transaction to sign
|
|
184
|
+
* @param options - Optional serializer function (defaults to viem's serializeTransaction)
|
|
185
|
+
* @returns The serialized signed transaction as a hex string
|
|
186
|
+
* @throws {KmsClientError} If KMS API call fails
|
|
187
|
+
* @throws {DerParsingError} If signature format is invalid
|
|
188
|
+
* @throws {RecoveryIdCalculationError} If recovery ID calculation fails
|
|
189
|
+
*
|
|
190
|
+
* @example
|
|
191
|
+
* ```typescript
|
|
192
|
+
* const signer = new GcpSigner({
|
|
193
|
+
* projectId: 'my-project',
|
|
194
|
+
* locationId: 'global',
|
|
195
|
+
* keyRingId: 'my-keyring',
|
|
196
|
+
* keyId: 'my-key',
|
|
197
|
+
* keyVersion: '1'
|
|
198
|
+
* })
|
|
199
|
+
* const signedTx = await signer.signTransaction({
|
|
200
|
+
* to: '0x...',
|
|
201
|
+
* value: parseEther('1'),
|
|
202
|
+
* chainId: 1
|
|
203
|
+
* })
|
|
204
|
+
* ```
|
|
205
|
+
*/
|
|
206
|
+
async signTransaction(transaction, { serializer = serializeTransaction, } = {}) {
|
|
207
|
+
// Serialize transaction for signing (without r, s, v)
|
|
208
|
+
const serializedTx = serializeTransaction({
|
|
209
|
+
...transaction,
|
|
210
|
+
r: undefined,
|
|
211
|
+
s: undefined,
|
|
212
|
+
v: undefined,
|
|
213
|
+
});
|
|
214
|
+
const hash = keccak256(serializedTx);
|
|
215
|
+
// Sign with GCP KMS
|
|
216
|
+
const { r, s } = await this.signHash(hash);
|
|
217
|
+
// Calculate recovery ID
|
|
218
|
+
const address = await this.getAddress();
|
|
219
|
+
const recoveryId = await calculateRecoveryId(hash, toHex(r, { size: 32 }), toHex(s, { size: 32 }), address);
|
|
220
|
+
// Calculate v value
|
|
221
|
+
const chainId = transaction.chainId;
|
|
222
|
+
const v = chainId
|
|
223
|
+
? BigInt(chainId * 2 + 35 + recoveryId) // EIP-155
|
|
224
|
+
: BigInt(27 + recoveryId); // Legacy
|
|
225
|
+
// Final serialization with signature
|
|
226
|
+
return serializer({
|
|
227
|
+
...transaction,
|
|
228
|
+
r: toHex(r, { size: 32 }),
|
|
229
|
+
s: toHex(s, { size: 32 }),
|
|
230
|
+
v,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Signs typed data according to EIP-712.
|
|
235
|
+
*
|
|
236
|
+
* This method:
|
|
237
|
+
* 1. Hashes the typed data using EIP-712 (domain separator + type hash)
|
|
238
|
+
* 2. Signs the hash with GCP KMS
|
|
239
|
+
* 3. Calculates the recovery ID
|
|
240
|
+
* 4. Returns the signature in the standard format: r (32 bytes) + s (32 bytes) + v (1 byte)
|
|
241
|
+
*
|
|
242
|
+
* @param typedData - The EIP-712 typed data to sign
|
|
243
|
+
* @returns The signature as a hex string (0x-prefixed, 130 characters)
|
|
244
|
+
* @throws {KmsClientError} If KMS API call fails
|
|
245
|
+
* @throws {DerParsingError} If signature format is invalid
|
|
246
|
+
* @throws {RecoveryIdCalculationError} If recovery ID calculation fails
|
|
247
|
+
*
|
|
248
|
+
* @example
|
|
249
|
+
* ```typescript
|
|
250
|
+
* const signer = new GcpSigner({
|
|
251
|
+
* projectId: 'my-project',
|
|
252
|
+
* locationId: 'global',
|
|
253
|
+
* keyRingId: 'my-keyring',
|
|
254
|
+
* keyId: 'my-key',
|
|
255
|
+
* keyVersion: '1'
|
|
256
|
+
* })
|
|
257
|
+
* const signature = await signer.signTypedData({
|
|
258
|
+
* domain: {
|
|
259
|
+
* name: 'MyApp',
|
|
260
|
+
* version: '1',
|
|
261
|
+
* chainId: 1,
|
|
262
|
+
* verifyingContract: '0x...'
|
|
263
|
+
* },
|
|
264
|
+
* types: {
|
|
265
|
+
* Person: [
|
|
266
|
+
* { name: 'name', type: 'string' },
|
|
267
|
+
* { name: 'wallet', type: 'address' }
|
|
268
|
+
* ]
|
|
269
|
+
* },
|
|
270
|
+
* primaryType: 'Person',
|
|
271
|
+
* message: {
|
|
272
|
+
* name: 'Alice',
|
|
273
|
+
* wallet: '0x...'
|
|
274
|
+
* }
|
|
275
|
+
* })
|
|
276
|
+
* ```
|
|
277
|
+
*/
|
|
278
|
+
async signTypedData(typedData) {
|
|
279
|
+
// EIP-712 hashing (viem handles domain separator and type hash)
|
|
280
|
+
const hash = hashTypedData(typedData);
|
|
281
|
+
// Sign with GCP KMS
|
|
282
|
+
const { r, s } = await this.signHash(hash);
|
|
283
|
+
// Calculate recovery ID
|
|
284
|
+
const address = await this.getAddress();
|
|
285
|
+
const recoveryId = await calculateRecoveryId(hash, toHex(r, { size: 32 }), toHex(s, { size: 32 }), address);
|
|
286
|
+
// Calculate v value (Legacy, no chain for typed data)
|
|
287
|
+
const v = 27 + recoveryId;
|
|
288
|
+
// Serialize signature
|
|
289
|
+
return concat([
|
|
290
|
+
toHex(r, { size: 32 }),
|
|
291
|
+
toHex(s, { size: 32 }),
|
|
292
|
+
toHex(v, { size: 1 }),
|
|
293
|
+
]);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
//# sourceMappingURL=signer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"signer.js","sourceRoot":"","sources":["../../src/gcp/signer.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export type { Address, Hex } from 'viem';
|
|
2
|
-
export { toKmsAccount } from './account';
|
|
2
|
+
export { toGcpKmsAccount, toKmsAccount } from './account';
|
|
3
3
|
export { DerParsingError, KmsClientError, KmsSignerError, RecoveryIdCalculationError, SignatureNormalizationError, } from './errors';
|
|
4
|
+
export { GcpSigner } from './gcp/signer';
|
|
4
5
|
export { KmsSigner } from './kms/signer';
|
|
5
|
-
export type { DerSignature, KmsConfig, SignatureData } from './types';
|
|
6
|
+
export type { DerSignature, GcpKmsConfig, KmsConfig, SignatureData, } from './types';
|
|
6
7
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,YAAY,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,MAAM,CAAC;AACzC,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,YAAY,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,MAAM,CAAC;AACzC,OAAO,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAE1D,OAAO,EACN,eAAe,EACf,cAAc,EACd,cAAc,EACd,0BAA0B,EAC1B,2BAA2B,GAC3B,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzC,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEzC,YAAY,EACX,YAAY,EACZ,YAAY,EACZ,SAAS,EACT,aAAa,GACb,MAAM,SAAS,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Main classes and functions
|
|
2
|
-
export { toKmsAccount } from './account';
|
|
2
|
+
export { toGcpKmsAccount, toKmsAccount } from './account';
|
|
3
3
|
// Errors
|
|
4
4
|
export { DerParsingError, KmsClientError, KmsSignerError, RecoveryIdCalculationError, SignatureNormalizationError, } from './errors';
|
|
5
|
+
export { GcpSigner } from './gcp/signer';
|
|
5
6
|
export { KmsSigner } from './kms/signer';
|
|
6
7
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,6BAA6B;AAI7B,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,6BAA6B;AAI7B,OAAO,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAC1D,SAAS;AACT,OAAO,EACN,eAAe,EACf,cAAc,EACd,cAAc,EACd,0BAA0B,EAC1B,2BAA2B,GAC3B,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzC,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC"}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -19,6 +19,36 @@ export interface KmsConfig {
|
|
|
19
19
|
secretAccessKey: string;
|
|
20
20
|
};
|
|
21
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* GCP KMS configuration for signing operations
|
|
24
|
+
*/
|
|
25
|
+
export interface GcpKmsConfig {
|
|
26
|
+
/**
|
|
27
|
+
* GCP project ID where the KMS key is located
|
|
28
|
+
*/
|
|
29
|
+
projectId: string;
|
|
30
|
+
/**
|
|
31
|
+
* GCP location/region (e.g., "global", "us-east1")
|
|
32
|
+
*/
|
|
33
|
+
locationId: string;
|
|
34
|
+
/**
|
|
35
|
+
* Key ring ID containing the crypto key
|
|
36
|
+
*/
|
|
37
|
+
keyRingId: string;
|
|
38
|
+
/**
|
|
39
|
+
* Crypto key ID to use for signing
|
|
40
|
+
*/
|
|
41
|
+
keyId: string;
|
|
42
|
+
/**
|
|
43
|
+
* Crypto key version number (e.g., "1")
|
|
44
|
+
*/
|
|
45
|
+
keyVersion: string;
|
|
46
|
+
/**
|
|
47
|
+
* Optional path to service account key file.
|
|
48
|
+
* If not provided, uses GOOGLE_APPLICATION_CREDENTIALS environment variable
|
|
49
|
+
*/
|
|
50
|
+
keyFilename?: string;
|
|
51
|
+
}
|
|
22
52
|
/**
|
|
23
53
|
* DER-parsed signature as raw byte arrays
|
|
24
54
|
* Used internally by DER parsing utilities
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,WAAW,SAAS;IACzB;;OAEG;IACH,MAAM,EAAE,MAAM,CAAC;IAEf;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;IAEd;;;OAGG;IACH,WAAW,CAAC,EAAE;QACb,WAAW,EAAE,MAAM,CAAC;QACpB,eAAe,EAAE,MAAM,CAAC;KACxB,CAAC;CACF;AAED;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC5B;;OAEG;IACH,CAAC,EAAE,UAAU,CAAC;IAEd;;OAEG;IACH,CAAC,EAAE,UAAU,CAAC;CACd;AAED;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC7B;;OAEG;IACH,CAAC,EAAE,MAAM,CAAC;IAEV;;OAEG;IACH,CAAC,EAAE,MAAM,CAAC;IAEV;;;;OAIG;IACH,CAAC,EAAE,MAAM,CAAC;CACV"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,WAAW,SAAS;IACzB;;OAEG;IACH,MAAM,EAAE,MAAM,CAAC;IAEf;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;IAEd;;;OAGG;IACH,WAAW,CAAC,EAAE;QACb,WAAW,EAAE,MAAM,CAAC;QACpB,eAAe,EAAE,MAAM,CAAC;KACxB,CAAC;CACF;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC5B;;OAEG;IACH,SAAS,EAAE,MAAM,CAAC;IAElB;;OAEG;IACH,UAAU,EAAE,MAAM,CAAC;IAEnB;;OAEG;IACH,SAAS,EAAE,MAAM,CAAC;IAElB;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;IAEd;;OAEG;IACH,UAAU,EAAE,MAAM,CAAC;IAEnB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC5B;;OAEG;IACH,CAAC,EAAE,UAAU,CAAC;IAEd;;OAEG;IACH,CAAC,EAAE,UAAU,CAAC;CACd;AAED;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC7B;;OAEG;IACH,CAAC,EAAE,MAAM,CAAC;IAEV;;OAEG;IACH,CAAC,EAAE,MAAM,CAAC;IAEV;;;;OAIG;IACH,CAAC,EAAE,MAAM,CAAC;CACV"}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "evm-kms-signer",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "AWS KMS-based Ethereum signer for viem with enterprise-grade security. Sign transactions and messages using keys stored in AWS KMS without exposing private keys.",
|
|
5
|
+
"description": "AWS/GCP KMS-based Ethereum signer for viem with enterprise-grade security. Sign transactions and messages using keys stored in AWS or GCP KMS without exposing private keys.",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
8
8
|
"exports": {
|
|
@@ -21,6 +21,8 @@
|
|
|
21
21
|
"ethereum",
|
|
22
22
|
"aws",
|
|
23
23
|
"kms",
|
|
24
|
+
"gcp",
|
|
25
|
+
"google-cloud",
|
|
24
26
|
"viem",
|
|
25
27
|
"signer",
|
|
26
28
|
"evm",
|
|
@@ -47,6 +49,7 @@
|
|
|
47
49
|
},
|
|
48
50
|
"dependencies": {
|
|
49
51
|
"@aws-sdk/client-kms": "^3.0.0",
|
|
52
|
+
"@google-cloud/kms": "^5.2.1",
|
|
50
53
|
"abitype": "^1.1.1"
|
|
51
54
|
},
|
|
52
55
|
"devDependencies": {
|
|
@@ -70,6 +73,8 @@
|
|
|
70
73
|
"test:run": "vitest run",
|
|
71
74
|
"test:coverage": "vitest run --coverage",
|
|
72
75
|
"example:sign": "tsx examples/sign-message.ts",
|
|
73
|
-
"example:tx": "tsx examples/send-transaction.ts"
|
|
76
|
+
"example:tx": "tsx examples/send-transaction.ts",
|
|
77
|
+
"example:gcp-sign": "tsx examples/gcp-sign-message.ts",
|
|
78
|
+
"example:gcp-tx": "tsx examples/gcp-send-transaction.ts"
|
|
74
79
|
}
|
|
75
80
|
}
|