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,230 @@
|
|
|
1
|
+
import { PublicKey, } from '@solana/web3.js';
|
|
2
|
+
import nacl from 'tweetnacl';
|
|
3
|
+
import { KmsClient } from './client.js';
|
|
4
|
+
import { extractEd25519PublicKey } from '../utils/publicKey.js';
|
|
5
|
+
import { SignatureVerificationError } from '../errors/index.js';
|
|
6
|
+
/**
|
|
7
|
+
* Solana transaction signer using AWS KMS ED25519 keys.
|
|
8
|
+
*
|
|
9
|
+
* Provides methods to sign Solana transactions and arbitrary messages
|
|
10
|
+
* using AWS KMS-managed ED25519 keys. Caches the public key after
|
|
11
|
+
* first retrieval to minimize KMS API calls.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```typescript
|
|
15
|
+
* // Create with KmsConfig
|
|
16
|
+
* const signer = new SolanaKmsSigner({
|
|
17
|
+
* region: 'us-east-1',
|
|
18
|
+
* keyId: 'arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012'
|
|
19
|
+
* });
|
|
20
|
+
*
|
|
21
|
+
* // Or create with existing KmsClient
|
|
22
|
+
* const client = new KmsClient(config);
|
|
23
|
+
* const signer = new SolanaKmsSigner(client);
|
|
24
|
+
*
|
|
25
|
+
* // Get public key
|
|
26
|
+
* const publicKey = await signer.getPublicKey();
|
|
27
|
+
*
|
|
28
|
+
* // Sign message
|
|
29
|
+
* const message = new TextEncoder().encode('Hello, Solana!');
|
|
30
|
+
* const signature = await signer.signMessage(message);
|
|
31
|
+
*
|
|
32
|
+
* // Sign transaction
|
|
33
|
+
* const transaction = new Transaction().add(instruction);
|
|
34
|
+
* transaction.recentBlockhash = recentBlockhash;
|
|
35
|
+
* transaction.feePayer = publicKey;
|
|
36
|
+
* const signedTx = await signer.signTransaction(transaction);
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export class SolanaKmsSigner {
|
|
40
|
+
/**
|
|
41
|
+
* Creates a new SolanaKmsSigner instance.
|
|
42
|
+
*
|
|
43
|
+
* @param config - Either KmsConfig or an existing KmsClient instance
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```typescript
|
|
47
|
+
* // With KmsConfig
|
|
48
|
+
* const signer = new SolanaKmsSigner({
|
|
49
|
+
* region: 'us-east-1',
|
|
50
|
+
* keyId: 'key-id'
|
|
51
|
+
* });
|
|
52
|
+
*
|
|
53
|
+
* // With KmsClient
|
|
54
|
+
* const client = new KmsClient(config);
|
|
55
|
+
* const signer = new SolanaKmsSigner(client);
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
constructor(config) {
|
|
59
|
+
if (config instanceof KmsClient) {
|
|
60
|
+
this.kmsClient = config;
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
this.kmsClient = new KmsClient(config);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Retrieves the Solana PublicKey associated with the KMS key.
|
|
68
|
+
*
|
|
69
|
+
* The public key is cached after first retrieval to minimize KMS API calls.
|
|
70
|
+
*
|
|
71
|
+
* @returns Solana PublicKey object
|
|
72
|
+
* @throws {KmsClientError} If KMS API call fails
|
|
73
|
+
* @throws {PublicKeyExtractionError} If DER decoding fails
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* ```typescript
|
|
77
|
+
* const publicKey = await signer.getPublicKey();
|
|
78
|
+
* console.log('Address:', publicKey.toBase58());
|
|
79
|
+
* ```
|
|
80
|
+
*/
|
|
81
|
+
async getPublicKey() {
|
|
82
|
+
// Return cached public key if available
|
|
83
|
+
if (this.publicKey) {
|
|
84
|
+
return this.publicKey;
|
|
85
|
+
}
|
|
86
|
+
// Get DER-encoded public key from KMS
|
|
87
|
+
const derPublicKey = await this.kmsClient.getPublicKey();
|
|
88
|
+
// Extract raw 32-byte ED25519 public key
|
|
89
|
+
const rawPublicKey = extractEd25519PublicKey(derPublicKey);
|
|
90
|
+
// Create Solana PublicKey and cache both forms
|
|
91
|
+
this.rawPublicKey = rawPublicKey;
|
|
92
|
+
this.publicKey = new PublicKey(rawPublicKey);
|
|
93
|
+
return this.publicKey;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Retrieves the raw 32-byte ED25519 public key.
|
|
97
|
+
*
|
|
98
|
+
* The public key is cached after first retrieval to minimize KMS API calls.
|
|
99
|
+
*
|
|
100
|
+
* @returns Raw 32-byte public key as Uint8Array
|
|
101
|
+
* @throws {KmsClientError} If KMS API call fails
|
|
102
|
+
* @throws {PublicKeyExtractionError} If DER decoding fails
|
|
103
|
+
*
|
|
104
|
+
* @example
|
|
105
|
+
* ```typescript
|
|
106
|
+
* const rawPublicKey = await signer.getRawPublicKey();
|
|
107
|
+
* console.log('Raw public key length:', rawPublicKey.length); // 32
|
|
108
|
+
* ```
|
|
109
|
+
*/
|
|
110
|
+
async getRawPublicKey() {
|
|
111
|
+
// Return cached raw public key if available
|
|
112
|
+
if (this.rawPublicKey) {
|
|
113
|
+
return this.rawPublicKey;
|
|
114
|
+
}
|
|
115
|
+
// Get DER-encoded public key from KMS
|
|
116
|
+
const derPublicKey = await this.kmsClient.getPublicKey();
|
|
117
|
+
// Extract raw 32-byte ED25519 public key
|
|
118
|
+
this.rawPublicKey = extractEd25519PublicKey(derPublicKey);
|
|
119
|
+
return this.rawPublicKey;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Signs an arbitrary message using the KMS key.
|
|
123
|
+
*
|
|
124
|
+
* The signature is verified using tweetnacl before being returned
|
|
125
|
+
* to ensure cryptographic correctness.
|
|
126
|
+
*
|
|
127
|
+
* @param message - Message to sign as Uint8Array
|
|
128
|
+
* @returns ED25519 signature (64 bytes)
|
|
129
|
+
* @throws {KmsClientError} If KMS API call fails
|
|
130
|
+
* @throws {SignatureVerificationError} If signature verification fails
|
|
131
|
+
*
|
|
132
|
+
* @example
|
|
133
|
+
* ```typescript
|
|
134
|
+
* const message = new TextEncoder().encode('Hello, Solana!');
|
|
135
|
+
* const signature = await signer.signMessage(message);
|
|
136
|
+
* console.log('Signature length:', signature.length); // 64
|
|
137
|
+
* ```
|
|
138
|
+
*/
|
|
139
|
+
async signMessage(message) {
|
|
140
|
+
// Get signature from KMS
|
|
141
|
+
const signature = await this.kmsClient.sign(message);
|
|
142
|
+
// Get raw public key for verification
|
|
143
|
+
const rawPublicKey = await this.getRawPublicKey();
|
|
144
|
+
// Verify signature using tweetnacl
|
|
145
|
+
const isValid = nacl.sign.detached.verify(message, signature, rawPublicKey);
|
|
146
|
+
if (!isValid) {
|
|
147
|
+
throw new SignatureVerificationError('Signature verification failed: signature does not match public key and message');
|
|
148
|
+
}
|
|
149
|
+
return signature;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Signs a Solana legacy Transaction.
|
|
153
|
+
*
|
|
154
|
+
* The transaction must have recentBlockhash and feePayer set before signing.
|
|
155
|
+
*
|
|
156
|
+
* @param transaction - Transaction to sign
|
|
157
|
+
* @returns Signed transaction
|
|
158
|
+
* @throws {KmsClientError} If KMS API call fails
|
|
159
|
+
* @throws {SignatureVerificationError} If signature verification fails
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
* ```typescript
|
|
163
|
+
* const transaction = new Transaction().add(instruction);
|
|
164
|
+
* transaction.recentBlockhash = recentBlockhash;
|
|
165
|
+
* transaction.feePayer = await signer.getPublicKey();
|
|
166
|
+
* const signedTx = await signer.signTransaction(transaction);
|
|
167
|
+
* ```
|
|
168
|
+
*/
|
|
169
|
+
async signTransaction(transaction) {
|
|
170
|
+
// Get public key for signing
|
|
171
|
+
const publicKey = await this.getPublicKey();
|
|
172
|
+
// Serialize transaction message
|
|
173
|
+
const message = transaction.serializeMessage();
|
|
174
|
+
// Sign the serialized message
|
|
175
|
+
const signature = await this.signMessage(message);
|
|
176
|
+
// Add signature to transaction
|
|
177
|
+
transaction.addSignature(publicKey, Buffer.from(signature));
|
|
178
|
+
return transaction;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Signs a Solana VersionedTransaction.
|
|
182
|
+
*
|
|
183
|
+
* The transaction must have a valid message with recentBlockhash set.
|
|
184
|
+
*
|
|
185
|
+
* @param transaction - VersionedTransaction to sign
|
|
186
|
+
* @returns Signed versioned transaction
|
|
187
|
+
* @throws {KmsClientError} If KMS API call fails
|
|
188
|
+
* @throws {SignatureVerificationError} If signature verification fails
|
|
189
|
+
*
|
|
190
|
+
* @example
|
|
191
|
+
* ```typescript
|
|
192
|
+
* const message = MessageV0.compile({
|
|
193
|
+
* payerKey: await signer.getPublicKey(),
|
|
194
|
+
* instructions: [instruction],
|
|
195
|
+
* recentBlockhash: recentBlockhash
|
|
196
|
+
* });
|
|
197
|
+
* const transaction = new VersionedTransaction(message);
|
|
198
|
+
* const signedTx = await signer.signVersionedTransaction(transaction);
|
|
199
|
+
* ```
|
|
200
|
+
*/
|
|
201
|
+
async signVersionedTransaction(transaction) {
|
|
202
|
+
// Serialize transaction message
|
|
203
|
+
const message = transaction.message.serialize();
|
|
204
|
+
// Sign the serialized message
|
|
205
|
+
const signature = await this.signMessage(message);
|
|
206
|
+
// Add signature to transaction's signatures array
|
|
207
|
+
transaction.addSignature(await this.getPublicKey(), Buffer.from(signature));
|
|
208
|
+
return transaction;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Signs multiple Solana transactions in parallel.
|
|
212
|
+
*
|
|
213
|
+
* All transactions must have recentBlockhash and feePayer set before signing.
|
|
214
|
+
*
|
|
215
|
+
* @param transactions - Array of transactions to sign
|
|
216
|
+
* @returns Array of signed transactions in the same order
|
|
217
|
+
* @throws {KmsClientError} If any KMS API call fails
|
|
218
|
+
* @throws {SignatureVerificationError} If any signature verification fails
|
|
219
|
+
*
|
|
220
|
+
* @example
|
|
221
|
+
* ```typescript
|
|
222
|
+
* const transactions = [tx1, tx2, tx3];
|
|
223
|
+
* const signedTxs = await signer.signAllTransactions(transactions);
|
|
224
|
+
* ```
|
|
225
|
+
*/
|
|
226
|
+
async signAllTransactions(transactions) {
|
|
227
|
+
return Promise.all(transactions.map((transaction) => this.signTransaction(transaction)));
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
//# sourceMappingURL=signer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"signer.js","sourceRoot":"","sources":["../../src/kms/signer.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,SAAS,GAGV,MAAM,iBAAiB,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAC;AAEhE,OAAO,EAAE,0BAA0B,EAAE,MAAM,oBAAoB,CAAC;AAEhE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,MAAM,OAAO,eAAe;IAK1B;;;;;;;;;;;;;;;;;OAiBG;IACH,YAAY,MAA6B;QACvC,IAAI,MAAM,YAAY,SAAS,EAAE,CAAC;YAChC,IAAI,CAAC,SAAS,GAAG,MAAM,CAAC;QAC1B,CAAC;aAAM,CAAC;YACN,IAAI,CAAC,SAAS,GAAG,IAAI,SAAS,CAAC,MAAM,CAAC,CAAC;QACzC,CAAC;IACH,CAAC;IAED;;;;;;;;;;;;;;OAcG;IACH,KAAK,CAAC,YAAY;QAChB,wCAAwC;QACxC,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;YACnB,OAAO,IAAI,CAAC,SAAS,CAAC;QACxB,CAAC;QAED,sCAAsC;QACtC,MAAM,YAAY,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,YAAY,EAAE,CAAC;QAEzD,yCAAyC;QACzC,MAAM,YAAY,GAAG,uBAAuB,CAAC,YAAY,CAAC,CAAC;QAE3D,+CAA+C;QAC/C,IAAI,CAAC,YAAY,GAAG,YAAY,CAAC;QACjC,IAAI,CAAC,SAAS,GAAG,IAAI,SAAS,CAAC,YAAY,CAAC,CAAC;QAE7C,OAAO,IAAI,CAAC,SAAS,CAAC;IACxB,CAAC;IAED;;;;;;;;;;;;;;OAcG;IACH,KAAK,CAAC,eAAe;QACnB,4CAA4C;QAC5C,IAAI,IAAI,CAAC,YAAY,EAAE,CAAC;YACtB,OAAO,IAAI,CAAC,YAAY,CAAC;QAC3B,CAAC;QAED,sCAAsC;QACtC,MAAM,YAAY,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,YAAY,EAAE,CAAC;QAEzD,yCAAyC;QACzC,IAAI,CAAC,YAAY,GAAG,uBAAuB,CAAC,YAAY,CAAC,CAAC;QAE1D,OAAO,IAAI,CAAC,YAAY,CAAC;IAC3B,CAAC;IAED;;;;;;;;;;;;;;;;;OAiBG;IACH,KAAK,CAAC,WAAW,CAAC,OAAmB;QACnC,yBAAyB;QACzB,MAAM,SAAS,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAErD,sCAAsC;QACtC,MAAM,YAAY,GAAG,MAAM,IAAI,CAAC,eAAe,EAAE,CAAC;QAElD,mCAAmC;QACnC,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CACvC,OAAO,EACP,SAAS,EACT,YAAY,CACb,CAAC;QAEF,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,MAAM,IAAI,0BAA0B,CAClC,gFAAgF,CACjF,CAAC;QACJ,CAAC;QAED,OAAO,SAAS,CAAC;IACnB,CAAC;IAED;;;;;;;;;;;;;;;;;OAiBG;IACH,KAAK,CAAC,eAAe,CAAC,WAAwB;QAC5C,6BAA6B;QAC7B,MAAM,SAAS,GAAG,MAAM,IAAI,CAAC,YAAY,EAAE,CAAC;QAE5C,gCAAgC;QAChC,MAAM,OAAO,GAAG,WAAW,CAAC,gBAAgB,EAAE,CAAC;QAE/C,8BAA8B;QAC9B,MAAM,SAAS,GAAG,MAAM,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;QAElD,+BAA+B;QAC/B,WAAW,CAAC,YAAY,CAAC,SAAS,EAAE,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC;QAE5D,OAAO,WAAW,CAAC;IACrB,CAAC;IAED;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,KAAK,CAAC,wBAAwB,CAC5B,WAAiC;QAEjC,gCAAgC;QAChC,MAAM,OAAO,GAAG,WAAW,CAAC,OAAO,CAAC,SAAS,EAAE,CAAC;QAEhD,8BAA8B;QAC9B,MAAM,SAAS,GAAG,MAAM,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;QAElD,kDAAkD;QAClD,WAAW,CAAC,YAAY,CAAC,MAAM,IAAI,CAAC,YAAY,EAAE,EAAE,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC;QAE5E,OAAO,WAAW,CAAC;IACrB,CAAC;IAED;;;;;;;;;;;;;;;OAeG;IACH,KAAK,CAAC,mBAAmB,CACvB,YAA2B;QAE3B,OAAO,OAAO,CAAC,GAAG,CAChB,YAAY,CAAC,GAAG,CAAC,CAAC,WAAW,EAAE,EAAE,CAAC,IAAI,CAAC,eAAe,CAAC,WAAW,CAAC,CAAC,CACrE,CAAC;IACJ,CAAC;CACF"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration for AWS KMS client.
|
|
3
|
+
*/
|
|
4
|
+
export interface KmsConfig {
|
|
5
|
+
/**
|
|
6
|
+
* AWS region where the KMS key is located (e.g., 'us-east-1').
|
|
7
|
+
*/
|
|
8
|
+
region: string;
|
|
9
|
+
/**
|
|
10
|
+
* KMS key ID or ARN to use for signing operations.
|
|
11
|
+
*/
|
|
12
|
+
keyId: string;
|
|
13
|
+
/**
|
|
14
|
+
* Optional AWS credentials. If not provided, the AWS SDK will use
|
|
15
|
+
* environment variables, IAM roles, or other credential providers.
|
|
16
|
+
*/
|
|
17
|
+
credentials?: {
|
|
18
|
+
/**
|
|
19
|
+
* AWS access key ID.
|
|
20
|
+
*/
|
|
21
|
+
accessKeyId: string;
|
|
22
|
+
/**
|
|
23
|
+
* AWS secret access key.
|
|
24
|
+
*/
|
|
25
|
+
secretAccessKey: string;
|
|
26
|
+
/**
|
|
27
|
+
* Optional session token for temporary credentials.
|
|
28
|
+
*/
|
|
29
|
+
sessionToken?: string;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Configuration for Solana KMS Signer.
|
|
34
|
+
* Currently extends KmsConfig with no additional fields.
|
|
35
|
+
* Additional configuration options can be added in the future.
|
|
36
|
+
*/
|
|
37
|
+
export interface SolanaKmsSignerConfig extends KmsConfig {
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,WAAW,SAAS;IACxB;;OAEG;IACH,MAAM,EAAE,MAAM,CAAC;IAEf;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;IAEd;;;OAGG;IACH,WAAW,CAAC,EAAE;QACZ;;WAEG;QACH,WAAW,EAAE,MAAM,CAAC;QAEpB;;WAEG;QACH,eAAe,EAAE,MAAM,CAAC;QAExB;;WAEG;QACH,YAAY,CAAC,EAAE,MAAM,CAAC;KACvB,CAAC;CACH;AAED;;;;GAIG;AACH,MAAM,WAAW,qBAAsB,SAAQ,SAAS;CAEvD"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extracts a 32-byte ED25519 public key from DER-encoded SubjectPublicKeyInfo.
|
|
3
|
+
*
|
|
4
|
+
* AWS KMS GetPublicKey returns DER-encoded public keys in X.509 SubjectPublicKeyInfo format:
|
|
5
|
+
* ```
|
|
6
|
+
* 30 2a # SEQUENCE, 42 bytes
|
|
7
|
+
* 30 05 # SEQUENCE, 5 bytes (AlgorithmIdentifier)
|
|
8
|
+
* 06 03 # OID, 3 bytes
|
|
9
|
+
* 2b 65 70 # 1.3.101.112 (Ed25519)
|
|
10
|
+
* 03 21 00 # BIT STRING, 33 bytes (32 bytes + 1 byte padding)
|
|
11
|
+
* [32 bytes] # ED25519 public key
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* This function extracts the raw 32-byte public key from the DER structure.
|
|
15
|
+
*
|
|
16
|
+
* @param derEncoded - DER-encoded SubjectPublicKeyInfo from AWS KMS
|
|
17
|
+
* @returns Raw 32-byte ED25519 public key
|
|
18
|
+
* @throws {PublicKeyExtractionError} If the DER encoding is invalid or unexpected
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```typescript
|
|
22
|
+
* const derEncoded = await kmsClient.getPublicKey();
|
|
23
|
+
* const rawPublicKey = extractEd25519PublicKey(derEncoded);
|
|
24
|
+
* // rawPublicKey is Uint8Array of 32 bytes
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export declare function extractEd25519PublicKey(derEncoded: Uint8Array): Uint8Array;
|
|
28
|
+
//# sourceMappingURL=publicKey.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"publicKey.d.ts","sourceRoot":"","sources":["../../src/utils/publicKey.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,uBAAuB,CAAC,UAAU,EAAE,UAAU,GAAG,UAAU,CAyC1E"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { PublicKeyExtractionError } from '../errors/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Extracts a 32-byte ED25519 public key from DER-encoded SubjectPublicKeyInfo.
|
|
4
|
+
*
|
|
5
|
+
* AWS KMS GetPublicKey returns DER-encoded public keys in X.509 SubjectPublicKeyInfo format:
|
|
6
|
+
* ```
|
|
7
|
+
* 30 2a # SEQUENCE, 42 bytes
|
|
8
|
+
* 30 05 # SEQUENCE, 5 bytes (AlgorithmIdentifier)
|
|
9
|
+
* 06 03 # OID, 3 bytes
|
|
10
|
+
* 2b 65 70 # 1.3.101.112 (Ed25519)
|
|
11
|
+
* 03 21 00 # BIT STRING, 33 bytes (32 bytes + 1 byte padding)
|
|
12
|
+
* [32 bytes] # ED25519 public key
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* This function extracts the raw 32-byte public key from the DER structure.
|
|
16
|
+
*
|
|
17
|
+
* @param derEncoded - DER-encoded SubjectPublicKeyInfo from AWS KMS
|
|
18
|
+
* @returns Raw 32-byte ED25519 public key
|
|
19
|
+
* @throws {PublicKeyExtractionError} If the DER encoding is invalid or unexpected
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```typescript
|
|
23
|
+
* const derEncoded = await kmsClient.getPublicKey();
|
|
24
|
+
* const rawPublicKey = extractEd25519PublicKey(derEncoded);
|
|
25
|
+
* // rawPublicKey is Uint8Array of 32 bytes
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export function extractEd25519PublicKey(derEncoded) {
|
|
29
|
+
// Validate DER structure - first byte must be SEQUENCE tag (0x30)
|
|
30
|
+
if (derEncoded[0] !== 0x30) {
|
|
31
|
+
throw new PublicKeyExtractionError('Invalid DER encoding: missing SEQUENCE tag');
|
|
32
|
+
}
|
|
33
|
+
// Parse AlgorithmIdentifier SEQUENCE to find where BIT STRING starts
|
|
34
|
+
// Position 2 should be another SEQUENCE tag for AlgorithmIdentifier
|
|
35
|
+
if (derEncoded[2] !== 0x30) {
|
|
36
|
+
throw new PublicKeyExtractionError('Invalid DER encoding: missing AlgorithmIdentifier SEQUENCE');
|
|
37
|
+
}
|
|
38
|
+
// Get length of AlgorithmIdentifier content
|
|
39
|
+
const algorithmIdentifierLength = derEncoded[3];
|
|
40
|
+
// BIT STRING starts after AlgorithmIdentifier SEQUENCE
|
|
41
|
+
// Position = 4 (first content byte) + algorithmIdentifierLength
|
|
42
|
+
const bitStringIndex = 4 + algorithmIdentifierLength;
|
|
43
|
+
// Verify BIT STRING tag (0x03)
|
|
44
|
+
if (derEncoded[bitStringIndex] !== 0x03) {
|
|
45
|
+
throw new PublicKeyExtractionError('Invalid DER encoding: missing BIT STRING');
|
|
46
|
+
}
|
|
47
|
+
// Verify BIT STRING length (0x21 = 33 bytes: 1 byte unused bits + 32 bytes public key)
|
|
48
|
+
const bitStringLength = derEncoded[bitStringIndex + 1];
|
|
49
|
+
if (bitStringLength !== 0x21) {
|
|
50
|
+
throw new PublicKeyExtractionError(`Unexpected BIT STRING length: expected 0x21 (33 bytes), got 0x${bitStringLength.toString(16)}`);
|
|
51
|
+
}
|
|
52
|
+
// First byte after length is unused bits (0x00), next 32 bytes is the public key
|
|
53
|
+
const publicKeyStart = bitStringIndex + 3;
|
|
54
|
+
return derEncoded.slice(publicKeyStart, publicKeyStart + 32);
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=publicKey.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"publicKey.js","sourceRoot":"","sources":["../../src/utils/publicKey.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,wBAAwB,EAAE,MAAM,oBAAoB,CAAC;AAE9D;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,UAAU,uBAAuB,CAAC,UAAsB;IAC5D,kEAAkE;IAClE,IAAI,UAAU,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;QAC3B,MAAM,IAAI,wBAAwB,CAChC,4CAA4C,CAC7C,CAAC;IACJ,CAAC;IAED,qEAAqE;IACrE,oEAAoE;IACpE,IAAI,UAAU,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;QAC3B,MAAM,IAAI,wBAAwB,CAChC,4DAA4D,CAC7D,CAAC;IACJ,CAAC;IAED,4CAA4C;IAC5C,MAAM,yBAAyB,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC;IAEhD,uDAAuD;IACvD,gEAAgE;IAChE,MAAM,cAAc,GAAG,CAAC,GAAG,yBAAyB,CAAC;IAErD,+BAA+B;IAC/B,IAAI,UAAU,CAAC,cAAc,CAAC,KAAK,IAAI,EAAE,CAAC;QACxC,MAAM,IAAI,wBAAwB,CAChC,0CAA0C,CAC3C,CAAC;IACJ,CAAC;IAED,uFAAuF;IACvF,MAAM,eAAe,GAAG,UAAU,CAAC,cAAc,GAAG,CAAC,CAAC,CAAC;IACvD,IAAI,eAAe,KAAK,IAAI,EAAE,CAAC;QAC7B,MAAM,IAAI,wBAAwB,CAChC,iEAAiE,eAAe,CAAC,QAAQ,CAAC,EAAE,CAAC,EAAE,CAChG,CAAC;IACJ,CAAC;IAED,iFAAiF;IACjF,MAAM,cAAc,GAAG,cAAc,GAAG,CAAC,CAAC;IAC1C,OAAO,UAAU,CAAC,KAAK,CAAC,cAAc,EAAE,cAAc,GAAG,EAAE,CAAC,CAAC;AAC/D,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "solana-kms-signer",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "AWS KMS-based Solana signer with ED25519 support",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsc",
|
|
16
|
+
"test": "vitest",
|
|
17
|
+
"test:run": "vitest run",
|
|
18
|
+
"test:ui": "vitest --ui",
|
|
19
|
+
"test:coverage": "vitest run --coverage",
|
|
20
|
+
"type-check": "tsc --noEmit",
|
|
21
|
+
"example:sign-message": "tsx examples/sign-message.ts",
|
|
22
|
+
"example:sign-transaction": "tsx examples/sign-transaction.ts",
|
|
23
|
+
"example:sign-versioned-transaction": "tsx examples/sign-versioned-transaction.ts",
|
|
24
|
+
"example:multiple-signatures": "tsx examples/multiple-signatures.ts",
|
|
25
|
+
"example:create-kms-key": "tsx examples/create-kms-key.ts"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"solana",
|
|
29
|
+
"kms",
|
|
30
|
+
"aws",
|
|
31
|
+
"signature",
|
|
32
|
+
"ed25519",
|
|
33
|
+
"transaction",
|
|
34
|
+
"signing",
|
|
35
|
+
"key-management",
|
|
36
|
+
"cryptography",
|
|
37
|
+
"blockchain"
|
|
38
|
+
],
|
|
39
|
+
"author": "Taegeon Alan Go",
|
|
40
|
+
"license": "MIT",
|
|
41
|
+
"repository": {
|
|
42
|
+
"type": "git",
|
|
43
|
+
"url": "git+https://github.com/gtg7784/solana-kms-signer.git"
|
|
44
|
+
},
|
|
45
|
+
"bugs": {
|
|
46
|
+
"url": "https://github.com/gtg7784/solana-kms-signer/issues"
|
|
47
|
+
},
|
|
48
|
+
"homepage": "https://github.com/gtg7784/solana-kms-signer#readme",
|
|
49
|
+
"files": [
|
|
50
|
+
"dist",
|
|
51
|
+
"src",
|
|
52
|
+
"LICENSE",
|
|
53
|
+
"README.md"
|
|
54
|
+
],
|
|
55
|
+
"engines": {
|
|
56
|
+
"node": ">=16.0.0"
|
|
57
|
+
},
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"@types/node": "^24.10.0",
|
|
60
|
+
"@vitest/coverage-v8": "^4.0.8",
|
|
61
|
+
"@vitest/ui": "^4.0.8",
|
|
62
|
+
"dotenv": "^17.2.3",
|
|
63
|
+
"tsx": "^4.20.6",
|
|
64
|
+
"typescript": "^5.9.3",
|
|
65
|
+
"vitest": "^4.0.8"
|
|
66
|
+
},
|
|
67
|
+
"packageManager": "pnpm@9.0.1+sha1.0e0a9c2d140ddf9aab730067eb7bcfb9e18bdee7",
|
|
68
|
+
"dependencies": {
|
|
69
|
+
"@aws-sdk/client-kms": "^3.928.0",
|
|
70
|
+
"@solana/web3.js": "^1.98.4",
|
|
71
|
+
"tweetnacl": "^1.0.3"
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { KmsClientError, PublicKeyExtractionError, SignatureVerificationError } from './index.js';
|
|
3
|
+
|
|
4
|
+
describe('Error Classes', () => {
|
|
5
|
+
describe('KmsClientError', () => {
|
|
6
|
+
it('should create error with message', () => {
|
|
7
|
+
// given
|
|
8
|
+
const message = 'Test KMS error';
|
|
9
|
+
|
|
10
|
+
// when
|
|
11
|
+
const error = new KmsClientError(message);
|
|
12
|
+
|
|
13
|
+
// then
|
|
14
|
+
expect(error).toBeInstanceOf(Error);
|
|
15
|
+
expect(error).toBeInstanceOf(KmsClientError);
|
|
16
|
+
expect(error.message).toBe(message);
|
|
17
|
+
expect(error.name).toBe('KmsClientError');
|
|
18
|
+
expect(error.cause).toBeUndefined();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('should create error with message and cause', () => {
|
|
22
|
+
// given
|
|
23
|
+
const message = 'Test KMS error';
|
|
24
|
+
const cause = new Error('Original error');
|
|
25
|
+
|
|
26
|
+
// when
|
|
27
|
+
const error = new KmsClientError(message, cause);
|
|
28
|
+
|
|
29
|
+
// then
|
|
30
|
+
expect(error.message).toBe(message);
|
|
31
|
+
expect(error.cause).toBe(cause);
|
|
32
|
+
expect(error.name).toBe('KmsClientError');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should create error with cause as object', () => {
|
|
36
|
+
// given
|
|
37
|
+
const message = 'Test KMS error';
|
|
38
|
+
const cause = { code: 'AccessDeniedException', statusCode: 403 };
|
|
39
|
+
|
|
40
|
+
// when
|
|
41
|
+
const error = new KmsClientError(message, cause);
|
|
42
|
+
|
|
43
|
+
// then
|
|
44
|
+
expect(error.cause).toBe(cause);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should have stack trace', () => {
|
|
48
|
+
// given
|
|
49
|
+
const message = 'Test KMS error';
|
|
50
|
+
|
|
51
|
+
// when
|
|
52
|
+
const error = new KmsClientError(message);
|
|
53
|
+
|
|
54
|
+
// then
|
|
55
|
+
expect(error.stack).toBeDefined();
|
|
56
|
+
expect(error.stack).toContain('KmsClientError');
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('PublicKeyExtractionError', () => {
|
|
61
|
+
it('should create error with message', () => {
|
|
62
|
+
// given
|
|
63
|
+
const message = 'Invalid DER encoding';
|
|
64
|
+
|
|
65
|
+
// when
|
|
66
|
+
const error = new PublicKeyExtractionError(message);
|
|
67
|
+
|
|
68
|
+
// then
|
|
69
|
+
expect(error).toBeInstanceOf(Error);
|
|
70
|
+
expect(error).toBeInstanceOf(PublicKeyExtractionError);
|
|
71
|
+
expect(error.message).toBe(message);
|
|
72
|
+
expect(error.name).toBe('PublicKeyExtractionError');
|
|
73
|
+
expect(error.cause).toBeUndefined();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should create error with message and cause', () => {
|
|
77
|
+
// given
|
|
78
|
+
const message = 'Invalid DER encoding';
|
|
79
|
+
const cause = new Error('DER parsing failed');
|
|
80
|
+
|
|
81
|
+
// when
|
|
82
|
+
const error = new PublicKeyExtractionError(message, cause);
|
|
83
|
+
|
|
84
|
+
// then
|
|
85
|
+
expect(error.message).toBe(message);
|
|
86
|
+
expect(error.cause).toBe(cause);
|
|
87
|
+
expect(error.name).toBe('PublicKeyExtractionError');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should have stack trace', () => {
|
|
91
|
+
// given
|
|
92
|
+
const message = 'Invalid DER encoding';
|
|
93
|
+
|
|
94
|
+
// when
|
|
95
|
+
const error = new PublicKeyExtractionError(message);
|
|
96
|
+
|
|
97
|
+
// then
|
|
98
|
+
expect(error.stack).toBeDefined();
|
|
99
|
+
expect(error.stack).toContain('PublicKeyExtractionError');
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe('SignatureVerificationError', () => {
|
|
104
|
+
it('should create error with message', () => {
|
|
105
|
+
// given
|
|
106
|
+
const message = 'Signature verification failed';
|
|
107
|
+
|
|
108
|
+
// when
|
|
109
|
+
const error = new SignatureVerificationError(message);
|
|
110
|
+
|
|
111
|
+
// then
|
|
112
|
+
expect(error).toBeInstanceOf(Error);
|
|
113
|
+
expect(error).toBeInstanceOf(SignatureVerificationError);
|
|
114
|
+
expect(error.message).toBe(message);
|
|
115
|
+
expect(error.name).toBe('SignatureVerificationError');
|
|
116
|
+
expect(error.cause).toBeUndefined();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('should create error with message and cause', () => {
|
|
120
|
+
// given
|
|
121
|
+
const message = 'Signature verification failed';
|
|
122
|
+
const cause = { signature: new Uint8Array(64), publicKey: new Uint8Array(32) };
|
|
123
|
+
|
|
124
|
+
// when
|
|
125
|
+
const error = new SignatureVerificationError(message, cause);
|
|
126
|
+
|
|
127
|
+
// then
|
|
128
|
+
expect(error.message).toBe(message);
|
|
129
|
+
expect(error.cause).toBe(cause);
|
|
130
|
+
expect(error.name).toBe('SignatureVerificationError');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should have stack trace', () => {
|
|
134
|
+
// given
|
|
135
|
+
const message = 'Signature verification failed';
|
|
136
|
+
|
|
137
|
+
// when
|
|
138
|
+
const error = new SignatureVerificationError(message);
|
|
139
|
+
|
|
140
|
+
// then
|
|
141
|
+
expect(error.stack).toBeDefined();
|
|
142
|
+
expect(error.stack).toContain('SignatureVerificationError');
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe('Error Chaining', () => {
|
|
147
|
+
it('should support error chaining with nested causes', () => {
|
|
148
|
+
// given
|
|
149
|
+
const rootCause = new Error('Root cause error');
|
|
150
|
+
const intermediateCause = new PublicKeyExtractionError('Intermediate error', rootCause);
|
|
151
|
+
|
|
152
|
+
// when
|
|
153
|
+
const topLevelError = new KmsClientError('Top level error', intermediateCause);
|
|
154
|
+
|
|
155
|
+
// then
|
|
156
|
+
expect(topLevelError.cause).toBe(intermediateCause);
|
|
157
|
+
expect((topLevelError.cause as PublicKeyExtractionError).cause).toBe(rootCause);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('should preserve cause immutability', () => {
|
|
161
|
+
// given
|
|
162
|
+
const message = 'Test error';
|
|
163
|
+
const cause = { code: 'TEST_ERROR' };
|
|
164
|
+
const error = new KmsClientError(message, cause);
|
|
165
|
+
|
|
166
|
+
// when/then
|
|
167
|
+
// TypeScript should prevent reassignment:
|
|
168
|
+
// error.cause = { code: 'DIFFERENT_ERROR' }; // This would cause TypeScript error
|
|
169
|
+
|
|
170
|
+
expect(error.cause).toBe(cause);
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
});
|