@solana/keychain-gcp-kms 0.0.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,161 @@
1
+ import { v1 } from '@google-cloud/kms';
2
+ import { assertIsAddress } from '@solana/addresses';
3
+ import { createSignatureDictionary, SignerErrorCode, throwSignerError } from '@solana/keychain-core';
4
+ /**
5
+ * Google Cloud KMS-based signer using EdDSA (Ed25519) signing
6
+ *
7
+ * The GCP KMS key must be created with:
8
+ * - Algorithm: EC_SIGN_ED25519
9
+ * - Purpose: ASYMMETRIC_SIGN
10
+ *
11
+ * Example gcloud CLI command to create a key:
12
+ * ```bash
13
+ * gcloud kms keys create my-key \
14
+ * --keyring=my-keyring \
15
+ * --location=us-east1 \
16
+ * --purpose=asymmetric-signing \
17
+ * --default-algorithm=ec-sign-ed25519
18
+ * ```
19
+ */
20
+ export class GcpKmsSigner {
21
+ constructor(config) {
22
+ if (!config.keyName) {
23
+ throwSignerError(SignerErrorCode.CONFIG_ERROR, {
24
+ message: 'Missing required keyName field',
25
+ });
26
+ }
27
+ if (!config.publicKey) {
28
+ throwSignerError(SignerErrorCode.CONFIG_ERROR, {
29
+ message: 'Missing required publicKey field',
30
+ });
31
+ }
32
+ try {
33
+ assertIsAddress(config.publicKey);
34
+ this.address = config.publicKey;
35
+ }
36
+ catch (error) {
37
+ throwSignerError(SignerErrorCode.CONFIG_ERROR, {
38
+ cause: error,
39
+ message: 'Invalid Solana public key format',
40
+ });
41
+ }
42
+ this.keyName = config.keyName;
43
+ this.requestDelayMs = config.requestDelayMs || 0;
44
+ this.validateRequestDelayMs(this.requestDelayMs);
45
+ this.client = new v1.KeyManagementServiceClient();
46
+ }
47
+ /**
48
+ * Validate request delay ms
49
+ */
50
+ validateRequestDelayMs(requestDelayMs) {
51
+ if (requestDelayMs < 0) {
52
+ throwSignerError(SignerErrorCode.CONFIG_ERROR, {
53
+ message: 'requestDelayMs must not be negative',
54
+ });
55
+ }
56
+ if (requestDelayMs > 3000) {
57
+ console.warn('requestDelayMs is greater than 3000ms, this may result in blockhash expiration errors for signing messages/transactions');
58
+ }
59
+ }
60
+ /**
61
+ * Add delay between concurrent requests
62
+ */
63
+ async delay(index) {
64
+ if (this.requestDelayMs > 0 && index > 0) {
65
+ await new Promise(resolve => setTimeout(resolve, index * this.requestDelayMs));
66
+ }
67
+ }
68
+ /**
69
+ * Sign message bytes using GCP KMS EdDSA signing
70
+ */
71
+ async signBytes(messageBytes) {
72
+ try {
73
+ // GCP KMS AsymmetricSign expects the raw message for Ed25519 (PureEdDSA)
74
+ const [response] = await this.client.asymmetricSign({
75
+ data: messageBytes,
76
+ name: this.keyName,
77
+ });
78
+ if (!response.signature) {
79
+ throwSignerError(SignerErrorCode.REMOTE_API_ERROR, {
80
+ message: 'No signature in GCP KMS response',
81
+ });
82
+ }
83
+ // Ed25519 signatures are 64 bytes
84
+ const signature = response.signature;
85
+ if (signature.length !== 64) {
86
+ throwSignerError(SignerErrorCode.SIGNING_FAILED, {
87
+ message: `Invalid signature length: expected 64 bytes, got ${signature.length}`,
88
+ });
89
+ }
90
+ return signature;
91
+ }
92
+ catch (error) {
93
+ // Re-throw SignerError as-is
94
+ if (error instanceof Error && error.name === 'SignerError') {
95
+ throw error;
96
+ }
97
+ if (error instanceof Error) {
98
+ // GCP SDK errors
99
+ const gcpError = error;
100
+ throwSignerError(SignerErrorCode.REMOTE_API_ERROR, {
101
+ cause: error,
102
+ message: `GCP KMS Sign operation failed: ${gcpError.message || error.message}`,
103
+ status: gcpError.code,
104
+ });
105
+ }
106
+ throwSignerError(SignerErrorCode.REMOTE_API_ERROR, {
107
+ cause: error,
108
+ message: 'GCP KMS Sign operation failed',
109
+ });
110
+ }
111
+ }
112
+ /**
113
+ * Sign multiple messages using GCP KMS
114
+ */
115
+ async signMessages(messages) {
116
+ return await Promise.all(messages.map(async (message, index) => {
117
+ await this.delay(index);
118
+ const messageBytes = message.content instanceof Uint8Array
119
+ ? message.content
120
+ : new Uint8Array(Array.from(message.content));
121
+ const signatureBytes = await this.signBytes(messageBytes);
122
+ return createSignatureDictionary({
123
+ signature: signatureBytes,
124
+ signerAddress: this.address,
125
+ });
126
+ }));
127
+ }
128
+ /**
129
+ * Sign multiple transactions using GCP KMS
130
+ */
131
+ async signTransactions(transactions) {
132
+ return await Promise.all(transactions.map(async (transaction, index) => {
133
+ await this.delay(index);
134
+ // Sign the transaction message bytes
135
+ const signatureBytes = await this.signBytes(new Uint8Array(transaction.messageBytes));
136
+ return createSignatureDictionary({
137
+ signature: signatureBytes,
138
+ signerAddress: this.address,
139
+ });
140
+ }));
141
+ }
142
+ /**
143
+ * Check if GCP KMS is available and the key is accessible
144
+ */
145
+ async isAvailable() {
146
+ try {
147
+ const [publicKey] = await this.client.getPublicKey({
148
+ name: this.keyName,
149
+ });
150
+ if (!publicKey) {
151
+ return false;
152
+ }
153
+ // Verify the algorithm is EC_SIGN_ED25519
154
+ return publicKey.algorithm === 'EC_SIGN_ED25519';
155
+ }
156
+ catch {
157
+ return false;
158
+ }
159
+ }
160
+ }
161
+ //# sourceMappingURL=gcp-kms-signer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gcp-kms-signer.js","sourceRoot":"","sources":["../src/gcp-kms-signer.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ export { GcpKmsSigner } from './gcp-kms-signer.js';
2
+ export type { GcpKmsSignerConfig } from './types.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,YAAY,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { GcpKmsSigner } from './gcp-kms-signer.js';
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Configuration for creating a GcpKmsSigner
3
+ */
4
+ export interface GcpKmsSignerConfig {
5
+ /** Full resource name of the crypto key version */
6
+ keyName: string;
7
+ /** Solana public key (base58-encoded) */
8
+ publicKey: string;
9
+ /** Optional delay in ms between concurrent signing requests to avoid rate limits (default: 0) */
10
+ requestDelayMs?: number;
11
+ }
12
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,WAAW,kBAAkB;IAC/B,mDAAmD;IACnD,OAAO,EAAE,MAAM,CAAC;IAChB,yCAAyC;IACzC,SAAS,EAAE,MAAM,CAAC;IAClB,iGAAiG;IACjG,cAAc,CAAC,EAAE,MAAM,CAAC;CAC3B"}
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
package/package.json CHANGED
@@ -1,12 +1,59 @@
1
1
  {
2
2
  "name": "@solana/keychain-gcp-kms",
3
- "version": "0.0.0",
4
- "description": "",
5
- "license": "ISC",
6
- "author": "",
7
- "type": "commonjs",
8
- "main": "index.js",
3
+ "author": "Solana Foundation",
4
+ "version": "0.4.0",
5
+ "description": "Google Cloud KMS-based signer for Solana transactions using EdDSA (Ed25519)",
6
+ "license": "MIT",
7
+ "repository": "https://github.com/solana-foundation/solana-keychain",
8
+ "keywords": [
9
+ "solana",
10
+ "signing",
11
+ "wallet",
12
+ "gcp",
13
+ "kms",
14
+ "ed25519",
15
+ "eddsa"
16
+ ],
17
+ "type": "module",
18
+ "sideEffects": false,
19
+ "main": "./dist/index.js",
20
+ "types": "./dist/index.d.ts",
21
+ "exports": {
22
+ ".": {
23
+ "types": "./dist/index.d.ts",
24
+ "import": "./dist/index.js"
25
+ }
26
+ },
27
+ "files": [
28
+ "dist",
29
+ "src"
30
+ ],
31
+ "dependencies": {
32
+ "@google-cloud/kms": "^5.2.1",
33
+ "@solana/addresses": "^6.0.1",
34
+ "@solana/keys": "^6.0.1",
35
+ "@solana/signers": "^6.0.1",
36
+ "@solana/transactions": "^6.0.1",
37
+ "@solana/keychain-core": "0.4.0"
38
+ },
39
+ "devDependencies": {
40
+ "@solana-program/memo": "^0.11.0",
41
+ "@solana/kit": "^6.0.1",
42
+ "dotenv": "^17.2.3",
43
+ "@solana/keychain-test-utils": "0.4.0"
44
+ },
45
+ "publishConfig": {
46
+ "access": "public"
47
+ },
9
48
  "scripts": {
10
- "test": "echo \"Error: no test specified\" && exit 1"
49
+ "build": "tsc --build",
50
+ "clean": "rm -rf dist *.tsbuildinfo",
51
+ "test": "vitest run",
52
+ "test:unit": "vitest run --config ../../vitest.config.unit.ts",
53
+ "test:integration": "vitest run --config ../../vitest.config.integration.ts",
54
+ "test:watch": "vitest",
55
+ "test:watch:unit": "vitest --config ../../vitest.config.unit.ts",
56
+ "test:watch:integration": "vitest --config ../../vitest.config.integration.ts",
57
+ "typecheck": "tsc --noEmit"
11
58
  }
12
- }
59
+ }
@@ -0,0 +1,82 @@
1
+ import {
2
+ appendTransactionMessageInstructions,
3
+ createSolanaRpc,
4
+ createTransactionMessage,
5
+ pipe,
6
+ setTransactionMessageFeePayerSigner,
7
+ setTransactionMessageLifetimeUsingBlockhash,
8
+ signTransactionMessageWithSigners,
9
+ } from '@solana/kit';
10
+ import { getAddMemoInstruction } from '@solana-program/memo';
11
+ import { config } from 'dotenv';
12
+ import { describe, expect, it } from 'vitest';
13
+
14
+ import { GcpKmsSigner } from '../gcp-kms-signer.js';
15
+
16
+ config();
17
+
18
+ const REQUIRED_ENV_VARS = ['GCP_KMS_KEY_NAME', 'GCP_KMS_SIGNER_PUBKEY'];
19
+
20
+ function hasRequiredEnvVars(): boolean {
21
+ return REQUIRED_ENV_VARS.every(v => process.env[v]);
22
+ }
23
+
24
+ function createGcpKmsSigner(): GcpKmsSigner {
25
+ return new GcpKmsSigner({
26
+ keyName: process.env.GCP_KMS_KEY_NAME!,
27
+ publicKey: process.env.GCP_KMS_SIGNER_PUBKEY!,
28
+ });
29
+ }
30
+
31
+ describe('GcpKmsSigner Integration', () => {
32
+ it.skipIf(!hasRequiredEnvVars())(
33
+ 'signs transactions with GCP KMS',
34
+ async () => {
35
+ const signer = createGcpKmsSigner();
36
+ const rpcUrl = process.env.SOLANA_RPC_URL ?? 'https://api.devnet.solana.com';
37
+
38
+ // Get real blockhash from devnet
39
+ const rpc = createSolanaRpc(rpcUrl);
40
+ const {
41
+ value: { blockhash, lastValidBlockHeight },
42
+ } = await rpc.getLatestBlockhash().send();
43
+
44
+ // Create memo transaction (doesn't need funds)
45
+ const transaction = pipe(
46
+ createTransactionMessage({ version: 0 }),
47
+ tx => setTransactionMessageFeePayerSigner(signer, tx),
48
+ tx => appendTransactionMessageInstructions([getAddMemoInstruction({ memo: 'GCP KMS test' })], tx),
49
+ tx => setTransactionMessageLifetimeUsingBlockhash({ blockhash, lastValidBlockHeight }, tx),
50
+ );
51
+
52
+ // Sign via GCP KMS
53
+ const signed = await signTransactionMessageWithSigners(transaction);
54
+
55
+ // Verify signature returned
56
+ expect(signed.signatures[signer.address]).toBeDefined();
57
+ expect(signed.signatures[signer.address]?.length).toBe(64);
58
+ },
59
+ 60_000,
60
+ ); // 1 minute timeout
61
+
62
+ it.skipIf(!hasRequiredEnvVars())('signs messages', async () => {
63
+ const signer = createGcpKmsSigner();
64
+
65
+ const message = {
66
+ content: new Uint8Array([1, 2, 3, 4, 5]),
67
+ signatures: {},
68
+ };
69
+
70
+ const result = await signer.signMessages([message]);
71
+
72
+ expect(result).toHaveLength(1);
73
+ expect(result[0]?.[signer.address]).toBeDefined();
74
+ expect(result[0]?.[signer.address]?.length).toBe(64);
75
+ });
76
+
77
+ it.skipIf(!hasRequiredEnvVars())('checks availability', async () => {
78
+ const signer = createGcpKmsSigner();
79
+ const available = await signer.isAvailable();
80
+ expect(available).toBe(true);
81
+ });
82
+ });