@solana/keychain-aws-kms 0.0.0 → 0.3.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 +176 -0
- package/dist/__tests__/aws-kms-signer.test.d.ts +2 -0
- package/dist/__tests__/aws-kms-signer.test.d.ts.map +1 -0
- package/dist/__tests__/aws-kms-signer.test.js +333 -0
- package/dist/__tests__/aws-kms-signer.test.js.map +1 -0
- package/dist/__tests__/setup.d.ts +3 -0
- package/dist/__tests__/setup.d.ts.map +1 -0
- package/dist/__tests__/setup.js +5 -0
- package/dist/__tests__/setup.js.map +1 -0
- package/dist/aws-kms-signer.d.ts +52 -0
- package/dist/aws-kms-signer.d.ts.map +1 -0
- package/dist/aws-kms-signer.js +174 -0
- package/dist/aws-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 +39 -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 -9
- package/src/__tests__/aws-kms-signer.test.ts +425 -0
- package/src/__tests__/setup.ts +5 -0
- package/src/aws-kms-signer.ts +214 -0
- package/src/index.ts +2 -0
- package/src/types.ts +40 -0
- package/index.js +0 -1
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
import { generateKeyPairSigner } from '@solana/signers';
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { assertIsSolanaSigner } from '@solana/keychain-core';
|
|
4
|
+
|
|
5
|
+
import { AwsKmsSigner } from '../aws-kms-signer.js';
|
|
6
|
+
import type { AwsKmsSignerConfig } from '../types.js';
|
|
7
|
+
|
|
8
|
+
// Mock AWS SDK
|
|
9
|
+
const mockSend = vi.fn();
|
|
10
|
+
|
|
11
|
+
vi.mock('@aws-sdk/client-kms', () => {
|
|
12
|
+
// Create proper constructor mock classes
|
|
13
|
+
class MockKMSClient {
|
|
14
|
+
send = mockSend;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
class MockSignCommand {
|
|
18
|
+
input: unknown;
|
|
19
|
+
constructor(params: unknown) {
|
|
20
|
+
this.input = params;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
class MockDescribeKeyCommand {
|
|
25
|
+
input: unknown;
|
|
26
|
+
constructor(params: unknown) {
|
|
27
|
+
this.input = params;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
KMSClient: MockKMSClient,
|
|
33
|
+
SignCommand: MockSignCommand,
|
|
34
|
+
DescribeKeyCommand: MockDescribeKeyCommand,
|
|
35
|
+
MessageType: {
|
|
36
|
+
RAW: 'RAW',
|
|
37
|
+
DIGEST: 'DIGEST',
|
|
38
|
+
},
|
|
39
|
+
SigningAlgorithmSpec: {
|
|
40
|
+
ED25519_SHA_512: 'ED25519_SHA_512',
|
|
41
|
+
ED25519_PH_SHA_512: 'ED25519_PH_SHA_512',
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe('AwsKmsSigner', () => {
|
|
47
|
+
const TEST_KEY_ID = 'arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012';
|
|
48
|
+
|
|
49
|
+
beforeEach(() => {
|
|
50
|
+
vi.clearAllMocks();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('constructor', () => {
|
|
54
|
+
it('creates an AwsKmsSigner with valid config', async () => {
|
|
55
|
+
const keyPair = await generateKeyPairSigner();
|
|
56
|
+
|
|
57
|
+
const config: AwsKmsSignerConfig = {
|
|
58
|
+
keyId: TEST_KEY_ID,
|
|
59
|
+
publicKey: keyPair.address,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const signer = new AwsKmsSigner(config);
|
|
63
|
+
|
|
64
|
+
expect(signer.address).toBe(keyPair.address);
|
|
65
|
+
assertIsSolanaSigner(signer);
|
|
66
|
+
expect(signer.signMessages).toBeDefined();
|
|
67
|
+
expect(signer.signTransactions).toBeDefined();
|
|
68
|
+
expect(signer.isAvailable).toBeDefined();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('sets address field correctly from config', async () => {
|
|
72
|
+
const keyPair = await generateKeyPairSigner();
|
|
73
|
+
|
|
74
|
+
const config: AwsKmsSignerConfig = {
|
|
75
|
+
keyId: TEST_KEY_ID,
|
|
76
|
+
publicKey: keyPair.address,
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const signer = new AwsKmsSigner(config);
|
|
80
|
+
expect(signer.address).toBe(keyPair.address);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should throw error for missing keyId', async () => {
|
|
84
|
+
const keyPair = await generateKeyPairSigner();
|
|
85
|
+
|
|
86
|
+
expect(() => {
|
|
87
|
+
new AwsKmsSigner({
|
|
88
|
+
keyId: '',
|
|
89
|
+
publicKey: keyPair.address,
|
|
90
|
+
});
|
|
91
|
+
}).toThrow('Missing required keyId field');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should throw error for missing publicKey', () => {
|
|
95
|
+
expect(() => {
|
|
96
|
+
new AwsKmsSigner({
|
|
97
|
+
keyId: TEST_KEY_ID,
|
|
98
|
+
publicKey: '',
|
|
99
|
+
});
|
|
100
|
+
}).toThrow('Missing required publicKey field');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should throw error for invalid public key', () => {
|
|
104
|
+
expect(() => {
|
|
105
|
+
new AwsKmsSigner({
|
|
106
|
+
keyId: TEST_KEY_ID,
|
|
107
|
+
publicKey: 'invalid-key',
|
|
108
|
+
});
|
|
109
|
+
}).toThrow('Invalid Solana public key format');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('should validate requestDelayMs', async () => {
|
|
113
|
+
const keyPair = await generateKeyPairSigner();
|
|
114
|
+
|
|
115
|
+
expect(() => {
|
|
116
|
+
new AwsKmsSigner({
|
|
117
|
+
keyId: TEST_KEY_ID,
|
|
118
|
+
publicKey: keyPair.address,
|
|
119
|
+
requestDelayMs: -1,
|
|
120
|
+
});
|
|
121
|
+
}).toThrow('requestDelayMs must not be negative');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('should warn for high requestDelayMs', async () => {
|
|
125
|
+
const keyPair = await generateKeyPairSigner();
|
|
126
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
127
|
+
|
|
128
|
+
new AwsKmsSigner({
|
|
129
|
+
keyId: TEST_KEY_ID,
|
|
130
|
+
publicKey: keyPair.address,
|
|
131
|
+
requestDelayMs: 5000,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('requestDelayMs is greater than 3000ms'));
|
|
135
|
+
|
|
136
|
+
warnSpy.mockRestore();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('should accept region configuration', async () => {
|
|
140
|
+
const keyPair = await generateKeyPairSigner();
|
|
141
|
+
|
|
142
|
+
const signer = new AwsKmsSigner({
|
|
143
|
+
keyId: TEST_KEY_ID,
|
|
144
|
+
publicKey: keyPair.address,
|
|
145
|
+
region: 'us-west-2',
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
expect(signer).toBeDefined();
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('should accept credentials configuration', async () => {
|
|
152
|
+
const keyPair = await generateKeyPairSigner();
|
|
153
|
+
|
|
154
|
+
const signer = new AwsKmsSigner({
|
|
155
|
+
keyId: TEST_KEY_ID,
|
|
156
|
+
publicKey: keyPair.address,
|
|
157
|
+
credentials: {
|
|
158
|
+
accessKeyId: 'test-access-key',
|
|
159
|
+
secretAccessKey: 'test-secret-key',
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
expect(signer).toBeDefined();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('should accept session token in credentials', async () => {
|
|
167
|
+
const keyPair = await generateKeyPairSigner();
|
|
168
|
+
|
|
169
|
+
const signer = new AwsKmsSigner({
|
|
170
|
+
keyId: TEST_KEY_ID,
|
|
171
|
+
publicKey: keyPair.address,
|
|
172
|
+
credentials: {
|
|
173
|
+
accessKeyId: 'test-access-key',
|
|
174
|
+
secretAccessKey: 'test-secret-key',
|
|
175
|
+
sessionToken: 'test-session-token',
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
expect(signer).toBeDefined();
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
describe('signMessages', () => {
|
|
184
|
+
it('should sign a message successfully', async () => {
|
|
185
|
+
const keyPair = await generateKeyPairSigner();
|
|
186
|
+
|
|
187
|
+
mockSend.mockResolvedValue({
|
|
188
|
+
Signature: new Uint8Array(64).fill(0x42),
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
const signer = new AwsKmsSigner({
|
|
192
|
+
keyId: TEST_KEY_ID,
|
|
193
|
+
publicKey: keyPair.address,
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// Provide required 'signatures' property to satisfy the type
|
|
197
|
+
const message = {
|
|
198
|
+
content: new Uint8Array([1, 2, 3, 4]),
|
|
199
|
+
signatures: {},
|
|
200
|
+
};
|
|
201
|
+
const result = await signer.signMessages([message]);
|
|
202
|
+
|
|
203
|
+
expect(result).toHaveLength(1);
|
|
204
|
+
expect(result[0]?.[signer.address]).toBeDefined();
|
|
205
|
+
expect(mockSend).toHaveBeenCalledTimes(1);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('should handle multiple messages with delay', async () => {
|
|
209
|
+
const keyPair = await generateKeyPairSigner();
|
|
210
|
+
|
|
211
|
+
mockSend.mockResolvedValue({
|
|
212
|
+
Signature: new Uint8Array(64).fill(0x42),
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
const signer = new AwsKmsSigner({
|
|
216
|
+
keyId: TEST_KEY_ID,
|
|
217
|
+
publicKey: keyPair.address,
|
|
218
|
+
requestDelayMs: 10,
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
const messages = [
|
|
222
|
+
{ content: new Uint8Array([1]) },
|
|
223
|
+
{ content: new Uint8Array([2]) },
|
|
224
|
+
{ content: new Uint8Array([3]) },
|
|
225
|
+
];
|
|
226
|
+
|
|
227
|
+
// Add missing 'signatures' property for each message to satisfy the type requirement
|
|
228
|
+
const messagesWithSignatures = messages.map(msg => ({
|
|
229
|
+
...msg,
|
|
230
|
+
signatures: {},
|
|
231
|
+
}));
|
|
232
|
+
|
|
233
|
+
const startTime = Date.now();
|
|
234
|
+
const result = await signer.signMessages(messagesWithSignatures);
|
|
235
|
+
const endTime = Date.now();
|
|
236
|
+
|
|
237
|
+
expect(result).toHaveLength(3);
|
|
238
|
+
expect(mockSend).toHaveBeenCalledTimes(3);
|
|
239
|
+
// Should have some delay (at least 15ms for 2 delays of 10ms each)
|
|
240
|
+
expect(endTime - startTime).toBeGreaterThanOrEqual(15);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('should throw error on invalid signature length', async () => {
|
|
244
|
+
const keyPair = await generateKeyPairSigner();
|
|
245
|
+
|
|
246
|
+
mockSend.mockResolvedValue({
|
|
247
|
+
Signature: new Uint8Array(32), // Wrong length
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
const signer = new AwsKmsSigner({
|
|
251
|
+
keyId: TEST_KEY_ID,
|
|
252
|
+
publicKey: keyPair.address,
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
const message = { content: new Uint8Array([1, 2, 3, 4]), signatures: {} };
|
|
256
|
+
|
|
257
|
+
await expect(signer.signMessages([message])).rejects.toThrow('Invalid signature length');
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('should throw error on missing signature', async () => {
|
|
261
|
+
const keyPair = await generateKeyPairSigner();
|
|
262
|
+
|
|
263
|
+
mockSend.mockResolvedValue({
|
|
264
|
+
Signature: undefined,
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
const signer = new AwsKmsSigner({
|
|
268
|
+
keyId: TEST_KEY_ID,
|
|
269
|
+
publicKey: keyPair.address,
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
const message = { content: new Uint8Array([1, 2, 3, 4]), signatures: {} };
|
|
273
|
+
|
|
274
|
+
await expect(signer.signMessages([message])).rejects.toThrow('No signature in AWS KMS response');
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
describe('signTransactions', () => {
|
|
279
|
+
it('should sign a transaction successfully', async () => {
|
|
280
|
+
const keyPair = await generateKeyPairSigner();
|
|
281
|
+
|
|
282
|
+
mockSend.mockResolvedValue({
|
|
283
|
+
Signature: new Uint8Array(64).fill(0x42),
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
const signer = new AwsKmsSigner({
|
|
287
|
+
keyId: TEST_KEY_ID,
|
|
288
|
+
publicKey: keyPair.address,
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
const transaction = {
|
|
292
|
+
messageBytes: new Uint8Array([1, 2, 3, 4]),
|
|
293
|
+
signatures: {},
|
|
294
|
+
} as unknown as Parameters<typeof signer.signTransactions>[0][0];
|
|
295
|
+
|
|
296
|
+
const result = await signer.signTransactions([transaction]);
|
|
297
|
+
|
|
298
|
+
expect(result).toHaveLength(1);
|
|
299
|
+
expect(result[0]).toHaveProperty(signer.address);
|
|
300
|
+
expect(mockSend).toHaveBeenCalledTimes(1);
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
describe('isAvailable', () => {
|
|
305
|
+
it('should return true for valid Ed25519 key', async () => {
|
|
306
|
+
const keyPair = await generateKeyPairSigner();
|
|
307
|
+
|
|
308
|
+
mockSend.mockResolvedValue({
|
|
309
|
+
KeyMetadata: {
|
|
310
|
+
KeyId: TEST_KEY_ID,
|
|
311
|
+
KeySpec: 'ECC_NIST_EDWARDS25519',
|
|
312
|
+
KeyUsage: 'SIGN_VERIFY',
|
|
313
|
+
KeyState: 'Enabled',
|
|
314
|
+
},
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
const signer = new AwsKmsSigner({
|
|
318
|
+
keyId: TEST_KEY_ID,
|
|
319
|
+
publicKey: keyPair.address,
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
const available = await signer.isAvailable();
|
|
323
|
+
|
|
324
|
+
expect(available).toBe(true);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it('should return false for wrong key spec', async () => {
|
|
328
|
+
const keyPair = await generateKeyPairSigner();
|
|
329
|
+
|
|
330
|
+
mockSend.mockResolvedValue({
|
|
331
|
+
KeyMetadata: {
|
|
332
|
+
KeyId: TEST_KEY_ID,
|
|
333
|
+
KeySpec: 'RSA_2048',
|
|
334
|
+
KeyUsage: 'SIGN_VERIFY',
|
|
335
|
+
KeyState: 'Enabled',
|
|
336
|
+
},
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
const signer = new AwsKmsSigner({
|
|
340
|
+
keyId: TEST_KEY_ID,
|
|
341
|
+
publicKey: keyPair.address,
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
const available = await signer.isAvailable();
|
|
345
|
+
|
|
346
|
+
expect(available).toBe(false);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it('should return false for wrong key usage', async () => {
|
|
350
|
+
const keyPair = await generateKeyPairSigner();
|
|
351
|
+
|
|
352
|
+
mockSend.mockResolvedValue({
|
|
353
|
+
KeyMetadata: {
|
|
354
|
+
KeyId: TEST_KEY_ID,
|
|
355
|
+
KeySpec: 'ECC_NIST_EDWARDS25519',
|
|
356
|
+
KeyUsage: 'ENCRYPT_DECRYPT',
|
|
357
|
+
KeyState: 'Enabled',
|
|
358
|
+
},
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
const signer = new AwsKmsSigner({
|
|
362
|
+
keyId: TEST_KEY_ID,
|
|
363
|
+
publicKey: keyPair.address,
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
const available = await signer.isAvailable();
|
|
367
|
+
|
|
368
|
+
expect(available).toBe(false);
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it('should return false for disabled key', async () => {
|
|
372
|
+
const keyPair = await generateKeyPairSigner();
|
|
373
|
+
|
|
374
|
+
mockSend.mockResolvedValue({
|
|
375
|
+
KeyMetadata: {
|
|
376
|
+
KeyId: TEST_KEY_ID,
|
|
377
|
+
KeySpec: 'ECC_NIST_EDWARDS25519',
|
|
378
|
+
KeyUsage: 'SIGN_VERIFY',
|
|
379
|
+
KeyState: 'Disabled',
|
|
380
|
+
},
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
const signer = new AwsKmsSigner({
|
|
384
|
+
keyId: TEST_KEY_ID,
|
|
385
|
+
publicKey: keyPair.address,
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
const available = await signer.isAvailable();
|
|
389
|
+
|
|
390
|
+
expect(available).toBe(false);
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
it('should return false on error', async () => {
|
|
394
|
+
const keyPair = await generateKeyPairSigner();
|
|
395
|
+
|
|
396
|
+
mockSend.mockRejectedValue(new Error('AWS error'));
|
|
397
|
+
|
|
398
|
+
const signer = new AwsKmsSigner({
|
|
399
|
+
keyId: TEST_KEY_ID,
|
|
400
|
+
publicKey: keyPair.address,
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
const available = await signer.isAvailable();
|
|
404
|
+
|
|
405
|
+
expect(available).toBe(false);
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
it('should return false on missing metadata', async () => {
|
|
409
|
+
const keyPair = await generateKeyPairSigner();
|
|
410
|
+
|
|
411
|
+
mockSend.mockResolvedValue({
|
|
412
|
+
KeyMetadata: undefined,
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
const signer = new AwsKmsSigner({
|
|
416
|
+
keyId: TEST_KEY_ID,
|
|
417
|
+
publicKey: keyPair.address,
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
const available = await signer.isAvailable();
|
|
421
|
+
|
|
422
|
+
expect(available).toBe(false);
|
|
423
|
+
});
|
|
424
|
+
});
|
|
425
|
+
});
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { DescribeKeyCommand, KMSClient, MessageType, SignCommand, SigningAlgorithmSpec } from '@aws-sdk/client-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 { AwsCredentials, AwsKmsSignerConfig } from './types.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* AWS KMS-based signer using EdDSA (Ed25519) signing
|
|
12
|
+
*
|
|
13
|
+
* The AWS KMS key must be created with:
|
|
14
|
+
* - Key spec: ECC_NIST_EDWARDS25519
|
|
15
|
+
* - Key usage: SIGN_VERIFY
|
|
16
|
+
*
|
|
17
|
+
* Example AWS CLI command to create a key:
|
|
18
|
+
* ```bash
|
|
19
|
+
* aws kms create-key \
|
|
20
|
+
* --key-spec ECC_NIST_EDWARDS25519 \
|
|
21
|
+
* --key-usage SIGN_VERIFY \
|
|
22
|
+
* --description "Solana signing key"
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export class AwsKmsSigner<TAddress extends string = string> implements SolanaSigner<TAddress> {
|
|
26
|
+
readonly address: Address<TAddress>;
|
|
27
|
+
private readonly keyId: string;
|
|
28
|
+
private readonly client: KMSClient;
|
|
29
|
+
private readonly requestDelayMs: number;
|
|
30
|
+
|
|
31
|
+
constructor(config: AwsKmsSignerConfig) {
|
|
32
|
+
if (!config.keyId) {
|
|
33
|
+
throwSignerError(SignerErrorCode.CONFIG_ERROR, {
|
|
34
|
+
message: 'Missing required keyId field',
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!config.publicKey) {
|
|
39
|
+
throwSignerError(SignerErrorCode.CONFIG_ERROR, {
|
|
40
|
+
message: 'Missing required publicKey field',
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
assertIsAddress(config.publicKey);
|
|
46
|
+
this.address = config.publicKey as Address<TAddress>;
|
|
47
|
+
} catch (error) {
|
|
48
|
+
throwSignerError(SignerErrorCode.CONFIG_ERROR, {
|
|
49
|
+
cause: error,
|
|
50
|
+
message: 'Invalid Solana public key format',
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
this.keyId = config.keyId;
|
|
55
|
+
this.requestDelayMs = config.requestDelayMs || 0;
|
|
56
|
+
this.validateRequestDelayMs(this.requestDelayMs);
|
|
57
|
+
|
|
58
|
+
// Create AWS KMS client
|
|
59
|
+
const clientConfig: {
|
|
60
|
+
credentials?: AwsCredentials;
|
|
61
|
+
region?: string;
|
|
62
|
+
} = {};
|
|
63
|
+
|
|
64
|
+
if (config.region) {
|
|
65
|
+
clientConfig.region = config.region;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (config.credentials) {
|
|
69
|
+
clientConfig.credentials = config.credentials;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
this.client = new KMSClient(clientConfig);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Validate request delay ms
|
|
77
|
+
*/
|
|
78
|
+
private validateRequestDelayMs(requestDelayMs: number): void {
|
|
79
|
+
if (requestDelayMs < 0) {
|
|
80
|
+
throwSignerError(SignerErrorCode.CONFIG_ERROR, {
|
|
81
|
+
message: 'requestDelayMs must not be negative',
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
if (requestDelayMs > 3000) {
|
|
85
|
+
console.warn(
|
|
86
|
+
'requestDelayMs is greater than 3000ms, this may result in blockhash expiration errors for signing messages/transactions',
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Add delay between concurrent requests
|
|
93
|
+
*/
|
|
94
|
+
private async delay(index: number): Promise<void> {
|
|
95
|
+
if (this.requestDelayMs > 0 && index > 0) {
|
|
96
|
+
await new Promise(resolve => setTimeout(resolve, index * this.requestDelayMs));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Sign message bytes using AWS KMS EdDSA signing
|
|
102
|
+
*/
|
|
103
|
+
private async signBytes(messageBytes: Uint8Array): Promise<SignatureBytes> {
|
|
104
|
+
try {
|
|
105
|
+
const command = new SignCommand({
|
|
106
|
+
KeyId: this.keyId,
|
|
107
|
+
Message: messageBytes,
|
|
108
|
+
MessageType: MessageType.RAW,
|
|
109
|
+
SigningAlgorithm: SigningAlgorithmSpec.ED25519_SHA_512,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const response = await this.client.send(command);
|
|
113
|
+
|
|
114
|
+
if (!response.Signature) {
|
|
115
|
+
throwSignerError(SignerErrorCode.REMOTE_API_ERROR, {
|
|
116
|
+
message: 'No signature in AWS KMS response',
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Ed25519 signatures are 64 bytes
|
|
121
|
+
const signature = new Uint8Array(response.Signature);
|
|
122
|
+
if (signature.length !== 64) {
|
|
123
|
+
throwSignerError(SignerErrorCode.SIGNING_FAILED, {
|
|
124
|
+
message: `Invalid signature length: expected 64 bytes, got ${signature.length}`,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return signature as SignatureBytes;
|
|
129
|
+
} catch (error: unknown) {
|
|
130
|
+
// Re-throw SignerError as-is
|
|
131
|
+
if (error instanceof Error && error.name === 'SignerError') {
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
134
|
+
if (error instanceof Error) {
|
|
135
|
+
// AWS SDK errors
|
|
136
|
+
const awsError = error as { $metadata?: { httpStatusCode?: number }; message?: string; name?: string };
|
|
137
|
+
throwSignerError(SignerErrorCode.REMOTE_API_ERROR, {
|
|
138
|
+
cause: error,
|
|
139
|
+
message: `AWS KMS Sign operation failed: ${awsError.message || error.message}`,
|
|
140
|
+
status: awsError.$metadata?.httpStatusCode,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
throwSignerError(SignerErrorCode.REMOTE_API_ERROR, {
|
|
144
|
+
cause: error,
|
|
145
|
+
message: 'AWS KMS Sign operation failed',
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Sign multiple messages using AWS KMS
|
|
152
|
+
*/
|
|
153
|
+
async signMessages(messages: readonly SignableMessage[]): Promise<readonly SignatureDictionary[]> {
|
|
154
|
+
return await Promise.all(
|
|
155
|
+
messages.map(async (message, index) => {
|
|
156
|
+
await this.delay(index);
|
|
157
|
+
const messageBytes =
|
|
158
|
+
message.content instanceof Uint8Array
|
|
159
|
+
? message.content
|
|
160
|
+
: new Uint8Array(Array.from(message.content));
|
|
161
|
+
const signatureBytes = await this.signBytes(messageBytes);
|
|
162
|
+
return createSignatureDictionary({
|
|
163
|
+
signature: signatureBytes,
|
|
164
|
+
signerAddress: this.address,
|
|
165
|
+
});
|
|
166
|
+
}),
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Sign multiple transactions using AWS KMS
|
|
172
|
+
*/
|
|
173
|
+
async signTransactions(
|
|
174
|
+
transactions: readonly (Transaction & TransactionWithinSizeLimit & TransactionWithLifetime)[],
|
|
175
|
+
): Promise<readonly SignatureDictionary[]> {
|
|
176
|
+
return await Promise.all(
|
|
177
|
+
transactions.map(async (transaction, index) => {
|
|
178
|
+
await this.delay(index);
|
|
179
|
+
// Sign the transaction message bytes
|
|
180
|
+
const signatureBytes = await this.signBytes(new Uint8Array(transaction.messageBytes));
|
|
181
|
+
return createSignatureDictionary({
|
|
182
|
+
signature: signatureBytes,
|
|
183
|
+
signerAddress: this.address,
|
|
184
|
+
});
|
|
185
|
+
}),
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Check if AWS KMS is available and the key is accessible
|
|
191
|
+
*/
|
|
192
|
+
async isAvailable(): Promise<boolean> {
|
|
193
|
+
try {
|
|
194
|
+
const command = new DescribeKeyCommand({
|
|
195
|
+
KeyId: this.keyId,
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
const response = await this.client.send(command);
|
|
199
|
+
|
|
200
|
+
if (!response.KeyMetadata) {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Verify the key spec is ECC_NIST_EDWARDS25519
|
|
205
|
+
const keySpec = response.KeyMetadata.KeySpec;
|
|
206
|
+
const keyUsage = response.KeyMetadata.KeyUsage;
|
|
207
|
+
const keyState = response.KeyMetadata.KeyState;
|
|
208
|
+
|
|
209
|
+
return keySpec === 'ECC_NIST_EDWARDS25519' && keyUsage === 'SIGN_VERIFY' && keyState === 'Enabled';
|
|
210
|
+
} catch {
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
package/src/index.ts
ADDED
package/src/types.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration for creating an AwsKmsSigner
|
|
3
|
+
*/
|
|
4
|
+
export interface AwsKmsSignerConfig {
|
|
5
|
+
/** Optional AWS credentials (defaults to default credential provider chain) */
|
|
6
|
+
credentials?: AwsCredentials;
|
|
7
|
+
/** AWS KMS key ID or ARN (must be an ECC_NIST_EDWARDS25519 key) */
|
|
8
|
+
keyId: string;
|
|
9
|
+
/** Solana public key (base58) corresponding to the AWS KMS key */
|
|
10
|
+
publicKey: string;
|
|
11
|
+
/** Optional AWS region (defaults to default region from AWS config) */
|
|
12
|
+
region?: string;
|
|
13
|
+
/** Optional delay in ms between concurrent signing requests to avoid rate limits (default: 0) */
|
|
14
|
+
requestDelayMs?: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* AWS credentials configuration
|
|
19
|
+
*/
|
|
20
|
+
export interface AwsCredentials {
|
|
21
|
+
accessKeyId: string;
|
|
22
|
+
secretAccessKey: string;
|
|
23
|
+
sessionToken?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* AWS KMS key metadata for validation
|
|
28
|
+
*/
|
|
29
|
+
export interface KmsKeyMetadata {
|
|
30
|
+
/** The AWS account ID that owns the key */
|
|
31
|
+
AWSAccountId?: string;
|
|
32
|
+
/** The key ID */
|
|
33
|
+
KeyId: string;
|
|
34
|
+
/** The key spec (must be ECC_NIST_EDWARDS25519 for EdDSA) */
|
|
35
|
+
KeySpec: 'ECC_NIST_EDWARDS25519';
|
|
36
|
+
/** The key state */
|
|
37
|
+
KeyState: 'Disabled' | 'Enabled' | 'PendingDeletion' | 'PendingImport';
|
|
38
|
+
/** The key usage (must be SIGN_VERIFY for signing) */
|
|
39
|
+
KeyUsage: 'SIGN_VERIFY';
|
|
40
|
+
}
|
package/index.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
module.exports = {};
|