@dnsid-ai/key-gcp 0.35.0 → 0.36.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 CHANGED
@@ -1,8 +1,10 @@
1
1
  # @dnsid-ai/key-gcp
2
2
 
3
- Optional Node.js Google Cloud KMS provider for an existing **software
4
- EC_SIGN_ED25519** key version. Install this package separately from the base SDK;
5
- its Google dependency is not required by base SDK consumers.
3
+ Optional Node.js Google Cloud KMS provider that opens existing **software
4
+ EC_SIGN_ED25519** key versions or explicitly creates a pending first version
5
+ through `create()` under an existing compatible CryptoKey. Install this package
6
+ separately from the base SDK; its Google dependency is not required by base SDK
7
+ consumers.
6
8
 
7
9
  ```ts
8
10
  import { KeyManagementServiceClient } from '@google-cloud/kms';
@@ -14,13 +16,28 @@ const keyProvider = await GcpCloudKmsKeyProvider.load(new KeyManagementServiceCl
14
16
  });
15
17
  ```
16
18
 
17
- Use ambient Google credentials with permission to read the public key and sign.
19
+ Use ambient Google credentials with `cloudkms.cryptoKeyVersions.viewPublicKey`
20
+ and `cloudkms.cryptoKeyVersions.useToSign` permissions. Both initial `create()`
21
+ and rotation through `generateKey()` also require
22
+ `cloudkms.cryptoKeyVersions.create` and `cloudkms.cryptoKeyVersions.get` for
23
+ version creation and readiness metadata reads. Initial `create()` additionally
24
+ requires `cloudkms.cryptoKeys.get` to validate the existing parent. Grant the
25
+ version metadata-read permission before either operation: a readiness read can
26
+ fail after the remote version has already been created.
18
27
  The reference must name a numeric immutable version, not a parent or alias. The
19
28
  provider exports only the public Ed25519 JWK, with an RFC 7638 thumbprint as `kid`.
20
29
  It sends raw data (never a digest) and verifies request/response CRC32C integrity
21
30
  and returned version bindings. Google transport and permission errors propagate;
22
31
  there is no local signing fallback, key import, or generation during opening.
23
32
 
33
+ For first-key creation, explicitly call `GcpCloudKmsKeyProvider.create` with an
34
+ existing `ASYMMETRIC_SIGN` CryptoKey parent whose version template is
35
+ `EC_SIGN_ED25519` / `SOFTWARE`; no prior version is required. See the
36
+ [KMS-first setup guide](https://github.com/dnsid-ai/dnsid-ts/blob/main/docs/kms-first-setup.md) for pending-key activation,
37
+ reference handoff, publication boundaries, and failure recovery. Subsequent
38
+ startup must reopen the saved immutable version with `load`, never recreate it;
39
+ normal configuration remains open-only.
40
+
24
41
  Configuration selects this optional package with `provider: "google-kms"`, the
25
42
  immutable version as `keyRef`, and optional `settings.algorithm: "EdDSA"` (the
26
43
  only supported deployment algorithm). SDK composition translates `EdDSA` to the
@@ -39,7 +56,12 @@ const keySource = {
39
56
  An explicit `generateKey()` or `generateKey('EdDSA')` creates a pending software
40
57
  Ed25519 version under the same CryptoKey. Other requested algorithms are rejected
41
58
  before creation. `validateKeyGeneration('EdDSA')` checks algorithm support without
42
- creating a version. Generation does not change deployment configuration or the active signer.
59
+ creating a version. Like first-key creation, generation waits for the exact created
60
+ version to become enabled, polling pending generation at most 20 times, 250ms apart.
61
+ Transport call deadlines remain caller-owned. Post-create failures retain the known
62
+ version reference and cause; service/integrity failures are not caller-argument
63
+ errors. There is no automatic recreation or deletion on failure.
64
+ Generation does not change deployment configuration or the active signer.
43
65
  Pending keys support `jwk` and `signKey`; `activate` drains in-flight signatures
44
66
  and blocks new signing while promoting the pending key. `listKeyIds` contains
45
67
  only the active ID, or is empty after local supersession. `supersede` and its
@@ -47,6 +69,14 @@ only the active ID, or is empty after local supersession. `supersede` and its
47
69
  destroyed. Historical public keys remain available through `jwk`.
48
70
 
49
71
  Lifecycle state is in memory. Applications own durable recovery and publication
50
- orchestration; reopening the configured version does not recover pending state.
72
+ orchestration. For recovery, pass the application's saved immutable A reference
73
+ as `activeKeyVersion` and saved B reference as optional `pendingKeyVersion` to
74
+ `load`. Both versions and public bindings are validated; duplicate versions or
75
+ public-key identities are rejected. B is inserted directly as pending: `jwk` and
76
+ rotation `signKey` are available, while `signingKey`, `sign`, and `listKeyIds`
77
+ continue to select only A. Existing `activate` performs the local transition.
78
+ This input is saved application state, not key discovery or permission to activate;
79
+ managed recovery still validates the saved rotation's exact bindings. Ordinary
80
+ A-only loading is unchanged and does not discover pending state.
51
81
  Retained-key configuration loading is rejected. Lifecycle behavior implements
52
82
  the provisional shared #56 clarification, not a claim of contract acceptance.
package/dist/index.cjs CHANGED
@@ -39,8 +39,10 @@ var GcpCloudKmsKeyProvider = class _GcpCloudKmsKeyProvider {
39
39
  constructor(client, version, publicKey) {
40
40
  this.client = client;
41
41
  this.version = version;
42
- this.activeKid = publicKey.kid;
43
- this.keys.set(publicKey.kid, { version, jwk: publicKey, state: "active" });
42
+ if (publicKey) {
43
+ this.activeKid = publicKey.kid;
44
+ this.keys.set(publicKey.kid, { version, jwk: publicKey, state: "active" });
45
+ }
44
46
  }
45
47
  client;
46
48
  version;
@@ -48,10 +50,46 @@ var GcpCloudKmsKeyProvider = class _GcpCloudKmsKeyProvider {
48
50
  activeKid;
49
51
  signing = /* @__PURE__ */ new Set();
50
52
  activating = false;
53
+ /**
54
+ * Creates one version under an existing ASYMMETRIC_SIGN CryptoKey with a
55
+ * SOFTWARE EC_SIGN_ED25519 template, without opening any prior version.
56
+ * Returns a locally pending provider and immutable reference; activate(kid)
57
+ * explicitly before initial manager injection. Never provisions the parent,
58
+ * writes files, deletes keys, or closes the caller-owned client.
59
+ * Pending generation polls the same version at most 20 times, 250ms apart.
60
+ * Each transport call uses the caller's client timeout configuration.
61
+ * Failed/indeterminate creation may leave an operator-owned remote version;
62
+ * do not blindly retry. Normal load/configuration remains open-only.
63
+ */
64
+ static async create(client, config) {
65
+ if (config.algorithm !== "EC_SIGN_ED25519") throw new import_protocol.ArgumentError("Google Cloud KMS algorithm must be EC_SIGN_ED25519");
66
+ if (typeof config.parent !== "string" || !/^projects\/[A-Za-z0-9._:-]+\/locations\/[A-Za-z0-9_-]+\/keyRings\/[A-Za-z0-9_-]+\/cryptoKeys\/[A-Za-z0-9_-]+$/.test(config.parent)) {
67
+ throw new import_protocol.ArgumentError("Google Cloud KMS creation requires a CryptoKey parent");
68
+ }
69
+ if (!client.getCryptoKey || !client.createCryptoKeyVersion) throw new import_protocol.ArgumentError("Google Cloud KMS client does not support initial creation");
70
+ const [raw] = await client.getCryptoKey({ name: config.parent });
71
+ const parent = raw;
72
+ if (parent?.name !== config.parent || parent.purpose !== "ASYMMETRIC_SIGN" && parent.purpose !== 5 || !ed25519Software(parent.versionTemplate)) {
73
+ throw new import_protocol.ArgumentError("Google Cloud KMS requires a compatible signing parent/template");
74
+ }
75
+ const provider = new _GcpCloudKmsKeyProvider(client, config.parent);
76
+ const kid = await provider.generatePendingKey(void 0);
77
+ return { provider, kid, keyRef: provider.keys.get(kid).version };
78
+ }
79
+ /**
80
+ * Opens the selected active version and, optionally, an application-saved pending
81
+ * version for recovery. Both bindings are validated; the pending key supports
82
+ * JWK lookup and rotation proof signing but is not the active signer or live
83
+ * inventory. No discovery, generation, or activation occurs. Applications own
84
+ * durable references and recovery checks; activate performs the local transition.
85
+ */
51
86
  static async load(client, config) {
52
87
  const version = config.activeKeyVersion;
53
- if (typeof version !== "string" || !/^projects\/[A-Za-z0-9._:-]+\/locations\/[A-Za-z0-9_-]+\/keyRings\/[A-Za-z0-9_-]+\/cryptoKeys\/[A-Za-z0-9_-]+\/cryptoKeyVersions\/[1-9][0-9]*$/.test(version)) {
54
- throw new import_protocol.ArgumentError("Google Cloud KMS requires an immutable CryptoKeyVersion name");
88
+ validateVersion(version);
89
+ const pendingVersion = config.pendingKeyVersion;
90
+ if (pendingVersion !== void 0) {
91
+ validateVersion(pendingVersion);
92
+ if (pendingVersion === version) throw new import_protocol.ArgumentError("Google Cloud KMS active and pending versions must differ");
55
93
  }
56
94
  if (config.algorithm !== "EC_SIGN_ED25519") throw new import_protocol.ArgumentError("Google Cloud KMS algorithm must be EC_SIGN_ED25519");
57
95
  if (config.retainedKeyVersions?.length) throw new import_protocol.ArgumentError("Google Cloud KMS retained key loading is not supported");
@@ -59,26 +97,12 @@ var GcpCloudKmsKeyProvider = class _GcpCloudKmsKeyProvider {
59
97
  const { KeyManagementServiceClient } = await import("@google-cloud/kms");
60
98
  client = new KeyManagementServiceClient();
61
99
  }
62
- const [raw] = await client.getPublicKey({ name: version });
63
- const response = raw;
64
- if (response?.name !== version) throw new import_protocol.ArgumentError("Google Cloud KMS public key version mismatch");
65
- if (response.algorithm !== "EC_SIGN_ED25519" && response.algorithm !== 40) throw new import_protocol.ArgumentError("Google Cloud KMS public key algorithm mismatch");
66
- if (response.protectionLevel !== "SOFTWARE" && response.protectionLevel !== 1) throw new import_protocol.ArgumentError("Google Cloud KMS public key must use SOFTWARE protection");
67
- if (typeof response.pem !== "string" || !checksumMatches(Buffer.from(response.pem), response.pemCrc32c)) {
68
- throw new import_protocol.ArgumentError("Google Cloud KMS public PEM checksum mismatch");
69
- }
70
- let publicKey;
71
- try {
72
- if (!response.pem.startsWith("-----BEGIN PUBLIC KEY-----")) throw new Error();
73
- const key = (0, import_node_crypto.createPublicKey)({ key: response.pem, format: "pem", type: "spki" });
74
- if (key.asymmetricKeyType !== "ed25519") throw new Error();
75
- const exported = key.export({ format: "jwk" });
76
- publicKey = { kty: "OKP", crv: "Ed25519", x: exported.x, kid: "", alg: "EdDSA", use: "sig" };
77
- } catch {
78
- throw new import_protocol.ArgumentError("Google Cloud KMS returned invalid Ed25519 public SPKI");
79
- }
80
- publicKey.kid = await (0, import_protocol.jwkThumbprint)(publicKey);
81
- return new _GcpCloudKmsKeyProvider(client, version, Object.freeze(publicKey));
100
+ const publicKey = await loadPublicKey(client, version);
101
+ const pendingKey = pendingVersion === void 0 ? void 0 : await loadPublicKey(client, pendingVersion);
102
+ if (pendingKey?.kid === publicKey.kid) throw new import_protocol.ArgumentError("Google Cloud KMS pending version has duplicate public key");
103
+ const provider = new _GcpCloudKmsKeyProvider(client, version, publicKey);
104
+ if (pendingKey) provider.keys.set(pendingKey.kid, { version: pendingVersion, jwk: pendingKey, state: "pending" });
105
+ return provider;
82
106
  }
83
107
  async signingKey() {
84
108
  if (!this.activeKid) throw new import_protocol.ArgumentError("Google Cloud KMS has no active key");
@@ -127,21 +151,46 @@ var GcpCloudKmsKeyProvider = class _GcpCloudKmsKeyProvider {
127
151
  async validateKeyGeneration(algorithm) {
128
152
  if (algorithm !== "EdDSA") throw new import_protocol.ArgumentError(`Google Cloud KMS generation settings do not support ${algorithm}`);
129
153
  }
130
- /** Explicit operator generation only; defaults to EdDSA and rejects other algorithms before creation. Never called by configuration opening. */
154
+ /**
155
+ * Explicit operator generation only; defaults to EdDSA and rejects other algorithms before creation.
156
+ * Pending generation polls the exact created version at most 20 times, 250ms apart,
157
+ * using the caller's transport timeouts, just as initial creation does.
158
+ * Returns a locally pending key without changing the active signer. Failures after
159
+ * creation retain the version reference and cause, without retrying or deleting it.
160
+ * Service/integrity failures are not caller-argument errors. Never called by configuration opening.
161
+ */
131
162
  async generateKey(algorithm) {
163
+ return this.generatePendingKey(algorithm);
164
+ }
165
+ async generatePendingKey(algorithm) {
132
166
  await this.validateKeyGeneration(algorithm ?? "EdDSA");
133
167
  if (!this.client.createCryptoKeyVersion) throw new import_protocol.ArgumentError("Google Cloud KMS client does not support version creation");
134
- const parent = this.version.slice(0, this.version.lastIndexOf("/cryptoKeyVersions/"));
168
+ const parent = this.version.includes("/cryptoKeyVersions/") ? this.version.slice(0, this.version.lastIndexOf("/cryptoKeyVersions/")) : this.version;
135
169
  const [raw] = await this.client.createCryptoKeyVersion({ parent, cryptoKeyVersion: { state: "ENABLED" } });
136
- const created = raw;
137
- if (!created?.name?.startsWith(parent + "/cryptoKeyVersions/") || created.algorithm !== "EC_SIGN_ED25519" && created.algorithm !== 40 || created.protectionLevel !== "SOFTWARE" && created.protectionLevel !== 1 || created.state !== "ENABLED" && created.state !== 1) {
138
- throw new import_protocol.ArgumentError("Google Cloud KMS created an unexpected key version");
170
+ let created = raw;
171
+ const name = created?.name;
172
+ if (typeof name !== "string" || !name.startsWith(parent + "/cryptoKeyVersions/") || !/^[1-9][0-9]*$/.test(name.slice((parent + "/cryptoKeyVersions/").length))) {
173
+ throw new import_protocol.ArgumentError(`Google Cloud KMS created an unexpected key version${typeof name === "string" && name ? `: ${name}` : ""}`);
174
+ }
175
+ try {
176
+ for (let poll = 0; ; poll++) {
177
+ if (created?.name !== name || !ed25519Software(created)) throw new import_protocol.ArgumentError("Google Cloud KMS created an unexpected key version");
178
+ if (created.state === "ENABLED" || created.state === 1) break;
179
+ if (created.state !== "PENDING_GENERATION" && created.state !== 5 || poll >= 20 || !this.client.getCryptoKeyVersion) {
180
+ throw new import_protocol.ArgumentError("Google Cloud KMS version is not ready for signing");
181
+ }
182
+ await new Promise((resolve) => setTimeout(resolve, 250));
183
+ const [metadata] = await this.client.getCryptoKeyVersion({ name });
184
+ created = metadata;
185
+ }
186
+ const pending = await _GcpCloudKmsKeyProvider.load(this.client, { activeKeyVersion: name, algorithm: "EC_SIGN_ED25519" });
187
+ const jwk = await pending.signingKey();
188
+ if (this.keys.has(jwk.kid)) throw new import_protocol.ArgumentError("Google Cloud KMS generated duplicate public key");
189
+ this.keys.set(jwk.kid, { version: name, jwk: Object.freeze(jwk), state: "pending" });
190
+ return jwk.kid;
191
+ } catch (cause) {
192
+ throw new Error(`Google Cloud KMS created version ${name}: ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
139
193
  }
140
- const pending = await _GcpCloudKmsKeyProvider.load(this.client, { activeKeyVersion: created.name, algorithm: "EC_SIGN_ED25519" });
141
- const jwk = await pending.signingKey();
142
- if (this.keys.has(jwk.kid)) throw new import_protocol.ArgumentError("Google Cloud KMS generated duplicate public key");
143
- this.keys.set(jwk.kid, { version: created.name, jwk: Object.freeze(jwk), state: "pending" });
144
- return jwk.kid;
145
194
  }
146
195
  async activate(kid) {
147
196
  if (this.activating) throw new import_protocol.ArgumentError("Google Cloud KMS activation is already in progress");
@@ -176,6 +225,33 @@ var GcpCloudKmsKeyProvider = class _GcpCloudKmsKeyProvider {
176
225
  return this.supersede(kid);
177
226
  }
178
227
  };
228
+ function validateVersion(version) {
229
+ if (typeof version !== "string" || !/^projects\/[A-Za-z0-9._:-]+\/locations\/[A-Za-z0-9_-]+\/keyRings\/[A-Za-z0-9_-]+\/cryptoKeys\/[A-Za-z0-9_-]+\/cryptoKeyVersions\/[1-9][0-9]*$/.test(version)) {
230
+ throw new import_protocol.ArgumentError("Google Cloud KMS requires an immutable CryptoKeyVersion name");
231
+ }
232
+ }
233
+ async function loadPublicKey(client, version) {
234
+ const [raw] = await client.getPublicKey({ name: version });
235
+ const response = raw;
236
+ if (response?.name !== version) throw new import_protocol.ArgumentError("Google Cloud KMS public key version mismatch");
237
+ if (response.algorithm !== "EC_SIGN_ED25519" && response.algorithm !== 40) throw new import_protocol.ArgumentError("Google Cloud KMS public key algorithm mismatch");
238
+ if (response.protectionLevel !== "SOFTWARE" && response.protectionLevel !== 1) throw new import_protocol.ArgumentError("Google Cloud KMS public key must use SOFTWARE protection");
239
+ if (typeof response.pem !== "string" || !checksumMatches(Buffer.from(response.pem), response.pemCrc32c)) {
240
+ throw new import_protocol.ArgumentError("Google Cloud KMS public PEM checksum mismatch");
241
+ }
242
+ let publicKey;
243
+ try {
244
+ if (!response.pem.startsWith("-----BEGIN PUBLIC KEY-----")) throw new Error();
245
+ const key = (0, import_node_crypto.createPublicKey)({ key: response.pem, format: "pem", type: "spki" });
246
+ if (key.asymmetricKeyType !== "ed25519") throw new Error();
247
+ const exported = key.export({ format: "jwk" });
248
+ publicKey = { kty: "OKP", crv: "Ed25519", x: exported.x, kid: "", alg: "EdDSA", use: "sig" };
249
+ } catch {
250
+ throw new import_protocol.ArgumentError("Google Cloud KMS returned invalid Ed25519 public SPKI");
251
+ }
252
+ publicKey.kid = await (0, import_protocol.jwkThumbprint)(publicKey);
253
+ return Object.freeze(publicKey);
254
+ }
179
255
  function checksumMatches(bytes, checksum) {
180
256
  const raw = checksum?.value;
181
257
  if (raw === void 0 || raw === null) return false;
@@ -192,6 +268,9 @@ function crc32c(bytes) {
192
268
  }
193
269
  return (crc ^ 4294967295) >>> 0;
194
270
  }
271
+ function ed25519Software(value) {
272
+ return !!value && (value.algorithm === "EC_SIGN_ED25519" || value.algorithm === 40) && (value.protectionLevel === "SOFTWARE" || value.protectionLevel === 1);
273
+ }
195
274
  // Annotate the CommonJS export names for ESM import in node:
196
275
  0 && (module.exports = {
197
276
  GcpCloudKmsKeyProvider
package/dist/index.d.cts CHANGED
@@ -3,6 +3,8 @@ import { KeyProvider, DnsIdJWK } from '@dnsid-ai/protocol';
3
3
  interface GcpCloudKmsConfig {
4
4
  /** Fully qualified immutable CryptoKeyVersion resource name. */
5
5
  activeKeyVersion: string;
6
+ /** Application-saved immutable pending version; never discovered or activated by load. */
7
+ pendingKeyVersion?: string;
6
8
  /** Retained-key loading is not supported by this runtime slice. */
7
9
  retainedKeyVersions?: string[];
8
10
  /** Must be EC_SIGN_ED25519. */
@@ -10,6 +12,14 @@ interface GcpCloudKmsConfig {
10
12
  }
11
13
  /** Structural seam implemented by the official KeyManagementServiceClient. */
12
14
  interface GcpCloudKmsClient {
15
+ /** Required only for initial creation; reads the existing signing parent/template. */
16
+ getCryptoKey?(request: {
17
+ name: string;
18
+ }): Promise<readonly unknown[]>;
19
+ /** Required when a newly created version is still PENDING_GENERATION. */
20
+ getCryptoKeyVersion?(request: {
21
+ name: string;
22
+ }): Promise<readonly unknown[]>;
13
23
  createCryptoKeyVersion?(request: {
14
24
  parent: string;
15
25
  cryptoKeyVersion: {
@@ -27,7 +37,7 @@ interface GcpCloudKmsClient {
27
37
  };
28
38
  }): Promise<readonly unknown[]>;
29
39
  }
30
- /** Opens one existing software Ed25519 version without cloud mutation or fallback. */
40
+ /** Opens existing software Ed25519 versions, or explicitly creates pending versions. */
31
41
  declare class GcpCloudKmsKeyProvider implements KeyProvider {
32
42
  private readonly client;
33
43
  private readonly version;
@@ -36,6 +46,32 @@ declare class GcpCloudKmsKeyProvider implements KeyProvider {
36
46
  private readonly signing;
37
47
  private activating;
38
48
  private constructor();
49
+ /**
50
+ * Creates one version under an existing ASYMMETRIC_SIGN CryptoKey with a
51
+ * SOFTWARE EC_SIGN_ED25519 template, without opening any prior version.
52
+ * Returns a locally pending provider and immutable reference; activate(kid)
53
+ * explicitly before initial manager injection. Never provisions the parent,
54
+ * writes files, deletes keys, or closes the caller-owned client.
55
+ * Pending generation polls the same version at most 20 times, 250ms apart.
56
+ * Each transport call uses the caller's client timeout configuration.
57
+ * Failed/indeterminate creation may leave an operator-owned remote version;
58
+ * do not blindly retry. Normal load/configuration remains open-only.
59
+ */
60
+ static create(client: GcpCloudKmsClient, config: {
61
+ parent: string;
62
+ algorithm: string;
63
+ }): Promise<{
64
+ provider: GcpCloudKmsKeyProvider;
65
+ kid: string;
66
+ keyRef: string;
67
+ }>;
68
+ /**
69
+ * Opens the selected active version and, optionally, an application-saved pending
70
+ * version for recovery. Both bindings are validated; the pending key supports
71
+ * JWK lookup and rotation proof signing but is not the active signer or live
72
+ * inventory. No discovery, generation, or activation occurs. Applications own
73
+ * durable references and recovery checks; activate performs the local transition.
74
+ */
39
75
  static load(client: GcpCloudKmsClient | undefined, config: GcpCloudKmsConfig): Promise<GcpCloudKmsKeyProvider>;
40
76
  signingKey(): Promise<DnsIdJWK>;
41
77
  jwk(kid: string): Promise<DnsIdJWK>;
@@ -49,8 +85,16 @@ declare class GcpCloudKmsKeyProvider implements KeyProvider {
49
85
  signKey(kid: string, payload: Uint8Array): Promise<Uint8Array>;
50
86
  private signVersion;
51
87
  validateKeyGeneration(algorithm: string): Promise<void>;
52
- /** Explicit operator generation only; defaults to EdDSA and rejects other algorithms before creation. Never called by configuration opening. */
88
+ /**
89
+ * Explicit operator generation only; defaults to EdDSA and rejects other algorithms before creation.
90
+ * Pending generation polls the exact created version at most 20 times, 250ms apart,
91
+ * using the caller's transport timeouts, just as initial creation does.
92
+ * Returns a locally pending key without changing the active signer. Failures after
93
+ * creation retain the version reference and cause, without retrying or deleting it.
94
+ * Service/integrity failures are not caller-argument errors. Never called by configuration opening.
95
+ */
53
96
  generateKey(algorithm?: string): Promise<string>;
97
+ private generatePendingKey;
54
98
  activate(kid: string): Promise<void>;
55
99
  /**
56
100
  * Removes local signing eligibility from an active, pending, or already
package/dist/index.d.ts CHANGED
@@ -3,6 +3,8 @@ import { KeyProvider, DnsIdJWK } from '@dnsid-ai/protocol';
3
3
  interface GcpCloudKmsConfig {
4
4
  /** Fully qualified immutable CryptoKeyVersion resource name. */
5
5
  activeKeyVersion: string;
6
+ /** Application-saved immutable pending version; never discovered or activated by load. */
7
+ pendingKeyVersion?: string;
6
8
  /** Retained-key loading is not supported by this runtime slice. */
7
9
  retainedKeyVersions?: string[];
8
10
  /** Must be EC_SIGN_ED25519. */
@@ -10,6 +12,14 @@ interface GcpCloudKmsConfig {
10
12
  }
11
13
  /** Structural seam implemented by the official KeyManagementServiceClient. */
12
14
  interface GcpCloudKmsClient {
15
+ /** Required only for initial creation; reads the existing signing parent/template. */
16
+ getCryptoKey?(request: {
17
+ name: string;
18
+ }): Promise<readonly unknown[]>;
19
+ /** Required when a newly created version is still PENDING_GENERATION. */
20
+ getCryptoKeyVersion?(request: {
21
+ name: string;
22
+ }): Promise<readonly unknown[]>;
13
23
  createCryptoKeyVersion?(request: {
14
24
  parent: string;
15
25
  cryptoKeyVersion: {
@@ -27,7 +37,7 @@ interface GcpCloudKmsClient {
27
37
  };
28
38
  }): Promise<readonly unknown[]>;
29
39
  }
30
- /** Opens one existing software Ed25519 version without cloud mutation or fallback. */
40
+ /** Opens existing software Ed25519 versions, or explicitly creates pending versions. */
31
41
  declare class GcpCloudKmsKeyProvider implements KeyProvider {
32
42
  private readonly client;
33
43
  private readonly version;
@@ -36,6 +46,32 @@ declare class GcpCloudKmsKeyProvider implements KeyProvider {
36
46
  private readonly signing;
37
47
  private activating;
38
48
  private constructor();
49
+ /**
50
+ * Creates one version under an existing ASYMMETRIC_SIGN CryptoKey with a
51
+ * SOFTWARE EC_SIGN_ED25519 template, without opening any prior version.
52
+ * Returns a locally pending provider and immutable reference; activate(kid)
53
+ * explicitly before initial manager injection. Never provisions the parent,
54
+ * writes files, deletes keys, or closes the caller-owned client.
55
+ * Pending generation polls the same version at most 20 times, 250ms apart.
56
+ * Each transport call uses the caller's client timeout configuration.
57
+ * Failed/indeterminate creation may leave an operator-owned remote version;
58
+ * do not blindly retry. Normal load/configuration remains open-only.
59
+ */
60
+ static create(client: GcpCloudKmsClient, config: {
61
+ parent: string;
62
+ algorithm: string;
63
+ }): Promise<{
64
+ provider: GcpCloudKmsKeyProvider;
65
+ kid: string;
66
+ keyRef: string;
67
+ }>;
68
+ /**
69
+ * Opens the selected active version and, optionally, an application-saved pending
70
+ * version for recovery. Both bindings are validated; the pending key supports
71
+ * JWK lookup and rotation proof signing but is not the active signer or live
72
+ * inventory. No discovery, generation, or activation occurs. Applications own
73
+ * durable references and recovery checks; activate performs the local transition.
74
+ */
39
75
  static load(client: GcpCloudKmsClient | undefined, config: GcpCloudKmsConfig): Promise<GcpCloudKmsKeyProvider>;
40
76
  signingKey(): Promise<DnsIdJWK>;
41
77
  jwk(kid: string): Promise<DnsIdJWK>;
@@ -49,8 +85,16 @@ declare class GcpCloudKmsKeyProvider implements KeyProvider {
49
85
  signKey(kid: string, payload: Uint8Array): Promise<Uint8Array>;
50
86
  private signVersion;
51
87
  validateKeyGeneration(algorithm: string): Promise<void>;
52
- /** Explicit operator generation only; defaults to EdDSA and rejects other algorithms before creation. Never called by configuration opening. */
88
+ /**
89
+ * Explicit operator generation only; defaults to EdDSA and rejects other algorithms before creation.
90
+ * Pending generation polls the exact created version at most 20 times, 250ms apart,
91
+ * using the caller's transport timeouts, just as initial creation does.
92
+ * Returns a locally pending key without changing the active signer. Failures after
93
+ * creation retain the version reference and cause, without retrying or deleting it.
94
+ * Service/integrity failures are not caller-argument errors. Never called by configuration opening.
95
+ */
53
96
  generateKey(algorithm?: string): Promise<string>;
97
+ private generatePendingKey;
54
98
  activate(kid: string): Promise<void>;
55
99
  /**
56
100
  * Removes local signing eligibility from an active, pending, or already
package/dist/index.js CHANGED
@@ -5,8 +5,10 @@ var GcpCloudKmsKeyProvider = class _GcpCloudKmsKeyProvider {
5
5
  constructor(client, version, publicKey) {
6
6
  this.client = client;
7
7
  this.version = version;
8
- this.activeKid = publicKey.kid;
9
- this.keys.set(publicKey.kid, { version, jwk: publicKey, state: "active" });
8
+ if (publicKey) {
9
+ this.activeKid = publicKey.kid;
10
+ this.keys.set(publicKey.kid, { version, jwk: publicKey, state: "active" });
11
+ }
10
12
  }
11
13
  client;
12
14
  version;
@@ -14,10 +16,46 @@ var GcpCloudKmsKeyProvider = class _GcpCloudKmsKeyProvider {
14
16
  activeKid;
15
17
  signing = /* @__PURE__ */ new Set();
16
18
  activating = false;
19
+ /**
20
+ * Creates one version under an existing ASYMMETRIC_SIGN CryptoKey with a
21
+ * SOFTWARE EC_SIGN_ED25519 template, without opening any prior version.
22
+ * Returns a locally pending provider and immutable reference; activate(kid)
23
+ * explicitly before initial manager injection. Never provisions the parent,
24
+ * writes files, deletes keys, or closes the caller-owned client.
25
+ * Pending generation polls the same version at most 20 times, 250ms apart.
26
+ * Each transport call uses the caller's client timeout configuration.
27
+ * Failed/indeterminate creation may leave an operator-owned remote version;
28
+ * do not blindly retry. Normal load/configuration remains open-only.
29
+ */
30
+ static async create(client, config) {
31
+ if (config.algorithm !== "EC_SIGN_ED25519") throw new ArgumentError("Google Cloud KMS algorithm must be EC_SIGN_ED25519");
32
+ if (typeof config.parent !== "string" || !/^projects\/[A-Za-z0-9._:-]+\/locations\/[A-Za-z0-9_-]+\/keyRings\/[A-Za-z0-9_-]+\/cryptoKeys\/[A-Za-z0-9_-]+$/.test(config.parent)) {
33
+ throw new ArgumentError("Google Cloud KMS creation requires a CryptoKey parent");
34
+ }
35
+ if (!client.getCryptoKey || !client.createCryptoKeyVersion) throw new ArgumentError("Google Cloud KMS client does not support initial creation");
36
+ const [raw] = await client.getCryptoKey({ name: config.parent });
37
+ const parent = raw;
38
+ if (parent?.name !== config.parent || parent.purpose !== "ASYMMETRIC_SIGN" && parent.purpose !== 5 || !ed25519Software(parent.versionTemplate)) {
39
+ throw new ArgumentError("Google Cloud KMS requires a compatible signing parent/template");
40
+ }
41
+ const provider = new _GcpCloudKmsKeyProvider(client, config.parent);
42
+ const kid = await provider.generatePendingKey(void 0);
43
+ return { provider, kid, keyRef: provider.keys.get(kid).version };
44
+ }
45
+ /**
46
+ * Opens the selected active version and, optionally, an application-saved pending
47
+ * version for recovery. Both bindings are validated; the pending key supports
48
+ * JWK lookup and rotation proof signing but is not the active signer or live
49
+ * inventory. No discovery, generation, or activation occurs. Applications own
50
+ * durable references and recovery checks; activate performs the local transition.
51
+ */
17
52
  static async load(client, config) {
18
53
  const version = config.activeKeyVersion;
19
- if (typeof version !== "string" || !/^projects\/[A-Za-z0-9._:-]+\/locations\/[A-Za-z0-9_-]+\/keyRings\/[A-Za-z0-9_-]+\/cryptoKeys\/[A-Za-z0-9_-]+\/cryptoKeyVersions\/[1-9][0-9]*$/.test(version)) {
20
- throw new ArgumentError("Google Cloud KMS requires an immutable CryptoKeyVersion name");
54
+ validateVersion(version);
55
+ const pendingVersion = config.pendingKeyVersion;
56
+ if (pendingVersion !== void 0) {
57
+ validateVersion(pendingVersion);
58
+ if (pendingVersion === version) throw new ArgumentError("Google Cloud KMS active and pending versions must differ");
21
59
  }
22
60
  if (config.algorithm !== "EC_SIGN_ED25519") throw new ArgumentError("Google Cloud KMS algorithm must be EC_SIGN_ED25519");
23
61
  if (config.retainedKeyVersions?.length) throw new ArgumentError("Google Cloud KMS retained key loading is not supported");
@@ -25,26 +63,12 @@ var GcpCloudKmsKeyProvider = class _GcpCloudKmsKeyProvider {
25
63
  const { KeyManagementServiceClient } = await import("@google-cloud/kms");
26
64
  client = new KeyManagementServiceClient();
27
65
  }
28
- const [raw] = await client.getPublicKey({ name: version });
29
- const response = raw;
30
- if (response?.name !== version) throw new ArgumentError("Google Cloud KMS public key version mismatch");
31
- if (response.algorithm !== "EC_SIGN_ED25519" && response.algorithm !== 40) throw new ArgumentError("Google Cloud KMS public key algorithm mismatch");
32
- if (response.protectionLevel !== "SOFTWARE" && response.protectionLevel !== 1) throw new ArgumentError("Google Cloud KMS public key must use SOFTWARE protection");
33
- if (typeof response.pem !== "string" || !checksumMatches(Buffer.from(response.pem), response.pemCrc32c)) {
34
- throw new ArgumentError("Google Cloud KMS public PEM checksum mismatch");
35
- }
36
- let publicKey;
37
- try {
38
- if (!response.pem.startsWith("-----BEGIN PUBLIC KEY-----")) throw new Error();
39
- const key = createPublicKey({ key: response.pem, format: "pem", type: "spki" });
40
- if (key.asymmetricKeyType !== "ed25519") throw new Error();
41
- const exported = key.export({ format: "jwk" });
42
- publicKey = { kty: "OKP", crv: "Ed25519", x: exported.x, kid: "", alg: "EdDSA", use: "sig" };
43
- } catch {
44
- throw new ArgumentError("Google Cloud KMS returned invalid Ed25519 public SPKI");
45
- }
46
- publicKey.kid = await jwkThumbprint(publicKey);
47
- return new _GcpCloudKmsKeyProvider(client, version, Object.freeze(publicKey));
66
+ const publicKey = await loadPublicKey(client, version);
67
+ const pendingKey = pendingVersion === void 0 ? void 0 : await loadPublicKey(client, pendingVersion);
68
+ if (pendingKey?.kid === publicKey.kid) throw new ArgumentError("Google Cloud KMS pending version has duplicate public key");
69
+ const provider = new _GcpCloudKmsKeyProvider(client, version, publicKey);
70
+ if (pendingKey) provider.keys.set(pendingKey.kid, { version: pendingVersion, jwk: pendingKey, state: "pending" });
71
+ return provider;
48
72
  }
49
73
  async signingKey() {
50
74
  if (!this.activeKid) throw new ArgumentError("Google Cloud KMS has no active key");
@@ -93,21 +117,46 @@ var GcpCloudKmsKeyProvider = class _GcpCloudKmsKeyProvider {
93
117
  async validateKeyGeneration(algorithm) {
94
118
  if (algorithm !== "EdDSA") throw new ArgumentError(`Google Cloud KMS generation settings do not support ${algorithm}`);
95
119
  }
96
- /** Explicit operator generation only; defaults to EdDSA and rejects other algorithms before creation. Never called by configuration opening. */
120
+ /**
121
+ * Explicit operator generation only; defaults to EdDSA and rejects other algorithms before creation.
122
+ * Pending generation polls the exact created version at most 20 times, 250ms apart,
123
+ * using the caller's transport timeouts, just as initial creation does.
124
+ * Returns a locally pending key without changing the active signer. Failures after
125
+ * creation retain the version reference and cause, without retrying or deleting it.
126
+ * Service/integrity failures are not caller-argument errors. Never called by configuration opening.
127
+ */
97
128
  async generateKey(algorithm) {
129
+ return this.generatePendingKey(algorithm);
130
+ }
131
+ async generatePendingKey(algorithm) {
98
132
  await this.validateKeyGeneration(algorithm ?? "EdDSA");
99
133
  if (!this.client.createCryptoKeyVersion) throw new ArgumentError("Google Cloud KMS client does not support version creation");
100
- const parent = this.version.slice(0, this.version.lastIndexOf("/cryptoKeyVersions/"));
134
+ const parent = this.version.includes("/cryptoKeyVersions/") ? this.version.slice(0, this.version.lastIndexOf("/cryptoKeyVersions/")) : this.version;
101
135
  const [raw] = await this.client.createCryptoKeyVersion({ parent, cryptoKeyVersion: { state: "ENABLED" } });
102
- const created = raw;
103
- if (!created?.name?.startsWith(parent + "/cryptoKeyVersions/") || created.algorithm !== "EC_SIGN_ED25519" && created.algorithm !== 40 || created.protectionLevel !== "SOFTWARE" && created.protectionLevel !== 1 || created.state !== "ENABLED" && created.state !== 1) {
104
- throw new ArgumentError("Google Cloud KMS created an unexpected key version");
136
+ let created = raw;
137
+ const name = created?.name;
138
+ if (typeof name !== "string" || !name.startsWith(parent + "/cryptoKeyVersions/") || !/^[1-9][0-9]*$/.test(name.slice((parent + "/cryptoKeyVersions/").length))) {
139
+ throw new ArgumentError(`Google Cloud KMS created an unexpected key version${typeof name === "string" && name ? `: ${name}` : ""}`);
140
+ }
141
+ try {
142
+ for (let poll = 0; ; poll++) {
143
+ if (created?.name !== name || !ed25519Software(created)) throw new ArgumentError("Google Cloud KMS created an unexpected key version");
144
+ if (created.state === "ENABLED" || created.state === 1) break;
145
+ if (created.state !== "PENDING_GENERATION" && created.state !== 5 || poll >= 20 || !this.client.getCryptoKeyVersion) {
146
+ throw new ArgumentError("Google Cloud KMS version is not ready for signing");
147
+ }
148
+ await new Promise((resolve) => setTimeout(resolve, 250));
149
+ const [metadata] = await this.client.getCryptoKeyVersion({ name });
150
+ created = metadata;
151
+ }
152
+ const pending = await _GcpCloudKmsKeyProvider.load(this.client, { activeKeyVersion: name, algorithm: "EC_SIGN_ED25519" });
153
+ const jwk = await pending.signingKey();
154
+ if (this.keys.has(jwk.kid)) throw new ArgumentError("Google Cloud KMS generated duplicate public key");
155
+ this.keys.set(jwk.kid, { version: name, jwk: Object.freeze(jwk), state: "pending" });
156
+ return jwk.kid;
157
+ } catch (cause) {
158
+ throw new Error(`Google Cloud KMS created version ${name}: ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
105
159
  }
106
- const pending = await _GcpCloudKmsKeyProvider.load(this.client, { activeKeyVersion: created.name, algorithm: "EC_SIGN_ED25519" });
107
- const jwk = await pending.signingKey();
108
- if (this.keys.has(jwk.kid)) throw new ArgumentError("Google Cloud KMS generated duplicate public key");
109
- this.keys.set(jwk.kid, { version: created.name, jwk: Object.freeze(jwk), state: "pending" });
110
- return jwk.kid;
111
160
  }
112
161
  async activate(kid) {
113
162
  if (this.activating) throw new ArgumentError("Google Cloud KMS activation is already in progress");
@@ -142,6 +191,33 @@ var GcpCloudKmsKeyProvider = class _GcpCloudKmsKeyProvider {
142
191
  return this.supersede(kid);
143
192
  }
144
193
  };
194
+ function validateVersion(version) {
195
+ if (typeof version !== "string" || !/^projects\/[A-Za-z0-9._:-]+\/locations\/[A-Za-z0-9_-]+\/keyRings\/[A-Za-z0-9_-]+\/cryptoKeys\/[A-Za-z0-9_-]+\/cryptoKeyVersions\/[1-9][0-9]*$/.test(version)) {
196
+ throw new ArgumentError("Google Cloud KMS requires an immutable CryptoKeyVersion name");
197
+ }
198
+ }
199
+ async function loadPublicKey(client, version) {
200
+ const [raw] = await client.getPublicKey({ name: version });
201
+ const response = raw;
202
+ if (response?.name !== version) throw new ArgumentError("Google Cloud KMS public key version mismatch");
203
+ if (response.algorithm !== "EC_SIGN_ED25519" && response.algorithm !== 40) throw new ArgumentError("Google Cloud KMS public key algorithm mismatch");
204
+ if (response.protectionLevel !== "SOFTWARE" && response.protectionLevel !== 1) throw new ArgumentError("Google Cloud KMS public key must use SOFTWARE protection");
205
+ if (typeof response.pem !== "string" || !checksumMatches(Buffer.from(response.pem), response.pemCrc32c)) {
206
+ throw new ArgumentError("Google Cloud KMS public PEM checksum mismatch");
207
+ }
208
+ let publicKey;
209
+ try {
210
+ if (!response.pem.startsWith("-----BEGIN PUBLIC KEY-----")) throw new Error();
211
+ const key = createPublicKey({ key: response.pem, format: "pem", type: "spki" });
212
+ if (key.asymmetricKeyType !== "ed25519") throw new Error();
213
+ const exported = key.export({ format: "jwk" });
214
+ publicKey = { kty: "OKP", crv: "Ed25519", x: exported.x, kid: "", alg: "EdDSA", use: "sig" };
215
+ } catch {
216
+ throw new ArgumentError("Google Cloud KMS returned invalid Ed25519 public SPKI");
217
+ }
218
+ publicKey.kid = await jwkThumbprint(publicKey);
219
+ return Object.freeze(publicKey);
220
+ }
145
221
  function checksumMatches(bytes, checksum) {
146
222
  const raw = checksum?.value;
147
223
  if (raw === void 0 || raw === null) return false;
@@ -158,6 +234,9 @@ function crc32c(bytes) {
158
234
  }
159
235
  return (crc ^ 4294967295) >>> 0;
160
236
  }
237
+ function ed25519Software(value) {
238
+ return !!value && (value.algorithm === "EC_SIGN_ED25519" || value.algorithm === 40) && (value.protectionLevel === "SOFTWARE" || value.protectionLevel === 1);
239
+ }
161
240
  export {
162
241
  GcpCloudKmsKeyProvider
163
242
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@dnsid-ai/key-gcp",
3
3
  "author": "Known Systems AI, Inc.",
4
- "version": "0.35.0",
4
+ "version": "0.36.0",
5
5
  "description": "DNSid Google Cloud KMS Ed25519 key provider integration.",
6
6
  "type": "module",
7
7
  "types": "./dist/index.d.ts",
@@ -40,7 +40,7 @@
40
40
  "prepack": "node ../../scripts/copy-notice.mjs"
41
41
  },
42
42
  "dependencies": {
43
- "@dnsid-ai/protocol": "^0.35.0",
43
+ "@dnsid-ai/protocol": "^0.36.0",
44
44
  "@google-cloud/kms": "^6.3.0"
45
45
  },
46
46
  "license": "Apache-2.0",