@solana/keychain-gcp-kms 0.0.0 → 0.4.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 +103 -0
- package/dist/__tests__/gcp-kms-signer.integration.test.d.ts +2 -0
- package/dist/__tests__/gcp-kms-signer.integration.test.d.ts.map +1 -0
- package/dist/__tests__/gcp-kms-signer.integration.test.js +49 -0
- package/dist/__tests__/gcp-kms-signer.integration.test.js.map +1 -0
- package/dist/__tests__/gcp-kms-signer.test.d.ts +2 -0
- package/dist/__tests__/gcp-kms-signer.test.d.ts.map +1 -0
- package/dist/__tests__/gcp-kms-signer.test.js +286 -0
- package/dist/__tests__/gcp-kms-signer.test.js.map +1 -0
- package/dist/gcp-kms-signer.d.ts +53 -0
- package/dist/gcp-kms-signer.d.ts.map +1 -0
- package/dist/gcp-kms-signer.js +161 -0
- package/dist/gcp-kms-signer.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +12 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +55 -8
- package/src/__tests__/gcp-kms-signer.integration.test.ts +82 -0
- package/src/__tests__/gcp-kms-signer.test.ts +361 -0
- package/src/gcp-kms-signer.ts +191 -0
- package/src/index.ts +2 -0
- package/src/types.ts +11 -0
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
import { address } from '@solana/addresses';
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { assertIsSolanaSigner } from '@solana/keychain-core';
|
|
4
|
+
|
|
5
|
+
import { GcpKmsSigner } from '../gcp-kms-signer.js';
|
|
6
|
+
import type { GcpKmsSignerConfig } from '../types.js';
|
|
7
|
+
|
|
8
|
+
// Mock GCP KMS SDK
|
|
9
|
+
const mockAsymmetricSign = vi.fn();
|
|
10
|
+
const mockGetPublicKey = vi.fn();
|
|
11
|
+
|
|
12
|
+
vi.mock('@google-cloud/kms', () => {
|
|
13
|
+
return {
|
|
14
|
+
v1: {
|
|
15
|
+
KeyManagementServiceClient: class {
|
|
16
|
+
asymmetricSign = mockAsymmetricSign;
|
|
17
|
+
getPublicKey = mockGetPublicKey;
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe('GcpKmsSigner', () => {
|
|
24
|
+
const TEST_KEY_NAME =
|
|
25
|
+
'projects/test-project/locations/us-east1/keyRings/test-ring/cryptoKeys/test-key/cryptoKeyVersions/1';
|
|
26
|
+
const TEST_PUBLIC_KEY = address('11111111111111111111111111111111');
|
|
27
|
+
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
vi.clearAllMocks();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('constructor', () => {
|
|
33
|
+
it('creates a GcpKmsSigner with valid config', () => {
|
|
34
|
+
const config: GcpKmsSignerConfig = {
|
|
35
|
+
keyName: TEST_KEY_NAME,
|
|
36
|
+
publicKey: TEST_PUBLIC_KEY,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const signer = new GcpKmsSigner(config);
|
|
40
|
+
|
|
41
|
+
expect(signer.address).toBe(TEST_PUBLIC_KEY);
|
|
42
|
+
assertIsSolanaSigner(signer);
|
|
43
|
+
expect(signer.signMessages).toBeDefined();
|
|
44
|
+
expect(signer.signTransactions).toBeDefined();
|
|
45
|
+
expect(signer.isAvailable).toBeDefined();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should throw error for missing keyName', () => {
|
|
49
|
+
expect(() => {
|
|
50
|
+
new GcpKmsSigner({
|
|
51
|
+
keyName: '',
|
|
52
|
+
publicKey: TEST_PUBLIC_KEY,
|
|
53
|
+
});
|
|
54
|
+
}).toThrow('Missing required keyName field');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should throw error for missing publicKey', () => {
|
|
58
|
+
expect(() => {
|
|
59
|
+
new GcpKmsSigner({
|
|
60
|
+
keyName: TEST_KEY_NAME,
|
|
61
|
+
publicKey: '',
|
|
62
|
+
});
|
|
63
|
+
}).toThrow('Missing required publicKey field');
|
|
64
|
+
});
|
|
65
|
+
it('should throw error for invalid public key', () => {
|
|
66
|
+
expect(() => {
|
|
67
|
+
new GcpKmsSigner({
|
|
68
|
+
keyName: TEST_KEY_NAME,
|
|
69
|
+
publicKey: 'invalid-key',
|
|
70
|
+
});
|
|
71
|
+
}).toThrow('Invalid Solana public key format');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should validate requestDelayMs', () => {
|
|
75
|
+
expect(() => {
|
|
76
|
+
new GcpKmsSigner({
|
|
77
|
+
keyName: TEST_KEY_NAME,
|
|
78
|
+
publicKey: TEST_PUBLIC_KEY,
|
|
79
|
+
requestDelayMs: -1,
|
|
80
|
+
});
|
|
81
|
+
}).toThrow('requestDelayMs must not be negative');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should warn for high requestDelayMs', () => {
|
|
85
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
86
|
+
|
|
87
|
+
new GcpKmsSigner({
|
|
88
|
+
keyName: TEST_KEY_NAME,
|
|
89
|
+
publicKey: TEST_PUBLIC_KEY,
|
|
90
|
+
requestDelayMs: 5000,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('requestDelayMs is greater than 3000ms'));
|
|
94
|
+
|
|
95
|
+
warnSpy.mockRestore();
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe('signMessages', () => {
|
|
100
|
+
it('should sign a message successfully', async () => {
|
|
101
|
+
mockAsymmetricSign.mockResolvedValue([
|
|
102
|
+
{
|
|
103
|
+
signature: new Uint8Array(64).fill(0x42),
|
|
104
|
+
},
|
|
105
|
+
]);
|
|
106
|
+
|
|
107
|
+
const signer = new GcpKmsSigner({
|
|
108
|
+
keyName: TEST_KEY_NAME,
|
|
109
|
+
publicKey: TEST_PUBLIC_KEY,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const message = {
|
|
113
|
+
content: new Uint8Array([1, 2, 3, 4]),
|
|
114
|
+
signatures: {},
|
|
115
|
+
};
|
|
116
|
+
const result = await signer.signMessages([message]);
|
|
117
|
+
|
|
118
|
+
expect(result).toHaveLength(1);
|
|
119
|
+
expect(result[0]?.[signer.address]).toBeDefined();
|
|
120
|
+
expect(mockAsymmetricSign).toHaveBeenCalledTimes(1);
|
|
121
|
+
expect(mockAsymmetricSign).toHaveBeenCalledWith({
|
|
122
|
+
data: message.content,
|
|
123
|
+
name: TEST_KEY_NAME,
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('should handle multiple messages with delay', async () => {
|
|
128
|
+
mockAsymmetricSign.mockResolvedValue([
|
|
129
|
+
{
|
|
130
|
+
signature: new Uint8Array(64).fill(0x42),
|
|
131
|
+
},
|
|
132
|
+
]);
|
|
133
|
+
|
|
134
|
+
const signer = new GcpKmsSigner({
|
|
135
|
+
keyName: TEST_KEY_NAME,
|
|
136
|
+
publicKey: TEST_PUBLIC_KEY,
|
|
137
|
+
requestDelayMs: 10,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const messages = [
|
|
141
|
+
{ content: new Uint8Array([1]), signatures: {} },
|
|
142
|
+
{ content: new Uint8Array([2]), signatures: {} },
|
|
143
|
+
{ content: new Uint8Array([3]), signatures: {} },
|
|
144
|
+
] as any;
|
|
145
|
+
|
|
146
|
+
const startTime = Date.now();
|
|
147
|
+
const result = await signer.signMessages(messages);
|
|
148
|
+
const endTime = Date.now();
|
|
149
|
+
|
|
150
|
+
expect(result).toHaveLength(3);
|
|
151
|
+
expect(mockAsymmetricSign).toHaveBeenCalledTimes(3);
|
|
152
|
+
// Should have some delay (at least 15ms for 2 delays of 10ms each)
|
|
153
|
+
expect(endTime - startTime).toBeGreaterThanOrEqual(15);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('should throw error on invalid signature length', async () => {
|
|
157
|
+
mockAsymmetricSign.mockResolvedValue([
|
|
158
|
+
{
|
|
159
|
+
signature: new Uint8Array(32), // Wrong length
|
|
160
|
+
},
|
|
161
|
+
]);
|
|
162
|
+
|
|
163
|
+
const signer = new GcpKmsSigner({
|
|
164
|
+
keyName: TEST_KEY_NAME,
|
|
165
|
+
publicKey: TEST_PUBLIC_KEY,
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const message = { content: new Uint8Array([1, 2, 3, 4]), signatures: {} };
|
|
169
|
+
|
|
170
|
+
await expect(signer.signMessages([message])).rejects.toThrow('Invalid signature length');
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('should throw error on missing signature', async () => {
|
|
174
|
+
mockAsymmetricSign.mockResolvedValue([{}]);
|
|
175
|
+
|
|
176
|
+
const signer = new GcpKmsSigner({
|
|
177
|
+
keyName: TEST_KEY_NAME,
|
|
178
|
+
publicKey: TEST_PUBLIC_KEY,
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
const message = { content: new Uint8Array([1, 2, 3, 4]), signatures: {} };
|
|
182
|
+
|
|
183
|
+
await expect(signer.signMessages([message])).rejects.toThrow('No signature in GCP KMS response');
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('should handle GCP KMS API errors', async () => {
|
|
187
|
+
const apiError = new Error('GCP Error');
|
|
188
|
+
(apiError as any).code = 403;
|
|
189
|
+
mockAsymmetricSign.mockRejectedValue(apiError);
|
|
190
|
+
|
|
191
|
+
const signer = new GcpKmsSigner({
|
|
192
|
+
keyName: TEST_KEY_NAME,
|
|
193
|
+
publicKey: TEST_PUBLIC_KEY,
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const message = { content: new Uint8Array([1, 2, 3, 4]), signatures: {} };
|
|
197
|
+
|
|
198
|
+
await expect(signer.signMessages([message])).rejects.toThrow('GCP KMS Sign operation failed: GCP Error');
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
describe('signTransactions', () => {
|
|
203
|
+
it('should sign a transaction successfully', async () => {
|
|
204
|
+
mockAsymmetricSign.mockResolvedValue([
|
|
205
|
+
{
|
|
206
|
+
signature: new Uint8Array(64).fill(0x42),
|
|
207
|
+
},
|
|
208
|
+
]);
|
|
209
|
+
|
|
210
|
+
const signer = new GcpKmsSigner({
|
|
211
|
+
keyName: TEST_KEY_NAME,
|
|
212
|
+
publicKey: TEST_PUBLIC_KEY,
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
const transaction = {
|
|
216
|
+
messageBytes: new Uint8Array([1, 2, 3, 4]),
|
|
217
|
+
signatures: {},
|
|
218
|
+
} as any;
|
|
219
|
+
|
|
220
|
+
const result = await signer.signTransactions([transaction]);
|
|
221
|
+
|
|
222
|
+
expect(result).toHaveLength(1);
|
|
223
|
+
expect(result[0]).toHaveProperty(signer.address);
|
|
224
|
+
expect(mockAsymmetricSign).toHaveBeenCalledTimes(1);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('should sign multiple transactions successfully', async () => {
|
|
228
|
+
mockAsymmetricSign.mockResolvedValue([
|
|
229
|
+
{
|
|
230
|
+
signature: new Uint8Array(64).fill(0x42),
|
|
231
|
+
},
|
|
232
|
+
]);
|
|
233
|
+
|
|
234
|
+
const signer = new GcpKmsSigner({
|
|
235
|
+
keyName: TEST_KEY_NAME,
|
|
236
|
+
publicKey: TEST_PUBLIC_KEY,
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
const transactions = [
|
|
240
|
+
{ messageBytes: new Uint8Array([1]), signatures: {} },
|
|
241
|
+
{ messageBytes: new Uint8Array([2]), signatures: {} },
|
|
242
|
+
] as any;
|
|
243
|
+
|
|
244
|
+
const result = await signer.signTransactions(transactions);
|
|
245
|
+
|
|
246
|
+
expect(result).toHaveLength(2);
|
|
247
|
+
expect(mockAsymmetricSign).toHaveBeenCalledTimes(2);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('should throw error on invalid signature length', async () => {
|
|
251
|
+
mockAsymmetricSign.mockResolvedValue([
|
|
252
|
+
{
|
|
253
|
+
signature: new Uint8Array(32),
|
|
254
|
+
},
|
|
255
|
+
]);
|
|
256
|
+
|
|
257
|
+
const signer = new GcpKmsSigner({
|
|
258
|
+
keyName: TEST_KEY_NAME,
|
|
259
|
+
publicKey: TEST_PUBLIC_KEY,
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
const transaction = { messageBytes: new Uint8Array([1, 2, 3, 4]), signatures: {} } as any;
|
|
263
|
+
|
|
264
|
+
await expect(signer.signTransactions([transaction])).rejects.toThrow('Invalid signature length');
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('should throw error on missing signature', async () => {
|
|
268
|
+
mockAsymmetricSign.mockResolvedValue([{}]);
|
|
269
|
+
|
|
270
|
+
const signer = new GcpKmsSigner({
|
|
271
|
+
keyName: TEST_KEY_NAME,
|
|
272
|
+
publicKey: TEST_PUBLIC_KEY,
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
const transaction = { messageBytes: new Uint8Array([1, 2, 3, 4]), signatures: {} } as any;
|
|
276
|
+
|
|
277
|
+
await expect(signer.signTransactions([transaction])).rejects.toThrow('No signature in GCP KMS response');
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('should handle GCP KMS API errors', async () => {
|
|
281
|
+
const apiError = new Error('GCP Error');
|
|
282
|
+
(apiError as any).code = 403;
|
|
283
|
+
mockAsymmetricSign.mockRejectedValue(apiError);
|
|
284
|
+
|
|
285
|
+
const signer = new GcpKmsSigner({
|
|
286
|
+
keyName: TEST_KEY_NAME,
|
|
287
|
+
publicKey: TEST_PUBLIC_KEY,
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
const transaction = { messageBytes: new Uint8Array([1, 2, 3, 4]), signatures: {} } as any;
|
|
291
|
+
|
|
292
|
+
await expect(signer.signTransactions([transaction])).rejects.toThrow(
|
|
293
|
+
'GCP KMS Sign operation failed: GCP Error',
|
|
294
|
+
);
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
describe('isAvailable', () => {
|
|
299
|
+
it('should return true for valid Ed25519 key', async () => {
|
|
300
|
+
mockGetPublicKey.mockResolvedValue([
|
|
301
|
+
{
|
|
302
|
+
name: TEST_KEY_NAME,
|
|
303
|
+
algorithm: 'EC_SIGN_ED25519',
|
|
304
|
+
},
|
|
305
|
+
]);
|
|
306
|
+
|
|
307
|
+
const signer = new GcpKmsSigner({
|
|
308
|
+
keyName: TEST_KEY_NAME,
|
|
309
|
+
publicKey: TEST_PUBLIC_KEY,
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
const available = await signer.isAvailable();
|
|
313
|
+
|
|
314
|
+
expect(available).toBe(true);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it('should return false for wrong algorithm', async () => {
|
|
318
|
+
mockGetPublicKey.mockResolvedValue([
|
|
319
|
+
{
|
|
320
|
+
name: TEST_KEY_NAME,
|
|
321
|
+
algorithm: 'RSA_SIGN_PKCS1_2048_SHA256',
|
|
322
|
+
},
|
|
323
|
+
]);
|
|
324
|
+
|
|
325
|
+
const signer = new GcpKmsSigner({
|
|
326
|
+
keyName: TEST_KEY_NAME,
|
|
327
|
+
publicKey: TEST_PUBLIC_KEY,
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
const available = await signer.isAvailable();
|
|
331
|
+
|
|
332
|
+
expect(available).toBe(false);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it('should return false for missing public key response', async () => {
|
|
336
|
+
mockGetPublicKey.mockResolvedValue([undefined]);
|
|
337
|
+
|
|
338
|
+
const signer = new GcpKmsSigner({
|
|
339
|
+
keyName: TEST_KEY_NAME,
|
|
340
|
+
publicKey: TEST_PUBLIC_KEY,
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
const available = await signer.isAvailable();
|
|
344
|
+
|
|
345
|
+
expect(available).toBe(false);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it('should return false on error', async () => {
|
|
349
|
+
mockGetPublicKey.mockRejectedValue(new Error('GCP error'));
|
|
350
|
+
|
|
351
|
+
const signer = new GcpKmsSigner({
|
|
352
|
+
keyName: TEST_KEY_NAME,
|
|
353
|
+
publicKey: TEST_PUBLIC_KEY,
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
const available = await signer.isAvailable();
|
|
357
|
+
|
|
358
|
+
expect(available).toBe(false);
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
});
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { v1 } from '@google-cloud/kms';
|
|
2
|
+
import { Address, assertIsAddress } from '@solana/addresses';
|
|
3
|
+
import { createSignatureDictionary, SignerErrorCode, SolanaSigner, throwSignerError } from '@solana/keychain-core';
|
|
4
|
+
import { SignatureBytes } from '@solana/keys';
|
|
5
|
+
import { SignableMessage, SignatureDictionary } from '@solana/signers';
|
|
6
|
+
import { Transaction, TransactionWithinSizeLimit, TransactionWithLifetime } from '@solana/transactions';
|
|
7
|
+
|
|
8
|
+
import type { GcpKmsSignerConfig } from './types.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Google Cloud KMS-based signer using EdDSA (Ed25519) signing
|
|
12
|
+
*
|
|
13
|
+
* The GCP KMS key must be created with:
|
|
14
|
+
* - Algorithm: EC_SIGN_ED25519
|
|
15
|
+
* - Purpose: ASYMMETRIC_SIGN
|
|
16
|
+
*
|
|
17
|
+
* Example gcloud CLI command to create a key:
|
|
18
|
+
* ```bash
|
|
19
|
+
* gcloud kms keys create my-key \
|
|
20
|
+
* --keyring=my-keyring \
|
|
21
|
+
* --location=us-east1 \
|
|
22
|
+
* --purpose=asymmetric-signing \
|
|
23
|
+
* --default-algorithm=ec-sign-ed25519
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export class GcpKmsSigner<TAddress extends string = string> implements SolanaSigner<TAddress> {
|
|
27
|
+
readonly address: Address<TAddress>;
|
|
28
|
+
private readonly keyName: string;
|
|
29
|
+
private readonly client: v1.KeyManagementServiceClient;
|
|
30
|
+
private readonly requestDelayMs: number;
|
|
31
|
+
|
|
32
|
+
constructor(config: GcpKmsSignerConfig) {
|
|
33
|
+
if (!config.keyName) {
|
|
34
|
+
throwSignerError(SignerErrorCode.CONFIG_ERROR, {
|
|
35
|
+
message: 'Missing required keyName field',
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!config.publicKey) {
|
|
40
|
+
throwSignerError(SignerErrorCode.CONFIG_ERROR, {
|
|
41
|
+
message: 'Missing required publicKey field',
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
assertIsAddress(config.publicKey);
|
|
47
|
+
this.address = config.publicKey as Address<TAddress>;
|
|
48
|
+
} catch (error) {
|
|
49
|
+
throwSignerError(SignerErrorCode.CONFIG_ERROR, {
|
|
50
|
+
cause: error,
|
|
51
|
+
message: 'Invalid Solana public key format',
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
this.keyName = config.keyName;
|
|
56
|
+
this.requestDelayMs = config.requestDelayMs || 0;
|
|
57
|
+
this.validateRequestDelayMs(this.requestDelayMs);
|
|
58
|
+
this.client = new v1.KeyManagementServiceClient();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Validate request delay ms
|
|
63
|
+
*/
|
|
64
|
+
private validateRequestDelayMs(requestDelayMs: number): void {
|
|
65
|
+
if (requestDelayMs < 0) {
|
|
66
|
+
throwSignerError(SignerErrorCode.CONFIG_ERROR, {
|
|
67
|
+
message: 'requestDelayMs must not be negative',
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
if (requestDelayMs > 3000) {
|
|
71
|
+
console.warn(
|
|
72
|
+
'requestDelayMs is greater than 3000ms, this may result in blockhash expiration errors for signing messages/transactions',
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Add delay between concurrent requests
|
|
79
|
+
*/
|
|
80
|
+
private async delay(index: number): Promise<void> {
|
|
81
|
+
if (this.requestDelayMs > 0 && index > 0) {
|
|
82
|
+
await new Promise(resolve => setTimeout(resolve, index * this.requestDelayMs));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Sign message bytes using GCP KMS EdDSA signing
|
|
88
|
+
*/
|
|
89
|
+
private async signBytes(messageBytes: Uint8Array): Promise<SignatureBytes> {
|
|
90
|
+
try {
|
|
91
|
+
// GCP KMS AsymmetricSign expects the raw message for Ed25519 (PureEdDSA)
|
|
92
|
+
const [response] = await this.client.asymmetricSign({
|
|
93
|
+
data: messageBytes,
|
|
94
|
+
name: this.keyName,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
if (!response.signature) {
|
|
98
|
+
throwSignerError(SignerErrorCode.REMOTE_API_ERROR, {
|
|
99
|
+
message: 'No signature in GCP KMS response',
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Ed25519 signatures are 64 bytes
|
|
104
|
+
const signature = response.signature as Uint8Array;
|
|
105
|
+
if (signature.length !== 64) {
|
|
106
|
+
throwSignerError(SignerErrorCode.SIGNING_FAILED, {
|
|
107
|
+
message: `Invalid signature length: expected 64 bytes, got ${signature.length}`,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return signature as SignatureBytes;
|
|
112
|
+
} catch (error: unknown) {
|
|
113
|
+
// Re-throw SignerError as-is
|
|
114
|
+
if (error instanceof Error && error.name === 'SignerError') {
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
if (error instanceof Error) {
|
|
118
|
+
// GCP SDK errors
|
|
119
|
+
const gcpError = error as { code?: number; message?: string };
|
|
120
|
+
throwSignerError(SignerErrorCode.REMOTE_API_ERROR, {
|
|
121
|
+
cause: error,
|
|
122
|
+
message: `GCP KMS Sign operation failed: ${gcpError.message || error.message}`,
|
|
123
|
+
status: gcpError.code,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
throwSignerError(SignerErrorCode.REMOTE_API_ERROR, {
|
|
127
|
+
cause: error,
|
|
128
|
+
message: 'GCP KMS Sign operation failed',
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Sign multiple messages using GCP KMS
|
|
135
|
+
*/
|
|
136
|
+
async signMessages(messages: readonly SignableMessage[]): Promise<readonly SignatureDictionary[]> {
|
|
137
|
+
return await Promise.all(
|
|
138
|
+
messages.map(async (message, index) => {
|
|
139
|
+
await this.delay(index);
|
|
140
|
+
const messageBytes =
|
|
141
|
+
message.content instanceof Uint8Array
|
|
142
|
+
? message.content
|
|
143
|
+
: new Uint8Array(Array.from(message.content));
|
|
144
|
+
const signatureBytes = await this.signBytes(messageBytes);
|
|
145
|
+
return createSignatureDictionary({
|
|
146
|
+
signature: signatureBytes,
|
|
147
|
+
signerAddress: this.address,
|
|
148
|
+
});
|
|
149
|
+
}),
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Sign multiple transactions using GCP KMS
|
|
155
|
+
*/
|
|
156
|
+
async signTransactions(
|
|
157
|
+
transactions: readonly (Transaction & TransactionWithinSizeLimit & TransactionWithLifetime)[],
|
|
158
|
+
): Promise<readonly SignatureDictionary[]> {
|
|
159
|
+
return await Promise.all(
|
|
160
|
+
transactions.map(async (transaction, index) => {
|
|
161
|
+
await this.delay(index);
|
|
162
|
+
// Sign the transaction message bytes
|
|
163
|
+
const signatureBytes = await this.signBytes(new Uint8Array(transaction.messageBytes));
|
|
164
|
+
return createSignatureDictionary({
|
|
165
|
+
signature: signatureBytes,
|
|
166
|
+
signerAddress: this.address,
|
|
167
|
+
});
|
|
168
|
+
}),
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Check if GCP KMS is available and the key is accessible
|
|
174
|
+
*/
|
|
175
|
+
async isAvailable(): Promise<boolean> {
|
|
176
|
+
try {
|
|
177
|
+
const [publicKey] = await this.client.getPublicKey({
|
|
178
|
+
name: this.keyName,
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
if (!publicKey) {
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Verify the algorithm is EC_SIGN_ED25519
|
|
186
|
+
return publicKey.algorithm === 'EC_SIGN_ED25519';
|
|
187
|
+
} catch {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
package/src/index.ts
ADDED
package/src/types.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration for creating a GcpKmsSigner
|
|
3
|
+
*/
|
|
4
|
+
export interface GcpKmsSignerConfig {
|
|
5
|
+
/** Full resource name of the crypto key version */
|
|
6
|
+
keyName: string;
|
|
7
|
+
/** Solana public key (base58-encoded) */
|
|
8
|
+
publicKey: string;
|
|
9
|
+
/** Optional delay in ms between concurrent signing requests to avoid rate limits (default: 0) */
|
|
10
|
+
requestDelayMs?: number;
|
|
11
|
+
}
|