solana-kms-signer 0.1.0 → 1.0.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 +82 -27
- package/dist/errors/index.d.ts.map +1 -1
- package/dist/errors/index.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -4
- package/dist/index.js.map +1 -1
- package/dist/kms/client.d.ts.map +1 -1
- package/dist/kms/client.js +1 -1
- package/dist/kms/client.js.map +1 -1
- package/dist/kms/signer.d.ts +2 -2
- package/dist/kms/signer.d.ts.map +1 -1
- package/dist/kms/signer.js +2 -2
- package/dist/kms/signer.js.map +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/publicKey.js.map +1 -1
- package/package.json +17 -16
- package/src/errors/index.test.ts +185 -170
- package/src/errors/index.ts +21 -12
- package/src/index.ts +10 -14
- package/src/kms/client.test.ts +318 -255
- package/src/kms/client.ts +100 -98
- package/src/kms/signer.test.ts +415 -396
- package/src/kms/signer.ts +205 -209
- package/src/types/index.ts +27 -27
- package/src/utils/publicKey.test.ts +178 -119
- package/src/utils/publicKey.ts +34 -34
package/src/kms/signer.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
PublicKey,
|
|
3
|
+
type Transaction,
|
|
4
|
+
type VersionedTransaction,
|
|
5
5
|
} from '@solana/web3.js';
|
|
6
6
|
import nacl from 'tweetnacl';
|
|
7
|
-
import { KmsClient } from './client.js';
|
|
8
|
-
import { extractEd25519PublicKey } from '../utils/publicKey.js';
|
|
9
|
-
import type { KmsConfig } from '../types/index.js';
|
|
10
7
|
import { SignatureVerificationError } from '../errors/index.js';
|
|
8
|
+
import type { KmsConfig } from '../types/index.js';
|
|
9
|
+
import { extractEd25519PublicKey } from '../utils/publicKey.js';
|
|
10
|
+
import { KmsClient } from './client.js';
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Solana transaction signer using AWS KMS ED25519 keys.
|
|
@@ -43,232 +43,228 @@ import { SignatureVerificationError } from '../errors/index.js';
|
|
|
43
43
|
* ```
|
|
44
44
|
*/
|
|
45
45
|
export class SolanaKmsSigner {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
46
|
+
private readonly kmsClient: KmsClient;
|
|
47
|
+
private publicKey?: PublicKey;
|
|
48
|
+
private rawPublicKey?: Uint8Array;
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
50
|
+
/**
|
|
51
|
+
* Creates a new SolanaKmsSigner instance.
|
|
52
|
+
*
|
|
53
|
+
* @param config - Either KmsConfig or an existing KmsClient instance
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```typescript
|
|
57
|
+
* // With KmsConfig
|
|
58
|
+
* const signer = new SolanaKmsSigner({
|
|
59
|
+
* region: 'us-east-1',
|
|
60
|
+
* keyId: 'key-id'
|
|
61
|
+
* });
|
|
62
|
+
*
|
|
63
|
+
* // With KmsClient
|
|
64
|
+
* const client = new KmsClient(config);
|
|
65
|
+
* const signer = new SolanaKmsSigner(client);
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
constructor(config: KmsConfig | KmsClient) {
|
|
69
|
+
if (config instanceof KmsClient) {
|
|
70
|
+
this.kmsClient = config;
|
|
71
|
+
} else {
|
|
72
|
+
this.kmsClient = new KmsClient(config);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
75
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
76
|
+
/**
|
|
77
|
+
* Retrieves the Solana PublicKey associated with the KMS key.
|
|
78
|
+
*
|
|
79
|
+
* The public key is cached after first retrieval to minimize KMS API calls.
|
|
80
|
+
*
|
|
81
|
+
* @returns Solana PublicKey object
|
|
82
|
+
* @throws {KmsClientError} If KMS API call fails
|
|
83
|
+
* @throws {PublicKeyExtractionError} If DER decoding fails
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* ```typescript
|
|
87
|
+
* const publicKey = await signer.getPublicKey();
|
|
88
|
+
* console.log('Address:', publicKey.toBase58());
|
|
89
|
+
* ```
|
|
90
|
+
*/
|
|
91
|
+
async getPublicKey(): Promise<PublicKey> {
|
|
92
|
+
// Return cached public key if available
|
|
93
|
+
if (this.publicKey) {
|
|
94
|
+
return this.publicKey;
|
|
95
|
+
}
|
|
96
96
|
|
|
97
|
-
|
|
98
|
-
|
|
97
|
+
// Get DER-encoded public key from KMS
|
|
98
|
+
const derPublicKey = await this.kmsClient.getPublicKey();
|
|
99
99
|
|
|
100
|
-
|
|
101
|
-
|
|
100
|
+
// Extract raw 32-byte ED25519 public key
|
|
101
|
+
const rawPublicKey = extractEd25519PublicKey(derPublicKey);
|
|
102
102
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
103
|
+
// Create Solana PublicKey and cache both forms
|
|
104
|
+
this.rawPublicKey = rawPublicKey;
|
|
105
|
+
this.publicKey = new PublicKey(rawPublicKey);
|
|
106
106
|
|
|
107
|
-
|
|
108
|
-
|
|
107
|
+
return this.publicKey;
|
|
108
|
+
}
|
|
109
109
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
110
|
+
/**
|
|
111
|
+
* Retrieves the raw 32-byte ED25519 public key.
|
|
112
|
+
*
|
|
113
|
+
* The public key is cached after first retrieval to minimize KMS API calls.
|
|
114
|
+
*
|
|
115
|
+
* @returns Raw 32-byte public key as Uint8Array
|
|
116
|
+
* @throws {KmsClientError} If KMS API call fails
|
|
117
|
+
* @throws {PublicKeyExtractionError} If DER decoding fails
|
|
118
|
+
*
|
|
119
|
+
* @example
|
|
120
|
+
* ```typescript
|
|
121
|
+
* const rawPublicKey = await signer.getRawPublicKey();
|
|
122
|
+
* console.log('Raw public key length:', rawPublicKey.length); // 32
|
|
123
|
+
* ```
|
|
124
|
+
*/
|
|
125
|
+
async getRawPublicKey(): Promise<Uint8Array> {
|
|
126
|
+
// Return cached raw public key if available
|
|
127
|
+
if (this.rawPublicKey) {
|
|
128
|
+
return this.rawPublicKey;
|
|
129
|
+
}
|
|
130
130
|
|
|
131
|
-
|
|
132
|
-
|
|
131
|
+
// Get DER-encoded public key from KMS
|
|
132
|
+
const derPublicKey = await this.kmsClient.getPublicKey();
|
|
133
133
|
|
|
134
|
-
|
|
135
|
-
|
|
134
|
+
// Extract raw 32-byte ED25519 public key
|
|
135
|
+
this.rawPublicKey = extractEd25519PublicKey(derPublicKey);
|
|
136
136
|
|
|
137
|
-
|
|
138
|
-
|
|
137
|
+
return this.rawPublicKey;
|
|
138
|
+
}
|
|
139
139
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
140
|
+
/**
|
|
141
|
+
* Signs an arbitrary message using the KMS key.
|
|
142
|
+
*
|
|
143
|
+
* The signature is verified using tweetnacl before being returned
|
|
144
|
+
* to ensure cryptographic correctness.
|
|
145
|
+
*
|
|
146
|
+
* @param message - Message to sign as Uint8Array
|
|
147
|
+
* @returns ED25519 signature (64 bytes)
|
|
148
|
+
* @throws {KmsClientError} If KMS API call fails
|
|
149
|
+
* @throws {SignatureVerificationError} If signature verification fails
|
|
150
|
+
*
|
|
151
|
+
* @example
|
|
152
|
+
* ```typescript
|
|
153
|
+
* const message = new TextEncoder().encode('Hello, Solana!');
|
|
154
|
+
* const signature = await signer.signMessage(message);
|
|
155
|
+
* console.log('Signature length:', signature.length); // 64
|
|
156
|
+
* ```
|
|
157
|
+
*/
|
|
158
|
+
async signMessage(message: Uint8Array): Promise<Uint8Array> {
|
|
159
|
+
// Get signature from KMS
|
|
160
|
+
const signature = await this.kmsClient.sign(message);
|
|
161
161
|
|
|
162
|
-
|
|
163
|
-
|
|
162
|
+
// Get raw public key for verification
|
|
163
|
+
const rawPublicKey = await this.getRawPublicKey();
|
|
164
164
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
message,
|
|
168
|
-
signature,
|
|
169
|
-
rawPublicKey
|
|
170
|
-
);
|
|
165
|
+
// Verify signature using tweetnacl
|
|
166
|
+
const isValid = nacl.sign.detached.verify(message, signature, rawPublicKey);
|
|
171
167
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
168
|
+
if (!isValid) {
|
|
169
|
+
throw new SignatureVerificationError(
|
|
170
|
+
'Signature verification failed: signature does not match public key and message',
|
|
171
|
+
);
|
|
172
|
+
}
|
|
177
173
|
|
|
178
|
-
|
|
179
|
-
|
|
174
|
+
return signature;
|
|
175
|
+
}
|
|
180
176
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
177
|
+
/**
|
|
178
|
+
* Signs a Solana legacy Transaction.
|
|
179
|
+
*
|
|
180
|
+
* The transaction must have recentBlockhash and feePayer set before signing.
|
|
181
|
+
*
|
|
182
|
+
* @param transaction - Transaction to sign
|
|
183
|
+
* @returns Signed transaction
|
|
184
|
+
* @throws {KmsClientError} If KMS API call fails
|
|
185
|
+
* @throws {SignatureVerificationError} If signature verification fails
|
|
186
|
+
*
|
|
187
|
+
* @example
|
|
188
|
+
* ```typescript
|
|
189
|
+
* const transaction = new Transaction().add(instruction);
|
|
190
|
+
* transaction.recentBlockhash = recentBlockhash;
|
|
191
|
+
* transaction.feePayer = await signer.getPublicKey();
|
|
192
|
+
* const signedTx = await signer.signTransaction(transaction);
|
|
193
|
+
* ```
|
|
194
|
+
*/
|
|
195
|
+
async signTransaction(transaction: Transaction): Promise<Transaction> {
|
|
196
|
+
// Get public key for signing
|
|
197
|
+
const publicKey = await this.getPublicKey();
|
|
202
198
|
|
|
203
|
-
|
|
204
|
-
|
|
199
|
+
// Serialize transaction message
|
|
200
|
+
const message = transaction.serializeMessage();
|
|
205
201
|
|
|
206
|
-
|
|
207
|
-
|
|
202
|
+
// Sign the serialized message
|
|
203
|
+
const signature = await this.signMessage(message);
|
|
208
204
|
|
|
209
|
-
|
|
210
|
-
|
|
205
|
+
// Add signature to transaction
|
|
206
|
+
transaction.addSignature(publicKey, Buffer.from(signature));
|
|
211
207
|
|
|
212
|
-
|
|
213
|
-
|
|
208
|
+
return transaction;
|
|
209
|
+
}
|
|
214
210
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
211
|
+
/**
|
|
212
|
+
* Signs a Solana VersionedTransaction.
|
|
213
|
+
*
|
|
214
|
+
* The transaction must have a valid message with recentBlockhash set.
|
|
215
|
+
*
|
|
216
|
+
* @param transaction - VersionedTransaction to sign
|
|
217
|
+
* @returns Signed versioned transaction
|
|
218
|
+
* @throws {KmsClientError} If KMS API call fails
|
|
219
|
+
* @throws {SignatureVerificationError} If signature verification fails
|
|
220
|
+
*
|
|
221
|
+
* @example
|
|
222
|
+
* ```typescript
|
|
223
|
+
* const message = MessageV0.compile({
|
|
224
|
+
* payerKey: await signer.getPublicKey(),
|
|
225
|
+
* instructions: [instruction],
|
|
226
|
+
* recentBlockhash: recentBlockhash
|
|
227
|
+
* });
|
|
228
|
+
* const transaction = new VersionedTransaction(message);
|
|
229
|
+
* const signedTx = await signer.signVersionedTransaction(transaction);
|
|
230
|
+
* ```
|
|
231
|
+
*/
|
|
232
|
+
async signVersionedTransaction(
|
|
233
|
+
transaction: VersionedTransaction,
|
|
234
|
+
): Promise<VersionedTransaction> {
|
|
235
|
+
// Serialize transaction message
|
|
236
|
+
const message = transaction.message.serialize();
|
|
241
237
|
|
|
242
|
-
|
|
243
|
-
|
|
238
|
+
// Sign the serialized message
|
|
239
|
+
const signature = await this.signMessage(message);
|
|
244
240
|
|
|
245
|
-
|
|
246
|
-
|
|
241
|
+
// Add signature to transaction's signatures array
|
|
242
|
+
transaction.addSignature(await this.getPublicKey(), Buffer.from(signature));
|
|
247
243
|
|
|
248
|
-
|
|
249
|
-
|
|
244
|
+
return transaction;
|
|
245
|
+
}
|
|
250
246
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
247
|
+
/**
|
|
248
|
+
* Signs multiple Solana transactions in parallel.
|
|
249
|
+
*
|
|
250
|
+
* All transactions must have recentBlockhash and feePayer set before signing.
|
|
251
|
+
*
|
|
252
|
+
* @param transactions - Array of transactions to sign
|
|
253
|
+
* @returns Array of signed transactions in the same order
|
|
254
|
+
* @throws {KmsClientError} If any KMS API call fails
|
|
255
|
+
* @throws {SignatureVerificationError} If any signature verification fails
|
|
256
|
+
*
|
|
257
|
+
* @example
|
|
258
|
+
* ```typescript
|
|
259
|
+
* const transactions = [tx1, tx2, tx3];
|
|
260
|
+
* const signedTxs = await signer.signAllTransactions(transactions);
|
|
261
|
+
* ```
|
|
262
|
+
*/
|
|
263
|
+
async signAllTransactions(
|
|
264
|
+
transactions: Transaction[],
|
|
265
|
+
): Promise<Transaction[]> {
|
|
266
|
+
return Promise.all(
|
|
267
|
+
transactions.map((transaction) => this.signTransaction(transaction)),
|
|
268
|
+
);
|
|
269
|
+
}
|
|
274
270
|
}
|
package/src/types/index.ts
CHANGED
|
@@ -2,36 +2,36 @@
|
|
|
2
2
|
* Configuration for AWS KMS client.
|
|
3
3
|
*/
|
|
4
4
|
export interface KmsConfig {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
/**
|
|
6
|
+
* AWS region where the KMS key is located (e.g., 'us-east-1').
|
|
7
|
+
*/
|
|
8
|
+
region: string;
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
/**
|
|
11
|
+
* KMS key ID or ARN to use for signing operations.
|
|
12
|
+
*/
|
|
13
|
+
keyId: string;
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Optional AWS credentials. If not provided, the AWS SDK will use
|
|
17
|
+
* environment variables, IAM roles, or other credential providers.
|
|
18
|
+
*/
|
|
19
|
+
credentials?: {
|
|
20
|
+
/**
|
|
21
|
+
* AWS access key ID.
|
|
22
|
+
*/
|
|
23
|
+
accessKeyId: string;
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
/**
|
|
26
|
+
* AWS secret access key.
|
|
27
|
+
*/
|
|
28
|
+
secretAccessKey: string;
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Optional session token for temporary credentials.
|
|
32
|
+
*/
|
|
33
|
+
sessionToken?: string;
|
|
34
|
+
};
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
/**
|
|
@@ -40,5 +40,5 @@ export interface KmsConfig {
|
|
|
40
40
|
* Additional configuration options can be added in the future.
|
|
41
41
|
*/
|
|
42
42
|
export interface SolanaKmsSignerConfig extends KmsConfig {
|
|
43
|
-
|
|
43
|
+
// Additional configuration options can be added here in the future
|
|
44
44
|
}
|