solana-kms-signer 0.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/LICENSE +21 -0
- package/README.md +610 -0
- package/dist/errors/index.d.ts +54 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/index.js +63 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/kms/client.d.ts +60 -0
- package/dist/kms/client.d.ts.map +1 -0
- package/dist/kms/client.js +108 -0
- package/dist/kms/client.js.map +1 -0
- package/dist/kms/signer.d.ts +170 -0
- package/dist/kms/signer.d.ts.map +1 -0
- package/dist/kms/signer.js +230 -0
- package/dist/kms/signer.js.map +1 -0
- package/dist/types/index.d.ts +39 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/publicKey.d.ts +28 -0
- package/dist/utils/publicKey.d.ts.map +1 -0
- package/dist/utils/publicKey.js +56 -0
- package/dist/utils/publicKey.js.map +1 -0
- package/package.json +73 -0
- package/src/errors/index.test.ts +173 -0
- package/src/errors/index.ts +61 -0
- package/src/index.ts +27 -0
- package/src/kms/client.test.ts +285 -0
- package/src/kms/client.ts +132 -0
- package/src/kms/signer.test.ts +446 -0
- package/src/kms/signer.ts +274 -0
- package/src/types/index.ts +44 -0
- package/src/utils/publicKey.test.ts +135 -0
- package/src/utils/publicKey.ts +70 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom error class for AWS KMS API-related errors.
|
|
3
|
+
*
|
|
4
|
+
* Thrown when AWS KMS operations fail, including:
|
|
5
|
+
* - GetPublicKey failures (AccessDeniedException, KeyNotFoundException, etc.)
|
|
6
|
+
* - Sign failures (ThrottlingException, KeyUnavailableException, etc.)
|
|
7
|
+
* - Network timeouts and service unavailability
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* throw new KmsClientError('Failed to get public key from KMS', originalError);
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
export class KmsClientError extends Error {
|
|
15
|
+
constructor(message: string, public readonly cause?: unknown) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.name = 'KmsClientError';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Custom error class for DER-encoded public key extraction errors.
|
|
23
|
+
*
|
|
24
|
+
* Thrown when extracting ED25519 public key from DER format fails, including:
|
|
25
|
+
* - Invalid DER structure (missing SEQUENCE tag)
|
|
26
|
+
* - Missing BIT STRING in DER encoding
|
|
27
|
+
* - Unexpected BIT STRING length (not 33 bytes)
|
|
28
|
+
* - Malformed SubjectPublicKeyInfo structure
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```typescript
|
|
32
|
+
* throw new PublicKeyExtractionError('Invalid DER encoding: missing SEQUENCE tag');
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export class PublicKeyExtractionError extends Error {
|
|
36
|
+
constructor(message: string, public readonly cause?: unknown) {
|
|
37
|
+
super(message);
|
|
38
|
+
this.name = 'PublicKeyExtractionError';
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Custom error class for ED25519 signature verification errors.
|
|
44
|
+
*
|
|
45
|
+
* Thrown when signature validation fails, including:
|
|
46
|
+
* - Signature verification returns false (invalid signature)
|
|
47
|
+
* - Incorrect signature length (not 64 bytes)
|
|
48
|
+
* - Signature does not match public key and message
|
|
49
|
+
* - Cryptographic verification failure
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```typescript
|
|
53
|
+
* throw new SignatureVerificationError('Signature verification failed', { signature, publicKey });
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
export class SignatureVerificationError extends Error {
|
|
57
|
+
constructor(message: string, public readonly cause?: unknown) {
|
|
58
|
+
super(message);
|
|
59
|
+
this.name = 'SignatureVerificationError';
|
|
60
|
+
}
|
|
61
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Solana KMS Signer
|
|
3
|
+
*
|
|
4
|
+
* A TypeScript library for signing Solana transactions using AWS KMS with ED25519 keys.
|
|
5
|
+
*
|
|
6
|
+
* @module solana-kms-signer
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// Core classes
|
|
10
|
+
export { KmsClient } from './kms/client.js';
|
|
11
|
+
export { SolanaKmsSigner } from './kms/signer.js';
|
|
12
|
+
|
|
13
|
+
// Utility functions
|
|
14
|
+
export { extractEd25519PublicKey } from './utils/publicKey.js';
|
|
15
|
+
|
|
16
|
+
// Type definitions
|
|
17
|
+
export type { KmsConfig, SolanaKmsSignerConfig } from './types/index.js';
|
|
18
|
+
|
|
19
|
+
// Error classes
|
|
20
|
+
export {
|
|
21
|
+
KmsClientError,
|
|
22
|
+
PublicKeyExtractionError,
|
|
23
|
+
SignatureVerificationError,
|
|
24
|
+
} from './errors/index.js';
|
|
25
|
+
|
|
26
|
+
// Re-export commonly used Solana types for convenience
|
|
27
|
+
export { PublicKey, Transaction, VersionedTransaction } from '@solana/web3.js';
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { vi, beforeEach, describe, it, expect } from 'vitest';
|
|
2
|
+
import { KmsClient } from './client.js';
|
|
3
|
+
import { KmsClientError } from '../errors/index.js';
|
|
4
|
+
import type { KmsConfig } from '../types/index.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Mock DER-encoded ED25519 public key matching AWS KMS GetPublicKey format.
|
|
8
|
+
* Reuses the same structure as publicKey.test.ts for consistency.
|
|
9
|
+
*/
|
|
10
|
+
const MOCK_DER_PUBLIC_KEY = new Uint8Array([
|
|
11
|
+
0x30, 0x2a, // SEQUENCE, 42 bytes
|
|
12
|
+
0x30, 0x05, // AlgorithmIdentifier SEQUENCE, 5 bytes
|
|
13
|
+
0x06, 0x03, 0x2b, 0x65, 0x70, // OID: 1.3.101.112 (Ed25519)
|
|
14
|
+
0x03, 0x21, 0x00, // BIT STRING, 33 bytes (32 + 1 padding)
|
|
15
|
+
// Mock 32-byte ED25519 public key
|
|
16
|
+
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
|
|
17
|
+
0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10,
|
|
18
|
+
0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18,
|
|
19
|
+
0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20,
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Mock ED25519 signature (64 bytes)
|
|
24
|
+
* ED25519 signatures are always exactly 64 bytes
|
|
25
|
+
*/
|
|
26
|
+
const MOCK_SIGNATURE = new Uint8Array(64).fill(0xab);
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Mock KMS configuration for testing
|
|
30
|
+
*/
|
|
31
|
+
const MOCK_KMS_CONFIG: KmsConfig = {
|
|
32
|
+
region: 'us-east-1',
|
|
33
|
+
keyId: 'arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012',
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// Mock AWS SDK client and commands
|
|
37
|
+
const mockSend = vi.fn();
|
|
38
|
+
|
|
39
|
+
vi.mock('@aws-sdk/client-kms', () => {
|
|
40
|
+
return {
|
|
41
|
+
KMSClient: class {
|
|
42
|
+
send = mockSend;
|
|
43
|
+
},
|
|
44
|
+
GetPublicKeyCommand: class {
|
|
45
|
+
constructor(public params: any) {}
|
|
46
|
+
},
|
|
47
|
+
SignCommand: class {
|
|
48
|
+
constructor(public params: any) {}
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('KmsClient', () => {
|
|
54
|
+
beforeEach(() => {
|
|
55
|
+
// Reset mock state before each test
|
|
56
|
+
mockSend.mockReset();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('Happy Path', () => {
|
|
60
|
+
it('should successfully retrieve public key as DER-encoded Uint8Array', async () => {
|
|
61
|
+
// given: KmsClient instance and mocked successful KMS response
|
|
62
|
+
const client = new KmsClient(MOCK_KMS_CONFIG);
|
|
63
|
+
mockSend.mockResolvedValueOnce({
|
|
64
|
+
PublicKey: MOCK_DER_PUBLIC_KEY,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// when: Getting public key from KMS
|
|
68
|
+
const publicKey = await client.getPublicKey();
|
|
69
|
+
|
|
70
|
+
// then: Should return DER-encoded public key as Uint8Array
|
|
71
|
+
expect(publicKey).toBeInstanceOf(Uint8Array);
|
|
72
|
+
expect(publicKey.length).toBe(MOCK_DER_PUBLIC_KEY.length);
|
|
73
|
+
expect(publicKey).toEqual(MOCK_DER_PUBLIC_KEY);
|
|
74
|
+
|
|
75
|
+
// then: Should have called KMS with correct parameters
|
|
76
|
+
expect(mockSend).toHaveBeenCalledTimes(1);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should successfully sign message and return 64-byte signature', async () => {
|
|
80
|
+
// given: KmsClient instance and message to sign
|
|
81
|
+
const client = new KmsClient(MOCK_KMS_CONFIG);
|
|
82
|
+
const message = new Uint8Array([1, 2, 3, 4, 5]);
|
|
83
|
+
|
|
84
|
+
mockSend.mockResolvedValueOnce({
|
|
85
|
+
Signature: MOCK_SIGNATURE,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// when: Signing message with KMS
|
|
89
|
+
const signature = await client.sign(message);
|
|
90
|
+
|
|
91
|
+
// then: Should return 64-byte signature
|
|
92
|
+
expect(signature).toBeInstanceOf(Uint8Array);
|
|
93
|
+
expect(signature.length).toBe(64);
|
|
94
|
+
expect(signature).toEqual(MOCK_SIGNATURE);
|
|
95
|
+
|
|
96
|
+
// then: Should have called KMS with correct SigningAlgorithm
|
|
97
|
+
expect(mockSend).toHaveBeenCalledTimes(1);
|
|
98
|
+
const signCommand = mockSend.mock.calls[0][0];
|
|
99
|
+
expect(signCommand.params.SigningAlgorithm).toBe('ED25519_SHA_512');
|
|
100
|
+
expect(signCommand.params.MessageType).toBe('RAW');
|
|
101
|
+
expect(signCommand.params.Message).toEqual(message);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe('Failure Paths', () => {
|
|
106
|
+
it('should throw KmsClientError when getPublicKey receives AccessDeniedException', async () => {
|
|
107
|
+
// given: KmsClient and mocked AccessDeniedException
|
|
108
|
+
const client = new KmsClient(MOCK_KMS_CONFIG);
|
|
109
|
+
const awsError = new Error('User: arn:aws:iam::123456789012:user/test is not authorized');
|
|
110
|
+
awsError.name = 'AccessDeniedException';
|
|
111
|
+
mockSend.mockRejectedValueOnce(awsError);
|
|
112
|
+
|
|
113
|
+
// when: Getting public key with insufficient permissions
|
|
114
|
+
const getPublicKeyPromise = client.getPublicKey();
|
|
115
|
+
|
|
116
|
+
// then: Should throw KmsClientError with cause
|
|
117
|
+
await expect(getPublicKeyPromise).rejects.toThrow(KmsClientError);
|
|
118
|
+
await expect(getPublicKeyPromise).rejects.toThrow('Failed to get public key from KMS');
|
|
119
|
+
|
|
120
|
+
// then: Verify cause is preserved in error
|
|
121
|
+
mockSend.mockRejectedValueOnce(awsError);
|
|
122
|
+
try {
|
|
123
|
+
await client.getPublicKey();
|
|
124
|
+
} catch (error) {
|
|
125
|
+
expect(error).toBeInstanceOf(KmsClientError);
|
|
126
|
+
expect((error as KmsClientError).cause).toBe(awsError);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('should throw KmsClientError when getPublicKey receives KeyNotFoundException', async () => {
|
|
131
|
+
// given: KmsClient and mocked KeyNotFoundException
|
|
132
|
+
const client = new KmsClient(MOCK_KMS_CONFIG);
|
|
133
|
+
const awsError = new Error('Key arn:aws:kms:us-east-1:123456789012:key/nonexistent is not found');
|
|
134
|
+
awsError.name = 'KeyNotFoundException';
|
|
135
|
+
mockSend.mockRejectedValueOnce(awsError);
|
|
136
|
+
|
|
137
|
+
// when: Getting public key for non-existent key
|
|
138
|
+
const getPublicKeyPromise = client.getPublicKey();
|
|
139
|
+
|
|
140
|
+
// then: Should throw KmsClientError with cause
|
|
141
|
+
await expect(getPublicKeyPromise).rejects.toThrow(KmsClientError);
|
|
142
|
+
await expect(getPublicKeyPromise).rejects.toThrow('Failed to get public key from KMS');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('should throw KmsClientError when getPublicKey receives InvalidKeyUsageException', async () => {
|
|
146
|
+
// given: KmsClient and mocked InvalidKeyUsageException
|
|
147
|
+
const client = new KmsClient(MOCK_KMS_CONFIG);
|
|
148
|
+
const awsError = new Error('The request is not valid for the specified key usage');
|
|
149
|
+
awsError.name = 'InvalidKeyUsageException';
|
|
150
|
+
mockSend.mockRejectedValueOnce(awsError);
|
|
151
|
+
|
|
152
|
+
// when: Getting public key for key with wrong usage (e.g., ENCRYPT_DECRYPT)
|
|
153
|
+
const getPublicKeyPromise = client.getPublicKey();
|
|
154
|
+
|
|
155
|
+
// then: Should throw KmsClientError with cause
|
|
156
|
+
await expect(getPublicKeyPromise).rejects.toThrow(KmsClientError);
|
|
157
|
+
await expect(getPublicKeyPromise).rejects.toThrow('Failed to get public key from KMS');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('should throw KmsClientError when sign receives ThrottlingException', async () => {
|
|
161
|
+
// given: KmsClient and mocked ThrottlingException
|
|
162
|
+
const client = new KmsClient(MOCK_KMS_CONFIG);
|
|
163
|
+
const message = new Uint8Array([1, 2, 3]);
|
|
164
|
+
const awsError = new Error('Rate exceeded');
|
|
165
|
+
awsError.name = 'ThrottlingException';
|
|
166
|
+
mockSend.mockRejectedValueOnce(awsError);
|
|
167
|
+
|
|
168
|
+
// when: Signing message with rate limit exceeded
|
|
169
|
+
const signPromise = client.sign(message);
|
|
170
|
+
|
|
171
|
+
// then: Should throw KmsClientError with cause
|
|
172
|
+
await expect(signPromise).rejects.toThrow(KmsClientError);
|
|
173
|
+
await expect(signPromise).rejects.toThrow('Failed to sign message with KMS');
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('should throw KmsClientError when sign receives KeyUnavailableException', async () => {
|
|
177
|
+
// given: KmsClient and mocked KeyUnavailableException
|
|
178
|
+
const client = new KmsClient(MOCK_KMS_CONFIG);
|
|
179
|
+
const message = new Uint8Array([1, 2, 3]);
|
|
180
|
+
const awsError = new Error('The request was rejected because the specified KMS key is not available');
|
|
181
|
+
awsError.name = 'KeyUnavailableException';
|
|
182
|
+
mockSend.mockRejectedValueOnce(awsError);
|
|
183
|
+
|
|
184
|
+
// when: Signing message with unavailable key
|
|
185
|
+
const signPromise = client.sign(message);
|
|
186
|
+
|
|
187
|
+
// then: Should throw KmsClientError with cause
|
|
188
|
+
await expect(signPromise).rejects.toThrow(KmsClientError);
|
|
189
|
+
await expect(signPromise).rejects.toThrow('Failed to sign message with KMS');
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('should handle signing empty message appropriately', async () => {
|
|
193
|
+
// given: KmsClient and empty message
|
|
194
|
+
const client = new KmsClient(MOCK_KMS_CONFIG);
|
|
195
|
+
const emptyMessage = new Uint8Array(0);
|
|
196
|
+
|
|
197
|
+
mockSend.mockResolvedValueOnce({
|
|
198
|
+
Signature: MOCK_SIGNATURE,
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// when: Signing empty message
|
|
202
|
+
const signature = await client.sign(emptyMessage);
|
|
203
|
+
|
|
204
|
+
// then: Should successfully return signature (AWS KMS allows empty messages)
|
|
205
|
+
expect(signature).toBeInstanceOf(Uint8Array);
|
|
206
|
+
expect(signature.length).toBe(64);
|
|
207
|
+
expect(mockSend).toHaveBeenCalledTimes(1);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('should throw KmsClientError on network timeout simulation', async () => {
|
|
211
|
+
// given: KmsClient and mocked network timeout
|
|
212
|
+
const client = new KmsClient(MOCK_KMS_CONFIG);
|
|
213
|
+
const message = new Uint8Array([1, 2, 3]);
|
|
214
|
+
const timeoutError = new Error('Connection timeout after 10000ms');
|
|
215
|
+
timeoutError.name = 'TimeoutError';
|
|
216
|
+
mockSend.mockRejectedValueOnce(timeoutError);
|
|
217
|
+
|
|
218
|
+
// when: Signing message with network timeout
|
|
219
|
+
const signPromise = client.sign(message);
|
|
220
|
+
|
|
221
|
+
// then: Should throw KmsClientError with timeout cause
|
|
222
|
+
await expect(signPromise).rejects.toThrow(KmsClientError);
|
|
223
|
+
await expect(signPromise).rejects.toThrow('Failed to sign message with KMS');
|
|
224
|
+
|
|
225
|
+
// then: Verify cause is preserved in error
|
|
226
|
+
mockSend.mockRejectedValueOnce(timeoutError);
|
|
227
|
+
try {
|
|
228
|
+
await client.sign(message);
|
|
229
|
+
} catch (error) {
|
|
230
|
+
expect(error).toBeInstanceOf(KmsClientError);
|
|
231
|
+
expect((error as KmsClientError).cause).toBe(timeoutError);
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
describe('Edge Cases', () => {
|
|
237
|
+
it('should throw KmsClientError when getPublicKey response has null PublicKey', async () => {
|
|
238
|
+
// given: KmsClient and response with null PublicKey
|
|
239
|
+
const client = new KmsClient(MOCK_KMS_CONFIG);
|
|
240
|
+
mockSend.mockResolvedValueOnce({
|
|
241
|
+
PublicKey: null,
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// when: Getting public key with null response
|
|
245
|
+
const getPublicKeyPromise = client.getPublicKey();
|
|
246
|
+
|
|
247
|
+
// then: Should throw KmsClientError
|
|
248
|
+
await expect(getPublicKeyPromise).rejects.toThrow(KmsClientError);
|
|
249
|
+
await expect(getPublicKeyPromise).rejects.toThrow('GetPublicKey response missing PublicKey field');
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('should throw KmsClientError when sign response has null Signature', async () => {
|
|
253
|
+
// given: KmsClient and response with null Signature
|
|
254
|
+
const client = new KmsClient(MOCK_KMS_CONFIG);
|
|
255
|
+
const message = new Uint8Array([1, 2, 3]);
|
|
256
|
+
mockSend.mockResolvedValueOnce({
|
|
257
|
+
Signature: null,
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
// when: Signing message with null response
|
|
261
|
+
const signPromise = client.sign(message);
|
|
262
|
+
|
|
263
|
+
// then: Should throw KmsClientError
|
|
264
|
+
await expect(signPromise).rejects.toThrow(KmsClientError);
|
|
265
|
+
await expect(signPromise).rejects.toThrow('Sign response missing Signature field');
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('should throw KmsClientError when signature length is not 64 bytes', async () => {
|
|
269
|
+
// given: KmsClient and invalid signature length (e.g., 32 bytes)
|
|
270
|
+
const client = new KmsClient(MOCK_KMS_CONFIG);
|
|
271
|
+
const message = new Uint8Array([1, 2, 3]);
|
|
272
|
+
const invalidSignature = new Uint8Array(32); // Wrong length
|
|
273
|
+
mockSend.mockResolvedValueOnce({
|
|
274
|
+
Signature: invalidSignature,
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
// when: Receiving signature with wrong length
|
|
278
|
+
const signPromise = client.sign(message);
|
|
279
|
+
|
|
280
|
+
// then: Should throw KmsClientError with length validation message
|
|
281
|
+
await expect(signPromise).rejects.toThrow(KmsClientError);
|
|
282
|
+
await expect(signPromise).rejects.toThrow('Invalid signature length: expected 64 bytes, got 32 bytes');
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
});
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import {
|
|
2
|
+
KMSClient,
|
|
3
|
+
GetPublicKeyCommand,
|
|
4
|
+
SignCommand,
|
|
5
|
+
} from '@aws-sdk/client-kms';
|
|
6
|
+
import type { KmsConfig } from '../types/index.js';
|
|
7
|
+
import { KmsClientError } from '../errors/index.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* AWS KMS client wrapper for ED25519 key operations.
|
|
11
|
+
*
|
|
12
|
+
* Provides methods to retrieve public keys and sign messages using
|
|
13
|
+
* AWS KMS ED25519 keys (KeySpec: ECC_ED25519).
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```typescript
|
|
17
|
+
* const client = new KmsClient({
|
|
18
|
+
* region: 'us-east-1',
|
|
19
|
+
* keyId: 'arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012'
|
|
20
|
+
* });
|
|
21
|
+
*
|
|
22
|
+
* const publicKey = await client.getPublicKey();
|
|
23
|
+
* const signature = await client.sign(message);
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export class KmsClient {
|
|
27
|
+
private readonly kmsClient: KMSClient;
|
|
28
|
+
private readonly config: KmsConfig;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Creates a new KmsClient instance.
|
|
32
|
+
*
|
|
33
|
+
* @param config - Configuration for AWS KMS connection
|
|
34
|
+
*/
|
|
35
|
+
constructor(config: KmsConfig) {
|
|
36
|
+
this.config = config;
|
|
37
|
+
this.kmsClient = new KMSClient({
|
|
38
|
+
region: config.region,
|
|
39
|
+
credentials: config.credentials,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Retrieves the public key from AWS KMS.
|
|
45
|
+
*
|
|
46
|
+
* Returns DER-encoded SubjectPublicKeyInfo (X.509 format).
|
|
47
|
+
* Use `extractEd25519PublicKey` to extract the raw 32-byte public key.
|
|
48
|
+
*
|
|
49
|
+
* @returns DER-encoded public key as Uint8Array (typically 42-44 bytes)
|
|
50
|
+
* @throws {KmsClientError} If KMS API call fails
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```typescript
|
|
54
|
+
* const derPublicKey = await client.getPublicKey();
|
|
55
|
+
* const rawPublicKey = extractEd25519PublicKey(derPublicKey);
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
async getPublicKey(): Promise<Uint8Array> {
|
|
59
|
+
try {
|
|
60
|
+
const command = new GetPublicKeyCommand({
|
|
61
|
+
KeyId: this.config.keyId,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const response = await this.kmsClient.send(command);
|
|
65
|
+
|
|
66
|
+
if (!response.PublicKey) {
|
|
67
|
+
throw new KmsClientError('GetPublicKey response missing PublicKey field');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Convert Buffer/Uint8Array to Uint8Array
|
|
71
|
+
return new Uint8Array(response.PublicKey);
|
|
72
|
+
} catch (error) {
|
|
73
|
+
if (error instanceof KmsClientError) {
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
throw new KmsClientError(
|
|
77
|
+
`Failed to get public key from KMS: ${error instanceof Error ? error.message : String(error)}`,
|
|
78
|
+
error
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Signs a message using the ED25519 key in AWS KMS.
|
|
85
|
+
*
|
|
86
|
+
* @param message - Message to sign as Uint8Array
|
|
87
|
+
* @returns ED25519 signature (64 bytes)
|
|
88
|
+
* @throws {KmsClientError} If KMS API call fails or signature is invalid
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* ```typescript
|
|
92
|
+
* const message = new TextEncoder().encode('Hello, Solana!');
|
|
93
|
+
* const signature = await client.sign(message);
|
|
94
|
+
* console.log('Signature length:', signature.length); // 64
|
|
95
|
+
* ```
|
|
96
|
+
*/
|
|
97
|
+
async sign(message: Uint8Array): Promise<Uint8Array> {
|
|
98
|
+
try {
|
|
99
|
+
const command = new SignCommand({
|
|
100
|
+
KeyId: this.config.keyId,
|
|
101
|
+
Message: message,
|
|
102
|
+
MessageType: 'RAW',
|
|
103
|
+
SigningAlgorithm: 'ED25519_SHA_512',
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const response = await this.kmsClient.send(command);
|
|
107
|
+
|
|
108
|
+
if (!response.Signature) {
|
|
109
|
+
throw new KmsClientError('Sign response missing Signature field');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const signature = new Uint8Array(response.Signature);
|
|
113
|
+
|
|
114
|
+
// Validate signature length (ED25519 signatures are always 64 bytes)
|
|
115
|
+
if (signature.length !== 64) {
|
|
116
|
+
throw new KmsClientError(
|
|
117
|
+
`Invalid signature length: expected 64 bytes, got ${signature.length} bytes`
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return signature;
|
|
122
|
+
} catch (error) {
|
|
123
|
+
if (error instanceof KmsClientError) {
|
|
124
|
+
throw error;
|
|
125
|
+
}
|
|
126
|
+
throw new KmsClientError(
|
|
127
|
+
`Failed to sign message with KMS: ${error instanceof Error ? error.message : String(error)}`,
|
|
128
|
+
error
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|