@dnsid-ai/sdk 0.29.0 → 0.31.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 +34 -0
- package/dist/index.cjs +171 -41
- package/dist/index.d.cts +29 -12
- package/dist/index.d.ts +29 -12
- package/dist/index.js +180 -48
- package/dist/node.cjs +33 -8
- package/dist/node.d.cts +2 -1
- package/dist/node.d.ts +2 -1
- package/dist/node.js +33 -8
- package/package.json +9 -9
package/README.md
CHANGED
|
@@ -68,6 +68,40 @@ If `ManagedKeyRotationActivationError` is raised, registry acceptance succeeded
|
|
|
68
68
|
but local key-state reconciliation is incomplete. Resume with the error's exact
|
|
69
69
|
rotation state; already-completed activation or supersession is not repeated.
|
|
70
70
|
|
|
71
|
+
Rotation requires `persistRotation`, `setApplicationSigningPaused`,
|
|
72
|
+
`setApplicationSigner`, and `verifyPublicationEvidence`. The verifier must return
|
|
73
|
+
fresh protocol-verified signed TXT/ku evidence; evict old publication evidence
|
|
74
|
+
before verification. Acceptance is checked against the saved bytes/hash, new
|
|
75
|
+
key, and indexed log reference before publication and continuity checks.
|
|
76
|
+
|
|
77
|
+
Supply stable `previousProviderReference` and `newProviderReference` values;
|
|
78
|
+
`targetKeyProvider` optionally selects another provider. Recovery uses
|
|
79
|
+
`resolveKeyProvider(reference)` to restore the saved providers, not current
|
|
80
|
+
deployment settings. It restores the saved new signer before unpausing.
|
|
81
|
+
Old rotation records without the new bindings must not be resumed automatically.
|
|
82
|
+
Recovery rejects unrelated active source keys. A removed previous key is accepted
|
|
83
|
+
only with saved acceptance and the exact saved target key already active; for
|
|
84
|
+
cross-provider recovery, the source must have no active key. Provider lookup
|
|
85
|
+
errors, including a listed but unavailable previous key, fail recovery.
|
|
86
|
+
Resolvers must reopen the saved provider state, not create replacement state.
|
|
87
|
+
|
|
88
|
+
Set `targetAlgorithm` (`EdDSA` or `ES256`) when changing algorithms; omission
|
|
89
|
+
preserves the previous algorithm. The target's `validateKeyGeneration(algorithm)`
|
|
90
|
+
checks settings/support before generation, and rotation checks the generated key
|
|
91
|
+
before registry preparation. Providers without this hook must already have an
|
|
92
|
+
active key using the selected algorithm. Local generation accepts either
|
|
93
|
+
algorithm; AWS generation must match the configured KMS algorithm.
|
|
94
|
+
|
|
95
|
+
Custom providers that can retire their active key may implement
|
|
96
|
+
`activeSigningKey()` to return `null` when none exists. Without this hook,
|
|
97
|
+
recovery treats an empty `listKeyIds()` as no active key and otherwise calls
|
|
98
|
+
`signingKey()`; lookup errors are never treated as completed supersession.
|
|
99
|
+
|
|
100
|
+
Cross-provider transfer requires a previous provider that supports superseding
|
|
101
|
+
its active key after transfer. Built-in local and AWS providers currently reject
|
|
102
|
+
this operation. AWS and CLI/in-memory provider state also needs application-owned
|
|
103
|
+
durable recovery. These limits are declared in `SDK_CONFORMANCE.knownDeviations`.
|
|
104
|
+
|
|
71
105
|
## Node.js convenience usage
|
|
72
106
|
|
|
73
107
|
```ts
|
package/dist/index.cjs
CHANGED
|
@@ -128,18 +128,35 @@ var ManagedKeyRotationActivationError = class extends import_protocol.Verificati
|
|
|
128
128
|
}
|
|
129
129
|
};
|
|
130
130
|
async function rotateManagedOperationalKey(options) {
|
|
131
|
+
assertCoordination(options);
|
|
132
|
+
const domain = (0, import_protocol.normalizeFQDN)(options.domain, true);
|
|
133
|
+
assertOperationReferences(options.previousProviderReference, options.newProviderReference, options.idempotencyKey);
|
|
134
|
+
const target = options.targetKeyProvider ?? options.keyProvider;
|
|
135
|
+
if (target === options.keyProvider !== (options.previousProviderReference === options.newProviderReference)) {
|
|
136
|
+
throw new import_protocol.ArgumentError("provider references must identify the selected previous and target providers");
|
|
137
|
+
}
|
|
131
138
|
const previousKey = await options.keyProvider.signingKey();
|
|
132
139
|
new import_protocol.JWKS([previousKey]).validateOperational();
|
|
133
140
|
const previousThumbprint = await (0, import_protocol.jwkThumbprint)(previousKey);
|
|
134
|
-
const
|
|
135
|
-
|
|
141
|
+
const algorithm = options.targetAlgorithm ?? previousKey.alg;
|
|
142
|
+
if (algorithm !== "EdDSA" && algorithm !== "ES256") throw new import_protocol.ArgumentError("unsupported target algorithm");
|
|
143
|
+
if (target.validateKeyGeneration) {
|
|
144
|
+
await target.validateKeyGeneration(algorithm);
|
|
145
|
+
} else {
|
|
146
|
+
const active = await target.signingKey();
|
|
147
|
+
new import_protocol.JWKS([active]).validateOperational();
|
|
148
|
+
if (active.alg !== algorithm) throw new import_protocol.ArgumentError("target provider must validate support for the selected algorithm");
|
|
149
|
+
}
|
|
150
|
+
const newKid = await target.generateKey(algorithm);
|
|
151
|
+
const newKey = await target.jwk(newKid);
|
|
136
152
|
new import_protocol.JWKS([newKey]).validateOperational();
|
|
153
|
+
if (newKey.alg !== algorithm) throw permanentRotationError("generated key does not use the selected algorithm");
|
|
137
154
|
if (await (0, import_protocol.jwkThumbprint)(newKey) === previousThumbprint) {
|
|
138
155
|
throw permanentRotationError("managed KEY_ROTATION requires a distinct pending operational key");
|
|
139
156
|
}
|
|
140
157
|
let raw;
|
|
141
158
|
try {
|
|
142
|
-
raw = await options.registryClient.prepareKeyRotation(
|
|
159
|
+
raw = await options.registryClient.prepareKeyRotation(domain, {
|
|
143
160
|
previousKeyId: previousThumbprint,
|
|
144
161
|
publicKey: newKey
|
|
145
162
|
}, options.idempotencyKey);
|
|
@@ -153,12 +170,12 @@ async function rotateManagedOperationalKey(options) {
|
|
|
153
170
|
if (raw.logReference !== options.logReference) {
|
|
154
171
|
throw permanentRotationError(`registry preparation returned unexpected log reference ${raw.logReference}`);
|
|
155
172
|
}
|
|
156
|
-
const context = { expectedFqdn:
|
|
173
|
+
const context = { expectedFqdn: domain, previousOperationalKey: previousKey };
|
|
157
174
|
let prepared = await (0, import_writer.parsePreparedC2spTlogEvent)(raw.entryBytes, raw.logReference, context);
|
|
158
175
|
if (prepared.envelope.type !== "KEY_ROTATION") {
|
|
159
176
|
throw permanentRotationError(`registry preparation returned ${String(prepared.envelope.type)} instead of KEY_ROTATION`);
|
|
160
177
|
}
|
|
161
|
-
if (prepared.envelope.fqdn !==
|
|
178
|
+
if (prepared.envelope.fqdn !== domain) {
|
|
162
179
|
throw permanentRotationError("registry KEY_ROTATION fqdn does not match the expected identity");
|
|
163
180
|
}
|
|
164
181
|
if (prepared.envelope.prev_thumb !== previousThumbprint) {
|
|
@@ -166,40 +183,61 @@ async function rotateManagedOperationalKey(options) {
|
|
|
166
183
|
}
|
|
167
184
|
await assertPreparedRotationKey(prepared.envelope.new_ku, prepared.envelope.new_thumb, newKey);
|
|
168
185
|
prepared = await (0, import_writer.signPreparedC2spTlogEvent)(prepared, "PreviousOperational", options.keyProvider, context);
|
|
169
|
-
prepared = await (0, import_writer.signPreparedC2spTlogEvent)(prepared, "NewOperational",
|
|
186
|
+
prepared = await (0, import_writer.signPreparedC2spTlogEvent)(prepared, "NewOperational", target, context);
|
|
170
187
|
const entryBytes = await (0, import_writer.c2spTlogEntryBytes)(prepared, context);
|
|
171
188
|
const rotation = {
|
|
172
|
-
domain
|
|
189
|
+
domain,
|
|
173
190
|
logReference: options.logReference,
|
|
174
191
|
previousKid: previousKey.kid,
|
|
175
192
|
newKid,
|
|
193
|
+
previousProviderReference: options.previousProviderReference,
|
|
194
|
+
newProviderReference: options.newProviderReference,
|
|
195
|
+
previousKey,
|
|
196
|
+
previousThumbprint,
|
|
197
|
+
newThumbprint: await (0, import_protocol.jwkThumbprint)(newKey),
|
|
198
|
+
entryHash: await sha256Hex(entryBytes),
|
|
176
199
|
entryBytes: entryBytes.slice(),
|
|
177
200
|
idempotencyKey: options.idempotencyKey,
|
|
178
201
|
activated: false,
|
|
179
202
|
applicationSigningPaused: true
|
|
180
203
|
};
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
204
|
+
try {
|
|
205
|
+
await options.persistRotation(rotation);
|
|
206
|
+
await options.setApplicationSigningPaused(true);
|
|
207
|
+
} catch (cause) {
|
|
208
|
+
throw new ManagedKeyRotationSubmissionError("managed rotation persistence or pause failed; resume this state", rotation, true, cause);
|
|
209
|
+
}
|
|
210
|
+
return submitManagedRotation(options, options.keyProvider, target, rotation);
|
|
184
211
|
}
|
|
185
212
|
async function resumeManagedOperationalKeyRotation(options) {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
213
|
+
assertCoordination(options);
|
|
214
|
+
if (typeof options.resolveKeyProvider !== "function") throw new import_protocol.ArgumentError("managed rotation recovery requires resolveKeyProvider");
|
|
215
|
+
const rotation = options.rotation;
|
|
216
|
+
try {
|
|
217
|
+
assertOperationReferences(rotation.previousProviderReference, rotation.newProviderReference, rotation.idempotencyKey);
|
|
218
|
+
const previous = await options.resolveKeyProvider(rotation.previousProviderReference);
|
|
219
|
+
const target = rotation.newProviderReference === rotation.previousProviderReference ? previous : await options.resolveKeyProvider(rotation.newProviderReference);
|
|
220
|
+
await validateSavedRotation(rotation, previous, target);
|
|
221
|
+
if (rotation.submission?.state === "rejected") {
|
|
222
|
+
throw new ManagedKeyRotationSubmissionError("cannot resume a rejected managed key rotation", rotation, false);
|
|
223
|
+
}
|
|
224
|
+
await options.setApplicationSigningPaused(true);
|
|
225
|
+
if (rotation.submission?.state === "accepted") {
|
|
226
|
+
await assertSubmissionMatchesRotation(rotation, rotation.submission);
|
|
227
|
+
return activateRotation(options, previous, target, rotation);
|
|
228
|
+
}
|
|
229
|
+
if (rotation.activated) throw permanentRotationError("activated rotation has no accepted submission");
|
|
230
|
+
await options.persistRotation(rotation);
|
|
231
|
+
return submitManagedRotation(options, previous, target, rotation);
|
|
232
|
+
} catch (cause) {
|
|
233
|
+
if (cause instanceof ManagedKeyRotationSubmissionError || cause instanceof ManagedKeyRotationActivationError) throw cause;
|
|
234
|
+
throw new ManagedKeyRotationActivationError("managed rotation recovery failed; signing must remain paused", rotation, cause);
|
|
195
235
|
}
|
|
196
|
-
await options.setApplicationSigningPaused(true);
|
|
197
|
-
return submitManagedRotation(options.keyProvider, options.registryClient, options.rotation, options.persistRotation, options.setApplicationSigningPaused);
|
|
198
236
|
}
|
|
199
|
-
async function submitManagedRotation(
|
|
237
|
+
async function submitManagedRotation(options, previous, target, rotation) {
|
|
200
238
|
let submission;
|
|
201
239
|
try {
|
|
202
|
-
submission = await registryClient.submitPreparedEvent(rotation.domain, rotation.entryBytes, rotation.idempotencyKey);
|
|
240
|
+
submission = await options.registryClient.submitPreparedEvent(rotation.domain, rotation.entryBytes, rotation.idempotencyKey);
|
|
203
241
|
} catch (cause) {
|
|
204
242
|
const retryWithSameBytes2 = structuredRetryWithSameBytes(cause);
|
|
205
243
|
throw new ManagedKeyRotationSubmissionError(
|
|
@@ -209,9 +247,13 @@ async function submitManagedRotation(keyProvider, registryClient, rotation, pers
|
|
|
209
247
|
cause
|
|
210
248
|
);
|
|
211
249
|
}
|
|
212
|
-
await assertSubmissionMatchesRotation(rotation, submission);
|
|
213
250
|
const next = { ...rotation, submission };
|
|
214
|
-
|
|
251
|
+
try {
|
|
252
|
+
await assertSubmissionMatchesRotation(rotation, submission);
|
|
253
|
+
await options.persistRotation(next);
|
|
254
|
+
} catch (cause) {
|
|
255
|
+
throw new ManagedKeyRotationSubmissionError("managed rotation acceptance binding or persistence failed; resume this state", next, true, cause);
|
|
256
|
+
}
|
|
215
257
|
if (submission.state === "rejected") {
|
|
216
258
|
throw new ManagedKeyRotationSubmissionError(
|
|
217
259
|
`registry rejected the prepared KEY_ROTATION: ${submission.errorCode ?? "unknown error"}`,
|
|
@@ -220,15 +262,24 @@ async function submitManagedRotation(keyProvider, registryClient, rotation, pers
|
|
|
220
262
|
);
|
|
221
263
|
}
|
|
222
264
|
if (submission.state !== "accepted") return next;
|
|
223
|
-
|
|
265
|
+
return activateRotation(options, previous, target, next);
|
|
266
|
+
}
|
|
267
|
+
async function activateRotation(options, previous, target, rotation) {
|
|
268
|
+
let recoverable = rotation;
|
|
224
269
|
try {
|
|
225
|
-
await
|
|
226
|
-
|
|
227
|
-
await
|
|
270
|
+
await verifyAcceptedPublication(options, rotation);
|
|
271
|
+
if (!rotation.activated) await reconcileAcceptedRotation(previous, target, rotation);
|
|
272
|
+
if (previous !== target && await activeKey(previous, await previous.listKeyIds())) {
|
|
273
|
+
throw permanentRotationError("superseded source provider still has an active signing key");
|
|
274
|
+
}
|
|
275
|
+
const activated = { ...rotation, activated: true, applicationSigningPaused: true };
|
|
276
|
+
await options.persistRotation(activated);
|
|
228
277
|
recoverable = activated;
|
|
229
|
-
await
|
|
278
|
+
await assertKeyBinding(await target.signingKey(), rotation.newKid, rotation.newThumbprint);
|
|
279
|
+
await options.setApplicationSigner(target, rotation.newKid);
|
|
280
|
+
await options.setApplicationSigningPaused(false);
|
|
230
281
|
const completed = { ...activated, applicationSigningPaused: false };
|
|
231
|
-
await persistRotation(completed);
|
|
282
|
+
await options.persistRotation(completed);
|
|
232
283
|
return completed;
|
|
233
284
|
} catch (cause) {
|
|
234
285
|
throw new ManagedKeyRotationActivationError(
|
|
@@ -238,15 +289,84 @@ async function submitManagedRotation(keyProvider, registryClient, rotation, pers
|
|
|
238
289
|
);
|
|
239
290
|
}
|
|
240
291
|
}
|
|
241
|
-
async function reconcileAcceptedRotation(
|
|
242
|
-
const
|
|
243
|
-
if (
|
|
244
|
-
await
|
|
245
|
-
|
|
246
|
-
|
|
292
|
+
async function reconcileAcceptedRotation(previous, target, rotation) {
|
|
293
|
+
const active = await target.signingKey();
|
|
294
|
+
if (active.kid !== rotation.newKid) {
|
|
295
|
+
if (previous === target) await assertKeyBinding(active, rotation.previousKid, rotation.previousThumbprint);
|
|
296
|
+
await target.activate(rotation.newKid);
|
|
297
|
+
}
|
|
298
|
+
await assertKeyBinding(await target.signingKey(), rotation.newKid, rotation.newThumbprint);
|
|
299
|
+
if ((await previous.listKeyIds()).includes(rotation.previousKid)) await previous.supersede(rotation.previousKid);
|
|
300
|
+
}
|
|
301
|
+
function assertCoordination(options) {
|
|
302
|
+
for (const name of ["persistRotation", "setApplicationSigningPaused", "setApplicationSigner", "verifyPublicationEvidence"]) {
|
|
303
|
+
if (typeof options[name] !== "function") throw new import_protocol.ArgumentError(`managed rotation requires ${name}`);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
function assertOperationReferences(previous, next, idempotencyKey) {
|
|
307
|
+
if (!previous?.trim() || !next?.trim() || !idempotencyKey || idempotencyKey.trim() !== idempotencyKey || idempotencyKey.length > 200) {
|
|
308
|
+
throw permanentRotationError("managed rotation requires provider references and a valid idempotency key");
|
|
247
309
|
}
|
|
248
|
-
|
|
249
|
-
|
|
310
|
+
}
|
|
311
|
+
async function assertKeyBinding(key, kid, thumbprint) {
|
|
312
|
+
new import_protocol.JWKS([key]).validateOperational();
|
|
313
|
+
if (key.kid !== kid || await (0, import_protocol.jwkThumbprint)(key) !== thumbprint) {
|
|
314
|
+
throw permanentRotationError("saved managed rotation key binding mismatch");
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
async function activeKey(provider, listedIds) {
|
|
318
|
+
if (provider.activeSigningKey) return provider.activeSigningKey();
|
|
319
|
+
return listedIds.length ? provider.signingKey() : null;
|
|
320
|
+
}
|
|
321
|
+
async function validateSavedRotation(rotation, previous, target) {
|
|
322
|
+
assertOperationReferences(rotation.previousProviderReference, rotation.newProviderReference, rotation.idempotencyKey);
|
|
323
|
+
if (typeof rotation.activated !== "boolean" || typeof rotation.applicationSigningPaused !== "boolean" || (0, import_protocol.normalizeFQDN)(rotation.domain, true) !== rotation.domain || await sha256Hex(rotation.entryBytes) !== rotation.entryHash) {
|
|
324
|
+
throw permanentRotationError("saved managed rotation domain or entry hash mismatch");
|
|
325
|
+
}
|
|
326
|
+
await assertKeyBinding(rotation.previousKey, rotation.previousKid, rotation.previousThumbprint);
|
|
327
|
+
const previousIds = await previous.listKeyIds();
|
|
328
|
+
const sourceActive = await activeKey(previous, previousIds);
|
|
329
|
+
if (previousIds.includes(rotation.previousKid)) {
|
|
330
|
+
await assertKeyBinding(await previous.jwk(rotation.previousKid), rotation.previousKid, rotation.previousThumbprint);
|
|
331
|
+
if (sourceActive) {
|
|
332
|
+
const targetIsActive = previous === target && rotation.submission?.state === "accepted" && sourceActive.kid === rotation.newKid;
|
|
333
|
+
await assertKeyBinding(
|
|
334
|
+
sourceActive,
|
|
335
|
+
targetIsActive ? rotation.newKid : rotation.previousKid,
|
|
336
|
+
targetIsActive ? rotation.newThumbprint : rotation.previousThumbprint
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
} else {
|
|
340
|
+
if (rotation.submission?.state !== "accepted") throw permanentRotationError("saved previous key is unavailable without saved acceptance");
|
|
341
|
+
await assertSubmissionMatchesRotation(rotation, rotation.submission);
|
|
342
|
+
await assertKeyBinding(await target.signingKey(), rotation.newKid, rotation.newThumbprint);
|
|
343
|
+
if (previous !== target && sourceActive) throw permanentRotationError("superseded source provider still has an active signing key");
|
|
344
|
+
}
|
|
345
|
+
const next = await target.jwk(rotation.newKid);
|
|
346
|
+
await assertKeyBinding(next, rotation.newKid, rotation.newThumbprint);
|
|
347
|
+
const context = { expectedFqdn: rotation.domain, previousOperationalKey: rotation.previousKey };
|
|
348
|
+
const prepared = await (0, import_writer.parsePreparedC2spTlogEvent)(rotation.entryBytes, rotation.logReference, context);
|
|
349
|
+
if (prepared.envelope.type !== "KEY_ROTATION" || prepared.envelope.fqdn !== rotation.domain || prepared.envelope.prev_thumb !== rotation.previousThumbprint) {
|
|
350
|
+
throw permanentRotationError("saved managed rotation event binding mismatch");
|
|
351
|
+
}
|
|
352
|
+
await assertPreparedRotationKey(prepared.envelope.new_ku, prepared.envelope.new_thumb, next);
|
|
353
|
+
await (0, import_writer.c2spTlogEntryBytes)(prepared, context);
|
|
354
|
+
if (rotation.submission) await assertSubmissionMatchesRotation(rotation, rotation.submission);
|
|
355
|
+
}
|
|
356
|
+
async function verifyAcceptedPublication(options, rotation) {
|
|
357
|
+
const verified = await options.verifyPublicationEvidence(rotation.domain);
|
|
358
|
+
if (verified.domain !== rotation.domain || verified.record.lr !== rotation.logReference) {
|
|
359
|
+
throw permanentRotationError("current signed publication does not match managed rotation");
|
|
360
|
+
}
|
|
361
|
+
await assertKeyBinding(verified.jwks.currentOperationalSigningKey(), rotation.newKid, rotation.newThumbprint);
|
|
362
|
+
const prepared = await (0, import_writer.parsePreparedC2spTlogEvent)(rotation.entryBytes, rotation.logReference, {
|
|
363
|
+
expectedFqdn: rotation.domain,
|
|
364
|
+
previousOperationalKey: rotation.previousKey
|
|
365
|
+
});
|
|
366
|
+
const signatures = prepared.envelope.sigs;
|
|
367
|
+
const event = await verified.logReader.readEvent(rotation.submission.logRef);
|
|
368
|
+
if (event.type !== "KEY_ROTATION" || event.domain !== rotation.domain || event.previousOperationalThumbprint !== rotation.previousThumbprint || event.newOperationalKid !== rotation.newKid || event.newOperationalThumbprint !== rotation.newThumbprint || event.signingKid !== signatures.prev_op.kid || event.sig !== signatures.prev_op.sig || event.newOperationalProof !== signatures.new_op.sig || event.timestamp.getTime() !== prepared.envelope.ts * 1e3) {
|
|
369
|
+
throw permanentRotationError("accepted managed rotation continuity mismatch");
|
|
250
370
|
}
|
|
251
371
|
}
|
|
252
372
|
async function assertPreparedRotationKey(value, thumbprint, expected) {
|
|
@@ -263,12 +383,18 @@ async function assertSubmissionMatchesRotation(rotation, submission) {
|
|
|
263
383
|
if (rotation.submission?.entryHash && submission.entryHash && rotation.submission.entryHash !== submission.entryHash) {
|
|
264
384
|
throw permanentRotationError("registry reconciled a different entry hash for this managed key rotation");
|
|
265
385
|
}
|
|
386
|
+
if (submission.entryHash !== void 0 && submission.entryHash !== rotation.entryHash) {
|
|
387
|
+
throw permanentRotationError("submission entry hash does not match the exact managed key rotation bytes");
|
|
388
|
+
}
|
|
266
389
|
if (submission.state !== "accepted") return;
|
|
267
390
|
const expectedHash = await sha256Hex(rotation.entryBytes);
|
|
268
391
|
if (submission.entryHash !== expectedHash) {
|
|
269
392
|
throw permanentRotationError("accepted submission entry hash does not match the exact managed key rotation bytes");
|
|
270
393
|
}
|
|
271
|
-
if (submission.
|
|
394
|
+
if (submission.logRef !== `${rotation.logReference}@${submission.index}` || !Number.isSafeInteger(submission.index) || submission.index < 0) {
|
|
395
|
+
throw permanentRotationError("accepted submission log reference does not match managed rotation");
|
|
396
|
+
}
|
|
397
|
+
if (submission.keyId !== rotation.newKid) {
|
|
272
398
|
throw permanentRotationError("accepted submission key_id does not match the generated pending key");
|
|
273
399
|
}
|
|
274
400
|
}
|
|
@@ -579,7 +705,11 @@ var SDK_CONFORMANCE = Object.freeze({
|
|
|
579
705
|
logBindings: Object.freeze({
|
|
580
706
|
"c2sp-tlog": c2spTlogConformance
|
|
581
707
|
}),
|
|
582
|
-
knownDeviations: Object.freeze([
|
|
708
|
+
knownDeviations: Object.freeze([
|
|
709
|
+
"Managed cross-provider rotation requires providers that can supersede the previous active key after transfer; built-in LocalKeyProvider and AwsKmsKeyProvider reject active-key supersession.",
|
|
710
|
+
"AWS KMS and CLI/in-memory key providers do not durably persist rotation key state; restart recovery requires application-managed provider state or a file-backed local key store.",
|
|
711
|
+
"Account onboarding validates bindings and reports proof readiness, but does not implement named managed creation or establish permanent registration replay claims."
|
|
712
|
+
])
|
|
583
713
|
});
|
|
584
714
|
|
|
585
715
|
// src/index.ts
|
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { VerificationError, KeyProvider,
|
|
1
|
+
import { VerificationError, DnsIdJWK, KeyProvider, VerifiedDomain, IdentityManagerDependencies, DnsidConfig, IdentityManager } from '@dnsid-ai/protocol';
|
|
2
2
|
export * from '@dnsid-ai/protocol';
|
|
3
3
|
export { ArgumentError, DNSID_VERSION, DNSSECMode, DNSSECState, DnsIdTxtRecord, DomainLog, DomainSnapshot, IdentityManager, InMemoryIdentityCache, JWKS, JWKS_MAX_RESPONSE_BYTES, LogRegistry, NoopLogReader, ParseError, SIGNING_ALGS, STATUS_MAX_RESPONSE_BYTES, ValidationError, VerificationCode, VerificationError, VerifiedDomain, activeStatusDocument, fromBase64Url, isDomainName, isTransientVerificationError, jwkSignatureAlg, jwkThumbprint, normalizeFQDN, parseKaDuration, parseKeyId, retryTransientVerification, toArrayBuffer, toBase64Url, validateAgentStatus, verifyWithKey } from '@dnsid-ai/protocol';
|
|
4
4
|
import * as jose from '@dnsid-ai/jose';
|
|
@@ -19,24 +19,42 @@ interface ManagedKeyRotationRegistry {
|
|
|
19
19
|
prepareKeyRotation(domain: string, request: KeyRotationPreparationRequest, idempotencyKey: string): Promise<PreparedRegistryEvent>;
|
|
20
20
|
submitPreparedEvent(domain: string, entryBytes: Uint8Array, idempotencyKey: string): Promise<SubmissionResult>;
|
|
21
21
|
}
|
|
22
|
-
interface
|
|
22
|
+
interface ManagedKeyRotationCoordination {
|
|
23
|
+
/** Durably persists exact bytes and every subsequent reconciliation state. */
|
|
24
|
+
persistRotation: (rotation: ManagedKeyRotationResult) => Promise<void>;
|
|
25
|
+
/** Blocks new application signing and drains in-flight request/directory signing before resolving;
|
|
26
|
+
* resumes signing only after durable activation. */
|
|
27
|
+
setApplicationSigningPaused: (paused: boolean) => Promise<void>;
|
|
28
|
+
/** Restores the application signer before signing is resumed. */
|
|
29
|
+
setApplicationSigner: (provider: KeyProvider, kid: string) => Promise<void>;
|
|
30
|
+
/** Must return fresh, protocol-verified signed TXT/ku evidence, not cached pre-rotation evidence. */
|
|
31
|
+
verifyPublicationEvidence: (domain: string) => Promise<VerifiedDomain>;
|
|
32
|
+
}
|
|
33
|
+
interface RotateManagedOperationalKeyOptions extends ManagedKeyRotationCoordination {
|
|
23
34
|
domain: string;
|
|
24
35
|
/** Exact unindexed c2sp-tlog reference configured for this identity. */
|
|
25
36
|
logReference: string;
|
|
26
37
|
keyProvider: KeyProvider;
|
|
38
|
+
previousProviderReference: string;
|
|
39
|
+
/** Omit to rotate within the previous provider. */
|
|
40
|
+
targetKeyProvider?: KeyProvider;
|
|
41
|
+
/** Defaults to the previous key's algorithm. Required when changing algorithms. */
|
|
42
|
+
targetAlgorithm?: 'EdDSA' | 'ES256';
|
|
43
|
+
newProviderReference: string;
|
|
27
44
|
registryClient: ManagedKeyRotationRegistry;
|
|
28
45
|
idempotencyKey: string;
|
|
29
|
-
/** Durably persists exact bytes and every subsequent reconciliation state. */
|
|
30
|
-
persistRotation: (rotation: ManagedKeyRotationResult) => Promise<void>;
|
|
31
|
-
/** Blocks new application signing and drains in-flight request/directory signing before resolving;
|
|
32
|
-
* resumes signing only after durable activation. */
|
|
33
|
-
setApplicationSigningPaused: (paused: boolean) => Promise<void>;
|
|
34
46
|
}
|
|
35
47
|
interface ManagedKeyRotationResult {
|
|
36
48
|
readonly domain: string;
|
|
37
49
|
readonly logReference: string;
|
|
38
50
|
readonly previousKid: string;
|
|
39
51
|
readonly newKid: string;
|
|
52
|
+
readonly previousProviderReference: string;
|
|
53
|
+
readonly newProviderReference: string;
|
|
54
|
+
readonly previousKey: DnsIdJWK;
|
|
55
|
+
readonly previousThumbprint: string;
|
|
56
|
+
readonly newThumbprint: string;
|
|
57
|
+
readonly entryHash: string;
|
|
40
58
|
/**
|
|
41
59
|
* Persist these exact bytes with idempotencyKey while activated is false.
|
|
42
60
|
* Callers must pause new application signing until this rotation is accepted.
|
|
@@ -47,12 +65,11 @@ interface ManagedKeyRotationResult {
|
|
|
47
65
|
readonly activated: boolean;
|
|
48
66
|
readonly applicationSigningPaused: boolean;
|
|
49
67
|
}
|
|
50
|
-
interface ResumeManagedOperationalKeyRotationOptions {
|
|
51
|
-
|
|
68
|
+
interface ResumeManagedOperationalKeyRotationOptions extends ManagedKeyRotationCoordination {
|
|
69
|
+
/** Resolves durable references, never the current deployment's selected provider. */
|
|
70
|
+
resolveKeyProvider: (reference: string) => Promise<KeyProvider>;
|
|
52
71
|
registryClient: Pick<ManagedKeyRotationRegistry, 'submitPreparedEvent'>;
|
|
53
72
|
rotation: ManagedKeyRotationResult;
|
|
54
|
-
persistRotation: (rotation: ManagedKeyRotationResult) => Promise<void>;
|
|
55
|
-
setApplicationSigningPaused: (paused: boolean) => Promise<void>;
|
|
56
73
|
}
|
|
57
74
|
/** Submission failed after the completed entry bytes were fixed. */
|
|
58
75
|
declare class ManagedKeyRotationSubmissionError extends VerificationError {
|
|
@@ -208,4 +225,4 @@ declare function createIdentityManager(config: DnsidConfig, deps: CreateIdentity
|
|
|
208
225
|
*/
|
|
209
226
|
declare function createIdentityVerifier(config: Omit<DnsidConfig, 'identity'>, deps: Omit<CreateIdentityManagerDependencies, 'keyProvider' | 'entityKeyProvider'>): IdentityManager;
|
|
210
227
|
|
|
211
|
-
export { type CreateIdentityManagerDependencies, type IssueManagedIdentityOptions, ManagedIssuanceActivationError, type ManagedIssuanceCoordination, type ManagedIssuanceRegistry, type ManagedIssuanceState, type ManagedIssuanceSubmission, ManagedIssuanceSubmissionError, ManagedKeyRotationActivationError, type ManagedKeyRotationRegistry, type ManagedKeyRotationResult, ManagedKeyRotationSubmissionError, type ResumeManagedIssuanceOptions, type ResumeManagedOperationalKeyRotationOptions, type RotateManagedOperationalKeyOptions, type SDKConformance, SDK_CONFORMANCE, createIdentityManager, createIdentityVerifier, issueManagedIdentity, resumeManagedIssuance, resumeManagedOperationalKeyRotation, rotateManagedOperationalKey };
|
|
228
|
+
export { type CreateIdentityManagerDependencies, type IssueManagedIdentityOptions, ManagedIssuanceActivationError, type ManagedIssuanceCoordination, type ManagedIssuanceRegistry, type ManagedIssuanceState, type ManagedIssuanceSubmission, ManagedIssuanceSubmissionError, ManagedKeyRotationActivationError, type ManagedKeyRotationCoordination, type ManagedKeyRotationRegistry, type ManagedKeyRotationResult, ManagedKeyRotationSubmissionError, type ResumeManagedIssuanceOptions, type ResumeManagedOperationalKeyRotationOptions, type RotateManagedOperationalKeyOptions, type SDKConformance, SDK_CONFORMANCE, createIdentityManager, createIdentityVerifier, issueManagedIdentity, resumeManagedIssuance, resumeManagedOperationalKeyRotation, rotateManagedOperationalKey };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { VerificationError, KeyProvider,
|
|
1
|
+
import { VerificationError, DnsIdJWK, KeyProvider, VerifiedDomain, IdentityManagerDependencies, DnsidConfig, IdentityManager } from '@dnsid-ai/protocol';
|
|
2
2
|
export * from '@dnsid-ai/protocol';
|
|
3
3
|
export { ArgumentError, DNSID_VERSION, DNSSECMode, DNSSECState, DnsIdTxtRecord, DomainLog, DomainSnapshot, IdentityManager, InMemoryIdentityCache, JWKS, JWKS_MAX_RESPONSE_BYTES, LogRegistry, NoopLogReader, ParseError, SIGNING_ALGS, STATUS_MAX_RESPONSE_BYTES, ValidationError, VerificationCode, VerificationError, VerifiedDomain, activeStatusDocument, fromBase64Url, isDomainName, isTransientVerificationError, jwkSignatureAlg, jwkThumbprint, normalizeFQDN, parseKaDuration, parseKeyId, retryTransientVerification, toArrayBuffer, toBase64Url, validateAgentStatus, verifyWithKey } from '@dnsid-ai/protocol';
|
|
4
4
|
import * as jose from '@dnsid-ai/jose';
|
|
@@ -19,24 +19,42 @@ interface ManagedKeyRotationRegistry {
|
|
|
19
19
|
prepareKeyRotation(domain: string, request: KeyRotationPreparationRequest, idempotencyKey: string): Promise<PreparedRegistryEvent>;
|
|
20
20
|
submitPreparedEvent(domain: string, entryBytes: Uint8Array, idempotencyKey: string): Promise<SubmissionResult>;
|
|
21
21
|
}
|
|
22
|
-
interface
|
|
22
|
+
interface ManagedKeyRotationCoordination {
|
|
23
|
+
/** Durably persists exact bytes and every subsequent reconciliation state. */
|
|
24
|
+
persistRotation: (rotation: ManagedKeyRotationResult) => Promise<void>;
|
|
25
|
+
/** Blocks new application signing and drains in-flight request/directory signing before resolving;
|
|
26
|
+
* resumes signing only after durable activation. */
|
|
27
|
+
setApplicationSigningPaused: (paused: boolean) => Promise<void>;
|
|
28
|
+
/** Restores the application signer before signing is resumed. */
|
|
29
|
+
setApplicationSigner: (provider: KeyProvider, kid: string) => Promise<void>;
|
|
30
|
+
/** Must return fresh, protocol-verified signed TXT/ku evidence, not cached pre-rotation evidence. */
|
|
31
|
+
verifyPublicationEvidence: (domain: string) => Promise<VerifiedDomain>;
|
|
32
|
+
}
|
|
33
|
+
interface RotateManagedOperationalKeyOptions extends ManagedKeyRotationCoordination {
|
|
23
34
|
domain: string;
|
|
24
35
|
/** Exact unindexed c2sp-tlog reference configured for this identity. */
|
|
25
36
|
logReference: string;
|
|
26
37
|
keyProvider: KeyProvider;
|
|
38
|
+
previousProviderReference: string;
|
|
39
|
+
/** Omit to rotate within the previous provider. */
|
|
40
|
+
targetKeyProvider?: KeyProvider;
|
|
41
|
+
/** Defaults to the previous key's algorithm. Required when changing algorithms. */
|
|
42
|
+
targetAlgorithm?: 'EdDSA' | 'ES256';
|
|
43
|
+
newProviderReference: string;
|
|
27
44
|
registryClient: ManagedKeyRotationRegistry;
|
|
28
45
|
idempotencyKey: string;
|
|
29
|
-
/** Durably persists exact bytes and every subsequent reconciliation state. */
|
|
30
|
-
persistRotation: (rotation: ManagedKeyRotationResult) => Promise<void>;
|
|
31
|
-
/** Blocks new application signing and drains in-flight request/directory signing before resolving;
|
|
32
|
-
* resumes signing only after durable activation. */
|
|
33
|
-
setApplicationSigningPaused: (paused: boolean) => Promise<void>;
|
|
34
46
|
}
|
|
35
47
|
interface ManagedKeyRotationResult {
|
|
36
48
|
readonly domain: string;
|
|
37
49
|
readonly logReference: string;
|
|
38
50
|
readonly previousKid: string;
|
|
39
51
|
readonly newKid: string;
|
|
52
|
+
readonly previousProviderReference: string;
|
|
53
|
+
readonly newProviderReference: string;
|
|
54
|
+
readonly previousKey: DnsIdJWK;
|
|
55
|
+
readonly previousThumbprint: string;
|
|
56
|
+
readonly newThumbprint: string;
|
|
57
|
+
readonly entryHash: string;
|
|
40
58
|
/**
|
|
41
59
|
* Persist these exact bytes with idempotencyKey while activated is false.
|
|
42
60
|
* Callers must pause new application signing until this rotation is accepted.
|
|
@@ -47,12 +65,11 @@ interface ManagedKeyRotationResult {
|
|
|
47
65
|
readonly activated: boolean;
|
|
48
66
|
readonly applicationSigningPaused: boolean;
|
|
49
67
|
}
|
|
50
|
-
interface ResumeManagedOperationalKeyRotationOptions {
|
|
51
|
-
|
|
68
|
+
interface ResumeManagedOperationalKeyRotationOptions extends ManagedKeyRotationCoordination {
|
|
69
|
+
/** Resolves durable references, never the current deployment's selected provider. */
|
|
70
|
+
resolveKeyProvider: (reference: string) => Promise<KeyProvider>;
|
|
52
71
|
registryClient: Pick<ManagedKeyRotationRegistry, 'submitPreparedEvent'>;
|
|
53
72
|
rotation: ManagedKeyRotationResult;
|
|
54
|
-
persistRotation: (rotation: ManagedKeyRotationResult) => Promise<void>;
|
|
55
|
-
setApplicationSigningPaused: (paused: boolean) => Promise<void>;
|
|
56
73
|
}
|
|
57
74
|
/** Submission failed after the completed entry bytes were fixed. */
|
|
58
75
|
declare class ManagedKeyRotationSubmissionError extends VerificationError {
|
|
@@ -208,4 +225,4 @@ declare function createIdentityManager(config: DnsidConfig, deps: CreateIdentity
|
|
|
208
225
|
*/
|
|
209
226
|
declare function createIdentityVerifier(config: Omit<DnsidConfig, 'identity'>, deps: Omit<CreateIdentityManagerDependencies, 'keyProvider' | 'entityKeyProvider'>): IdentityManager;
|
|
210
227
|
|
|
211
|
-
export { type CreateIdentityManagerDependencies, type IssueManagedIdentityOptions, ManagedIssuanceActivationError, type ManagedIssuanceCoordination, type ManagedIssuanceRegistry, type ManagedIssuanceState, type ManagedIssuanceSubmission, ManagedIssuanceSubmissionError, ManagedKeyRotationActivationError, type ManagedKeyRotationRegistry, type ManagedKeyRotationResult, ManagedKeyRotationSubmissionError, type ResumeManagedIssuanceOptions, type ResumeManagedOperationalKeyRotationOptions, type RotateManagedOperationalKeyOptions, type SDKConformance, SDK_CONFORMANCE, createIdentityManager, createIdentityVerifier, issueManagedIdentity, resumeManagedIssuance, resumeManagedOperationalKeyRotation, rotateManagedOperationalKey };
|
|
228
|
+
export { type CreateIdentityManagerDependencies, type IssueManagedIdentityOptions, ManagedIssuanceActivationError, type ManagedIssuanceCoordination, type ManagedIssuanceRegistry, type ManagedIssuanceState, type ManagedIssuanceSubmission, ManagedIssuanceSubmissionError, ManagedKeyRotationActivationError, type ManagedKeyRotationCoordination, type ManagedKeyRotationRegistry, type ManagedKeyRotationResult, ManagedKeyRotationSubmissionError, type ResumeManagedIssuanceOptions, type ResumeManagedOperationalKeyRotationOptions, type RotateManagedOperationalKeyOptions, type SDKConformance, SDK_CONFORMANCE, createIdentityManager, createIdentityVerifier, issueManagedIdentity, resumeManagedIssuance, resumeManagedOperationalKeyRotation, rotateManagedOperationalKey };
|
package/dist/index.js
CHANGED
|
@@ -25,8 +25,8 @@ import {
|
|
|
25
25
|
ValidationError,
|
|
26
26
|
VerificationError as VerificationError3,
|
|
27
27
|
VerificationCode as VerificationCode3,
|
|
28
|
-
ArgumentError,
|
|
29
|
-
normalizeFQDN as
|
|
28
|
+
ArgumentError as ArgumentError2,
|
|
29
|
+
normalizeFQDN as normalizeFQDN3,
|
|
30
30
|
isDomainName,
|
|
31
31
|
parseKeyId,
|
|
32
32
|
toBase64Url,
|
|
@@ -55,6 +55,8 @@ import {
|
|
|
55
55
|
|
|
56
56
|
// src/managed-key-rotation.ts
|
|
57
57
|
import {
|
|
58
|
+
ArgumentError,
|
|
59
|
+
normalizeFQDN,
|
|
58
60
|
JWKS,
|
|
59
61
|
VerificationCode,
|
|
60
62
|
VerificationError,
|
|
@@ -84,18 +86,35 @@ var ManagedKeyRotationActivationError = class extends VerificationError {
|
|
|
84
86
|
}
|
|
85
87
|
};
|
|
86
88
|
async function rotateManagedOperationalKey(options) {
|
|
89
|
+
assertCoordination(options);
|
|
90
|
+
const domain = normalizeFQDN(options.domain, true);
|
|
91
|
+
assertOperationReferences(options.previousProviderReference, options.newProviderReference, options.idempotencyKey);
|
|
92
|
+
const target = options.targetKeyProvider ?? options.keyProvider;
|
|
93
|
+
if (target === options.keyProvider !== (options.previousProviderReference === options.newProviderReference)) {
|
|
94
|
+
throw new ArgumentError("provider references must identify the selected previous and target providers");
|
|
95
|
+
}
|
|
87
96
|
const previousKey = await options.keyProvider.signingKey();
|
|
88
97
|
new JWKS([previousKey]).validateOperational();
|
|
89
98
|
const previousThumbprint = await jwkThumbprint(previousKey);
|
|
90
|
-
const
|
|
91
|
-
|
|
99
|
+
const algorithm = options.targetAlgorithm ?? previousKey.alg;
|
|
100
|
+
if (algorithm !== "EdDSA" && algorithm !== "ES256") throw new ArgumentError("unsupported target algorithm");
|
|
101
|
+
if (target.validateKeyGeneration) {
|
|
102
|
+
await target.validateKeyGeneration(algorithm);
|
|
103
|
+
} else {
|
|
104
|
+
const active = await target.signingKey();
|
|
105
|
+
new JWKS([active]).validateOperational();
|
|
106
|
+
if (active.alg !== algorithm) throw new ArgumentError("target provider must validate support for the selected algorithm");
|
|
107
|
+
}
|
|
108
|
+
const newKid = await target.generateKey(algorithm);
|
|
109
|
+
const newKey = await target.jwk(newKid);
|
|
92
110
|
new JWKS([newKey]).validateOperational();
|
|
111
|
+
if (newKey.alg !== algorithm) throw permanentRotationError("generated key does not use the selected algorithm");
|
|
93
112
|
if (await jwkThumbprint(newKey) === previousThumbprint) {
|
|
94
113
|
throw permanentRotationError("managed KEY_ROTATION requires a distinct pending operational key");
|
|
95
114
|
}
|
|
96
115
|
let raw;
|
|
97
116
|
try {
|
|
98
|
-
raw = await options.registryClient.prepareKeyRotation(
|
|
117
|
+
raw = await options.registryClient.prepareKeyRotation(domain, {
|
|
99
118
|
previousKeyId: previousThumbprint,
|
|
100
119
|
publicKey: newKey
|
|
101
120
|
}, options.idempotencyKey);
|
|
@@ -109,12 +128,12 @@ async function rotateManagedOperationalKey(options) {
|
|
|
109
128
|
if (raw.logReference !== options.logReference) {
|
|
110
129
|
throw permanentRotationError(`registry preparation returned unexpected log reference ${raw.logReference}`);
|
|
111
130
|
}
|
|
112
|
-
const context = { expectedFqdn:
|
|
131
|
+
const context = { expectedFqdn: domain, previousOperationalKey: previousKey };
|
|
113
132
|
let prepared = await parsePreparedC2spTlogEvent(raw.entryBytes, raw.logReference, context);
|
|
114
133
|
if (prepared.envelope.type !== "KEY_ROTATION") {
|
|
115
134
|
throw permanentRotationError(`registry preparation returned ${String(prepared.envelope.type)} instead of KEY_ROTATION`);
|
|
116
135
|
}
|
|
117
|
-
if (prepared.envelope.fqdn !==
|
|
136
|
+
if (prepared.envelope.fqdn !== domain) {
|
|
118
137
|
throw permanentRotationError("registry KEY_ROTATION fqdn does not match the expected identity");
|
|
119
138
|
}
|
|
120
139
|
if (prepared.envelope.prev_thumb !== previousThumbprint) {
|
|
@@ -122,40 +141,61 @@ async function rotateManagedOperationalKey(options) {
|
|
|
122
141
|
}
|
|
123
142
|
await assertPreparedRotationKey(prepared.envelope.new_ku, prepared.envelope.new_thumb, newKey);
|
|
124
143
|
prepared = await signPreparedC2spTlogEvent(prepared, "PreviousOperational", options.keyProvider, context);
|
|
125
|
-
prepared = await signPreparedC2spTlogEvent(prepared, "NewOperational",
|
|
144
|
+
prepared = await signPreparedC2spTlogEvent(prepared, "NewOperational", target, context);
|
|
126
145
|
const entryBytes = await c2spTlogEntryBytes(prepared, context);
|
|
127
146
|
const rotation = {
|
|
128
|
-
domain
|
|
147
|
+
domain,
|
|
129
148
|
logReference: options.logReference,
|
|
130
149
|
previousKid: previousKey.kid,
|
|
131
150
|
newKid,
|
|
151
|
+
previousProviderReference: options.previousProviderReference,
|
|
152
|
+
newProviderReference: options.newProviderReference,
|
|
153
|
+
previousKey,
|
|
154
|
+
previousThumbprint,
|
|
155
|
+
newThumbprint: await jwkThumbprint(newKey),
|
|
156
|
+
entryHash: await sha256Hex(entryBytes),
|
|
132
157
|
entryBytes: entryBytes.slice(),
|
|
133
158
|
idempotencyKey: options.idempotencyKey,
|
|
134
159
|
activated: false,
|
|
135
160
|
applicationSigningPaused: true
|
|
136
161
|
};
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
162
|
+
try {
|
|
163
|
+
await options.persistRotation(rotation);
|
|
164
|
+
await options.setApplicationSigningPaused(true);
|
|
165
|
+
} catch (cause) {
|
|
166
|
+
throw new ManagedKeyRotationSubmissionError("managed rotation persistence or pause failed; resume this state", rotation, true, cause);
|
|
167
|
+
}
|
|
168
|
+
return submitManagedRotation(options, options.keyProvider, target, rotation);
|
|
140
169
|
}
|
|
141
170
|
async function resumeManagedOperationalKeyRotation(options) {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
171
|
+
assertCoordination(options);
|
|
172
|
+
if (typeof options.resolveKeyProvider !== "function") throw new ArgumentError("managed rotation recovery requires resolveKeyProvider");
|
|
173
|
+
const rotation = options.rotation;
|
|
174
|
+
try {
|
|
175
|
+
assertOperationReferences(rotation.previousProviderReference, rotation.newProviderReference, rotation.idempotencyKey);
|
|
176
|
+
const previous = await options.resolveKeyProvider(rotation.previousProviderReference);
|
|
177
|
+
const target = rotation.newProviderReference === rotation.previousProviderReference ? previous : await options.resolveKeyProvider(rotation.newProviderReference);
|
|
178
|
+
await validateSavedRotation(rotation, previous, target);
|
|
179
|
+
if (rotation.submission?.state === "rejected") {
|
|
180
|
+
throw new ManagedKeyRotationSubmissionError("cannot resume a rejected managed key rotation", rotation, false);
|
|
181
|
+
}
|
|
182
|
+
await options.setApplicationSigningPaused(true);
|
|
183
|
+
if (rotation.submission?.state === "accepted") {
|
|
184
|
+
await assertSubmissionMatchesRotation(rotation, rotation.submission);
|
|
185
|
+
return activateRotation(options, previous, target, rotation);
|
|
186
|
+
}
|
|
187
|
+
if (rotation.activated) throw permanentRotationError("activated rotation has no accepted submission");
|
|
188
|
+
await options.persistRotation(rotation);
|
|
189
|
+
return submitManagedRotation(options, previous, target, rotation);
|
|
190
|
+
} catch (cause) {
|
|
191
|
+
if (cause instanceof ManagedKeyRotationSubmissionError || cause instanceof ManagedKeyRotationActivationError) throw cause;
|
|
192
|
+
throw new ManagedKeyRotationActivationError("managed rotation recovery failed; signing must remain paused", rotation, cause);
|
|
151
193
|
}
|
|
152
|
-
await options.setApplicationSigningPaused(true);
|
|
153
|
-
return submitManagedRotation(options.keyProvider, options.registryClient, options.rotation, options.persistRotation, options.setApplicationSigningPaused);
|
|
154
194
|
}
|
|
155
|
-
async function submitManagedRotation(
|
|
195
|
+
async function submitManagedRotation(options, previous, target, rotation) {
|
|
156
196
|
let submission;
|
|
157
197
|
try {
|
|
158
|
-
submission = await registryClient.submitPreparedEvent(rotation.domain, rotation.entryBytes, rotation.idempotencyKey);
|
|
198
|
+
submission = await options.registryClient.submitPreparedEvent(rotation.domain, rotation.entryBytes, rotation.idempotencyKey);
|
|
159
199
|
} catch (cause) {
|
|
160
200
|
const retryWithSameBytes2 = structuredRetryWithSameBytes(cause);
|
|
161
201
|
throw new ManagedKeyRotationSubmissionError(
|
|
@@ -165,9 +205,13 @@ async function submitManagedRotation(keyProvider, registryClient, rotation, pers
|
|
|
165
205
|
cause
|
|
166
206
|
);
|
|
167
207
|
}
|
|
168
|
-
await assertSubmissionMatchesRotation(rotation, submission);
|
|
169
208
|
const next = { ...rotation, submission };
|
|
170
|
-
|
|
209
|
+
try {
|
|
210
|
+
await assertSubmissionMatchesRotation(rotation, submission);
|
|
211
|
+
await options.persistRotation(next);
|
|
212
|
+
} catch (cause) {
|
|
213
|
+
throw new ManagedKeyRotationSubmissionError("managed rotation acceptance binding or persistence failed; resume this state", next, true, cause);
|
|
214
|
+
}
|
|
171
215
|
if (submission.state === "rejected") {
|
|
172
216
|
throw new ManagedKeyRotationSubmissionError(
|
|
173
217
|
`registry rejected the prepared KEY_ROTATION: ${submission.errorCode ?? "unknown error"}`,
|
|
@@ -176,15 +220,24 @@ async function submitManagedRotation(keyProvider, registryClient, rotation, pers
|
|
|
176
220
|
);
|
|
177
221
|
}
|
|
178
222
|
if (submission.state !== "accepted") return next;
|
|
179
|
-
|
|
223
|
+
return activateRotation(options, previous, target, next);
|
|
224
|
+
}
|
|
225
|
+
async function activateRotation(options, previous, target, rotation) {
|
|
226
|
+
let recoverable = rotation;
|
|
180
227
|
try {
|
|
181
|
-
await
|
|
182
|
-
|
|
183
|
-
await
|
|
228
|
+
await verifyAcceptedPublication(options, rotation);
|
|
229
|
+
if (!rotation.activated) await reconcileAcceptedRotation(previous, target, rotation);
|
|
230
|
+
if (previous !== target && await activeKey(previous, await previous.listKeyIds())) {
|
|
231
|
+
throw permanentRotationError("superseded source provider still has an active signing key");
|
|
232
|
+
}
|
|
233
|
+
const activated = { ...rotation, activated: true, applicationSigningPaused: true };
|
|
234
|
+
await options.persistRotation(activated);
|
|
184
235
|
recoverable = activated;
|
|
185
|
-
await
|
|
236
|
+
await assertKeyBinding(await target.signingKey(), rotation.newKid, rotation.newThumbprint);
|
|
237
|
+
await options.setApplicationSigner(target, rotation.newKid);
|
|
238
|
+
await options.setApplicationSigningPaused(false);
|
|
186
239
|
const completed = { ...activated, applicationSigningPaused: false };
|
|
187
|
-
await persistRotation(completed);
|
|
240
|
+
await options.persistRotation(completed);
|
|
188
241
|
return completed;
|
|
189
242
|
} catch (cause) {
|
|
190
243
|
throw new ManagedKeyRotationActivationError(
|
|
@@ -194,15 +247,84 @@ async function submitManagedRotation(keyProvider, registryClient, rotation, pers
|
|
|
194
247
|
);
|
|
195
248
|
}
|
|
196
249
|
}
|
|
197
|
-
async function reconcileAcceptedRotation(
|
|
198
|
-
const
|
|
199
|
-
if (
|
|
200
|
-
await
|
|
201
|
-
|
|
202
|
-
|
|
250
|
+
async function reconcileAcceptedRotation(previous, target, rotation) {
|
|
251
|
+
const active = await target.signingKey();
|
|
252
|
+
if (active.kid !== rotation.newKid) {
|
|
253
|
+
if (previous === target) await assertKeyBinding(active, rotation.previousKid, rotation.previousThumbprint);
|
|
254
|
+
await target.activate(rotation.newKid);
|
|
255
|
+
}
|
|
256
|
+
await assertKeyBinding(await target.signingKey(), rotation.newKid, rotation.newThumbprint);
|
|
257
|
+
if ((await previous.listKeyIds()).includes(rotation.previousKid)) await previous.supersede(rotation.previousKid);
|
|
258
|
+
}
|
|
259
|
+
function assertCoordination(options) {
|
|
260
|
+
for (const name of ["persistRotation", "setApplicationSigningPaused", "setApplicationSigner", "verifyPublicationEvidence"]) {
|
|
261
|
+
if (typeof options[name] !== "function") throw new ArgumentError(`managed rotation requires ${name}`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
function assertOperationReferences(previous, next, idempotencyKey) {
|
|
265
|
+
if (!previous?.trim() || !next?.trim() || !idempotencyKey || idempotencyKey.trim() !== idempotencyKey || idempotencyKey.length > 200) {
|
|
266
|
+
throw permanentRotationError("managed rotation requires provider references and a valid idempotency key");
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
async function assertKeyBinding(key, kid, thumbprint) {
|
|
270
|
+
new JWKS([key]).validateOperational();
|
|
271
|
+
if (key.kid !== kid || await jwkThumbprint(key) !== thumbprint) {
|
|
272
|
+
throw permanentRotationError("saved managed rotation key binding mismatch");
|
|
203
273
|
}
|
|
204
|
-
|
|
205
|
-
|
|
274
|
+
}
|
|
275
|
+
async function activeKey(provider, listedIds) {
|
|
276
|
+
if (provider.activeSigningKey) return provider.activeSigningKey();
|
|
277
|
+
return listedIds.length ? provider.signingKey() : null;
|
|
278
|
+
}
|
|
279
|
+
async function validateSavedRotation(rotation, previous, target) {
|
|
280
|
+
assertOperationReferences(rotation.previousProviderReference, rotation.newProviderReference, rotation.idempotencyKey);
|
|
281
|
+
if (typeof rotation.activated !== "boolean" || typeof rotation.applicationSigningPaused !== "boolean" || normalizeFQDN(rotation.domain, true) !== rotation.domain || await sha256Hex(rotation.entryBytes) !== rotation.entryHash) {
|
|
282
|
+
throw permanentRotationError("saved managed rotation domain or entry hash mismatch");
|
|
283
|
+
}
|
|
284
|
+
await assertKeyBinding(rotation.previousKey, rotation.previousKid, rotation.previousThumbprint);
|
|
285
|
+
const previousIds = await previous.listKeyIds();
|
|
286
|
+
const sourceActive = await activeKey(previous, previousIds);
|
|
287
|
+
if (previousIds.includes(rotation.previousKid)) {
|
|
288
|
+
await assertKeyBinding(await previous.jwk(rotation.previousKid), rotation.previousKid, rotation.previousThumbprint);
|
|
289
|
+
if (sourceActive) {
|
|
290
|
+
const targetIsActive = previous === target && rotation.submission?.state === "accepted" && sourceActive.kid === rotation.newKid;
|
|
291
|
+
await assertKeyBinding(
|
|
292
|
+
sourceActive,
|
|
293
|
+
targetIsActive ? rotation.newKid : rotation.previousKid,
|
|
294
|
+
targetIsActive ? rotation.newThumbprint : rotation.previousThumbprint
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
} else {
|
|
298
|
+
if (rotation.submission?.state !== "accepted") throw permanentRotationError("saved previous key is unavailable without saved acceptance");
|
|
299
|
+
await assertSubmissionMatchesRotation(rotation, rotation.submission);
|
|
300
|
+
await assertKeyBinding(await target.signingKey(), rotation.newKid, rotation.newThumbprint);
|
|
301
|
+
if (previous !== target && sourceActive) throw permanentRotationError("superseded source provider still has an active signing key");
|
|
302
|
+
}
|
|
303
|
+
const next = await target.jwk(rotation.newKid);
|
|
304
|
+
await assertKeyBinding(next, rotation.newKid, rotation.newThumbprint);
|
|
305
|
+
const context = { expectedFqdn: rotation.domain, previousOperationalKey: rotation.previousKey };
|
|
306
|
+
const prepared = await parsePreparedC2spTlogEvent(rotation.entryBytes, rotation.logReference, context);
|
|
307
|
+
if (prepared.envelope.type !== "KEY_ROTATION" || prepared.envelope.fqdn !== rotation.domain || prepared.envelope.prev_thumb !== rotation.previousThumbprint) {
|
|
308
|
+
throw permanentRotationError("saved managed rotation event binding mismatch");
|
|
309
|
+
}
|
|
310
|
+
await assertPreparedRotationKey(prepared.envelope.new_ku, prepared.envelope.new_thumb, next);
|
|
311
|
+
await c2spTlogEntryBytes(prepared, context);
|
|
312
|
+
if (rotation.submission) await assertSubmissionMatchesRotation(rotation, rotation.submission);
|
|
313
|
+
}
|
|
314
|
+
async function verifyAcceptedPublication(options, rotation) {
|
|
315
|
+
const verified = await options.verifyPublicationEvidence(rotation.domain);
|
|
316
|
+
if (verified.domain !== rotation.domain || verified.record.lr !== rotation.logReference) {
|
|
317
|
+
throw permanentRotationError("current signed publication does not match managed rotation");
|
|
318
|
+
}
|
|
319
|
+
await assertKeyBinding(verified.jwks.currentOperationalSigningKey(), rotation.newKid, rotation.newThumbprint);
|
|
320
|
+
const prepared = await parsePreparedC2spTlogEvent(rotation.entryBytes, rotation.logReference, {
|
|
321
|
+
expectedFqdn: rotation.domain,
|
|
322
|
+
previousOperationalKey: rotation.previousKey
|
|
323
|
+
});
|
|
324
|
+
const signatures = prepared.envelope.sigs;
|
|
325
|
+
const event = await verified.logReader.readEvent(rotation.submission.logRef);
|
|
326
|
+
if (event.type !== "KEY_ROTATION" || event.domain !== rotation.domain || event.previousOperationalThumbprint !== rotation.previousThumbprint || event.newOperationalKid !== rotation.newKid || event.newOperationalThumbprint !== rotation.newThumbprint || event.signingKid !== signatures.prev_op.kid || event.sig !== signatures.prev_op.sig || event.newOperationalProof !== signatures.new_op.sig || event.timestamp.getTime() !== prepared.envelope.ts * 1e3) {
|
|
327
|
+
throw permanentRotationError("accepted managed rotation continuity mismatch");
|
|
206
328
|
}
|
|
207
329
|
}
|
|
208
330
|
async function assertPreparedRotationKey(value, thumbprint, expected) {
|
|
@@ -219,12 +341,18 @@ async function assertSubmissionMatchesRotation(rotation, submission) {
|
|
|
219
341
|
if (rotation.submission?.entryHash && submission.entryHash && rotation.submission.entryHash !== submission.entryHash) {
|
|
220
342
|
throw permanentRotationError("registry reconciled a different entry hash for this managed key rotation");
|
|
221
343
|
}
|
|
344
|
+
if (submission.entryHash !== void 0 && submission.entryHash !== rotation.entryHash) {
|
|
345
|
+
throw permanentRotationError("submission entry hash does not match the exact managed key rotation bytes");
|
|
346
|
+
}
|
|
222
347
|
if (submission.state !== "accepted") return;
|
|
223
348
|
const expectedHash = await sha256Hex(rotation.entryBytes);
|
|
224
349
|
if (submission.entryHash !== expectedHash) {
|
|
225
350
|
throw permanentRotationError("accepted submission entry hash does not match the exact managed key rotation bytes");
|
|
226
351
|
}
|
|
227
|
-
if (submission.
|
|
352
|
+
if (submission.logRef !== `${rotation.logReference}@${submission.index}` || !Number.isSafeInteger(submission.index) || submission.index < 0) {
|
|
353
|
+
throw permanentRotationError("accepted submission log reference does not match managed rotation");
|
|
354
|
+
}
|
|
355
|
+
if (submission.keyId !== rotation.newKid) {
|
|
228
356
|
throw permanentRotationError("accepted submission key_id does not match the generated pending key");
|
|
229
357
|
}
|
|
230
358
|
}
|
|
@@ -249,7 +377,7 @@ import {
|
|
|
249
377
|
VerificationCode as VerificationCode2,
|
|
250
378
|
VerificationError as VerificationError2,
|
|
251
379
|
jwkThumbprint as jwkThumbprint2,
|
|
252
|
-
normalizeFQDN
|
|
380
|
+
normalizeFQDN as normalizeFQDN2
|
|
253
381
|
} from "@dnsid-ai/protocol";
|
|
254
382
|
import {
|
|
255
383
|
c2spTlogEntryBytes as c2spTlogEntryBytes2,
|
|
@@ -279,8 +407,8 @@ var ManagedIssuanceActivationError = class extends VerificationError2 {
|
|
|
279
407
|
}
|
|
280
408
|
};
|
|
281
409
|
async function issueManagedIdentity(options) {
|
|
282
|
-
const domain =
|
|
283
|
-
const governanceId =
|
|
410
|
+
const domain = normalizeFQDN2(options.domain, true);
|
|
411
|
+
const governanceId = normalizeFQDN2(options.governanceId, true);
|
|
284
412
|
if (!options.idempotencyKey || options.idempotencyKey.trim() !== options.idempotencyKey || options.idempotencyKey.length > 200) {
|
|
285
413
|
throw permanentLogError("managed ISSUANCE idempotencyKey must contain 1 to 200 characters without surrounding whitespace");
|
|
286
414
|
}
|
|
@@ -549,7 +677,11 @@ var SDK_CONFORMANCE = Object.freeze({
|
|
|
549
677
|
logBindings: Object.freeze({
|
|
550
678
|
"c2sp-tlog": c2spTlogConformance
|
|
551
679
|
}),
|
|
552
|
-
knownDeviations: Object.freeze([
|
|
680
|
+
knownDeviations: Object.freeze([
|
|
681
|
+
"Managed cross-provider rotation requires providers that can supersede the previous active key after transfer; built-in LocalKeyProvider and AwsKmsKeyProvider reject active-key supersession.",
|
|
682
|
+
"AWS KMS and CLI/in-memory key providers do not durably persist rotation key state; restart recovery requires application-managed provider state or a file-backed local key store.",
|
|
683
|
+
"Account onboarding validates bindings and reports proof readiness, but does not implement named managed creation or establish permanent registration replay claims."
|
|
684
|
+
])
|
|
553
685
|
});
|
|
554
686
|
|
|
555
687
|
// src/index.ts
|
|
@@ -560,7 +692,7 @@ function createIdentityVerifier(config, deps) {
|
|
|
560
692
|
return new IdentityManager(config, deps);
|
|
561
693
|
}
|
|
562
694
|
export {
|
|
563
|
-
ArgumentError,
|
|
695
|
+
ArgumentError2 as ArgumentError,
|
|
564
696
|
DEFAULT_REGISTRY_URL,
|
|
565
697
|
DNSID_VERSION2 as DNSID_VERSION,
|
|
566
698
|
DNSSECMode,
|
|
@@ -607,7 +739,7 @@ export {
|
|
|
607
739
|
jose,
|
|
608
740
|
jwkSignatureAlg,
|
|
609
741
|
jwkThumbprint3 as jwkThumbprint,
|
|
610
|
-
|
|
742
|
+
normalizeFQDN3 as normalizeFQDN,
|
|
611
743
|
parseKaDuration,
|
|
612
744
|
parseKeyId,
|
|
613
745
|
publishClientControlledRecord,
|
package/dist/node.cjs
CHANGED
|
@@ -162,9 +162,14 @@ var LocalKeyProvider = class _LocalKeyProvider {
|
|
|
162
162
|
const sig = await crypto.subtle.sign(signAlgorithm(key), cryptoKey, toArrayBuffer(payload));
|
|
163
163
|
return new Uint8Array(sig);
|
|
164
164
|
}
|
|
165
|
-
async
|
|
165
|
+
async validateKeyGeneration(algorithm) {
|
|
166
|
+
if (algorithm !== "EdDSA" && algorithm !== "ES256") throw new import_protocol.ArgumentError(`unsupported local key algorithm: ${algorithm}`);
|
|
167
|
+
}
|
|
168
|
+
async generateKey(algorithm) {
|
|
166
169
|
return this.mutate(async (next) => {
|
|
167
|
-
const
|
|
170
|
+
const selected = algorithm ?? localKeyAlgorithm(next.active);
|
|
171
|
+
await this.validateKeyGeneration(selected);
|
|
172
|
+
const key = await generateStoredKey(selected);
|
|
168
173
|
next.pending.push(key);
|
|
169
174
|
return key.kid;
|
|
170
175
|
});
|
|
@@ -321,6 +326,13 @@ var import_protocol2 = require("@dnsid-ai/protocol");
|
|
|
321
326
|
async function createNodeIdentityManager(config, deps = {}) {
|
|
322
327
|
const { transport, ...core } = (0, import_protocol2.validateDnsidConfig)(config);
|
|
323
328
|
const { dnsServer, caBundlePath, privateAddressHosts } = transport;
|
|
329
|
+
validateInjectedTransport(transport, deps);
|
|
330
|
+
const transportModule = deps.fetchJson && deps.dnsResolver ? null : await loadNodeTransport();
|
|
331
|
+
const fetchJson = deps.fetchJson ?? ((url, fetchOptions) => transportModule.fetchJson(url, { ...fetchOptions, dnsServer, caBundlePath, privateAddressHosts }));
|
|
332
|
+
const dnsResolver = deps.dnsResolver ?? transportModule.createDefaultDnsResolver({ dnsServer });
|
|
333
|
+
return new import_protocol2.IdentityManager(core, { ...deps, dnsResolver, fetchJson });
|
|
334
|
+
}
|
|
335
|
+
function validateInjectedTransport({ dnsServer, caBundlePath, privateAddressHosts }, deps) {
|
|
324
336
|
if (dnsServer !== void 0 && deps.dnsResolver && deps.fetchJson) {
|
|
325
337
|
throw new import_protocol2.ArgumentError("config.transport.dnsServer has no SDK-managed consumer when both dnsResolver and fetchJson are injected");
|
|
326
338
|
}
|
|
@@ -330,10 +342,6 @@ async function createNodeIdentityManager(config, deps = {}) {
|
|
|
330
342
|
if (privateAddressHosts !== void 0 && deps.fetchJson) {
|
|
331
343
|
throw new import_protocol2.ArgumentError("config.transport.privateAddressHosts has no SDK-managed consumer when fetchJson is injected");
|
|
332
344
|
}
|
|
333
|
-
const transportModule = deps.fetchJson && deps.dnsResolver ? null : await loadNodeTransport();
|
|
334
|
-
const fetchJson = deps.fetchJson ?? ((url, fetchOptions) => transportModule.fetchJson(url, { ...fetchOptions, dnsServer, caBundlePath, privateAddressHosts }));
|
|
335
|
-
const dnsResolver = deps.dnsResolver ?? transportModule.createDefaultDnsResolver({ dnsServer });
|
|
336
|
-
return new import_protocol2.IdentityManager(core, { ...deps, dnsResolver, fetchJson });
|
|
337
345
|
}
|
|
338
346
|
async function createNodeIdentityVerifier(config = {}, deps = {}) {
|
|
339
347
|
return createNodeIdentityManager(config, deps);
|
|
@@ -559,7 +567,23 @@ function mergeSection(base, overlay) {
|
|
|
559
567
|
}
|
|
560
568
|
var MANAGED_DEFAULTS_MS = 10 * 60 * 1e3;
|
|
561
569
|
async function constructIdentityManager(loaded, deps = {}) {
|
|
562
|
-
(0, import_protocol3.validateDnsidConfig)(loaded.dnsid ?? {});
|
|
570
|
+
const config = (0, import_protocol3.validateDnsidConfig)(loaded.dnsid ?? {});
|
|
571
|
+
validateInjectedTransport(config.transport ?? {}, deps);
|
|
572
|
+
if (!config.identity && (deps.keyProvider || deps.entityKeyProvider)) {
|
|
573
|
+
throw new import_protocol3.ArgumentError("key providers have no consumer without config.identity");
|
|
574
|
+
}
|
|
575
|
+
if (config.identity && !deps.keyProvider) {
|
|
576
|
+
const source = loaded.keySource;
|
|
577
|
+
if (source) await validateKeySource(source);
|
|
578
|
+
if (source?.generation) throw new import_protocol3.ArgumentError("identity-manager construction opens existing keys only; use managed setup for generation");
|
|
579
|
+
if (!source?.keyRef && !source?.cliDirectory && !source?.keyStorePath) {
|
|
580
|
+
throw new import_protocol3.ArgumentError("deps.keyProvider or a configured operational key source is required when config.identity is set");
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
const entityKeyPath = loaded.dnsid?.identity && !deps.entityKeyProvider ? loaded.keySource?.entityKeyPath : void 0;
|
|
584
|
+
if (entityKeyPath !== void 0 && (typeof entityKeyPath !== "string" || !entityKeyPath.trim())) {
|
|
585
|
+
throw new import_protocol3.ArgumentError("keySource.entityKeyPath must be nonempty");
|
|
586
|
+
}
|
|
563
587
|
const filled = { ...deps };
|
|
564
588
|
if (!filled.logRegistry && loaded.logTrust) {
|
|
565
589
|
filled.logRegistry = await logRegistryFromTrust(loaded.logTrust, loaded.dnsid?.transport);
|
|
@@ -621,9 +645,10 @@ async function operationalKeyProvider(source, domain) {
|
|
|
621
645
|
async function validateKeySource(source = {}) {
|
|
622
646
|
const provider = source.provider ?? "file";
|
|
623
647
|
if (!["file", "aws-kms", "google-kms", "azure-key-vault"].includes(provider)) throw new import_protocol3.ArgumentError(`unknown key provider: ${provider}`);
|
|
624
|
-
for (const field of ["keyRef", "keyStorePath", "cliDirectory"
|
|
648
|
+
for (const field of ["keyRef", "keyStorePath", "cliDirectory"]) {
|
|
625
649
|
if (source[field] !== void 0 && (typeof source[field] !== "string" || !source[field].trim())) throw new import_protocol3.ArgumentError(`keySource.${field} must be nonempty`);
|
|
626
650
|
}
|
|
651
|
+
if (source.settings !== void 0) object(source.settings, "keySource.settings");
|
|
627
652
|
if (source.keyRef && source.generation) throw new import_protocol3.ArgumentError("keyRef and generation are mutually exclusive");
|
|
628
653
|
if ((source.cliDirectory || source.keyStorePath) && (provider !== "file" || source.keyRef || source.generation)) throw new import_protocol3.ArgumentError("CLI/keyStorePath cannot combine with cloud, keyRef, or generation");
|
|
629
654
|
if (source.generation && (typeof source.generation.locator !== "string" || !source.generation.locator.trim() || !["EdDSA", "ES256"].includes(source.generation.algorithm))) throw new import_protocol3.ArgumentError("generation requires a stable locator and supported EdDSA or ES256 algorithm");
|
package/dist/node.d.cts
CHANGED
|
@@ -36,7 +36,8 @@ declare class LocalKeyProvider implements KeyProvider {
|
|
|
36
36
|
sign(payload: Uint8Array): Promise<Uint8Array>;
|
|
37
37
|
signKey(kid: string, payload: Uint8Array): Promise<Uint8Array>;
|
|
38
38
|
private signWithKey;
|
|
39
|
-
|
|
39
|
+
validateKeyGeneration(algorithm: string): Promise<void>;
|
|
40
|
+
generateKey(algorithm?: string): Promise<string>;
|
|
40
41
|
activate(kid: string): Promise<void>;
|
|
41
42
|
supersede(kid: string): Promise<void>;
|
|
42
43
|
/** @deprecated Use supersede(). */
|
package/dist/node.d.ts
CHANGED
|
@@ -36,7 +36,8 @@ declare class LocalKeyProvider implements KeyProvider {
|
|
|
36
36
|
sign(payload: Uint8Array): Promise<Uint8Array>;
|
|
37
37
|
signKey(kid: string, payload: Uint8Array): Promise<Uint8Array>;
|
|
38
38
|
private signWithKey;
|
|
39
|
-
|
|
39
|
+
validateKeyGeneration(algorithm: string): Promise<void>;
|
|
40
|
+
generateKey(algorithm?: string): Promise<string>;
|
|
40
41
|
activate(kid: string): Promise<void>;
|
|
41
42
|
supersede(kid: string): Promise<void>;
|
|
42
43
|
/** @deprecated Use supersede(). */
|
package/dist/node.js
CHANGED
|
@@ -115,9 +115,14 @@ var LocalKeyProvider = class _LocalKeyProvider {
|
|
|
115
115
|
const sig = await crypto.subtle.sign(signAlgorithm(key), cryptoKey, toArrayBuffer(payload));
|
|
116
116
|
return new Uint8Array(sig);
|
|
117
117
|
}
|
|
118
|
-
async
|
|
118
|
+
async validateKeyGeneration(algorithm) {
|
|
119
|
+
if (algorithm !== "EdDSA" && algorithm !== "ES256") throw new ArgumentError(`unsupported local key algorithm: ${algorithm}`);
|
|
120
|
+
}
|
|
121
|
+
async generateKey(algorithm) {
|
|
119
122
|
return this.mutate(async (next) => {
|
|
120
|
-
const
|
|
123
|
+
const selected = algorithm ?? localKeyAlgorithm(next.active);
|
|
124
|
+
await this.validateKeyGeneration(selected);
|
|
125
|
+
const key = await generateStoredKey(selected);
|
|
121
126
|
next.pending.push(key);
|
|
122
127
|
return key.kid;
|
|
123
128
|
});
|
|
@@ -274,6 +279,13 @@ import { IdentityManager, ArgumentError as ArgumentError2, validateDnsidConfig }
|
|
|
274
279
|
async function createNodeIdentityManager(config, deps = {}) {
|
|
275
280
|
const { transport, ...core } = validateDnsidConfig(config);
|
|
276
281
|
const { dnsServer, caBundlePath, privateAddressHosts } = transport;
|
|
282
|
+
validateInjectedTransport(transport, deps);
|
|
283
|
+
const transportModule = deps.fetchJson && deps.dnsResolver ? null : await loadNodeTransport();
|
|
284
|
+
const fetchJson = deps.fetchJson ?? ((url, fetchOptions) => transportModule.fetchJson(url, { ...fetchOptions, dnsServer, caBundlePath, privateAddressHosts }));
|
|
285
|
+
const dnsResolver = deps.dnsResolver ?? transportModule.createDefaultDnsResolver({ dnsServer });
|
|
286
|
+
return new IdentityManager(core, { ...deps, dnsResolver, fetchJson });
|
|
287
|
+
}
|
|
288
|
+
function validateInjectedTransport({ dnsServer, caBundlePath, privateAddressHosts }, deps) {
|
|
277
289
|
if (dnsServer !== void 0 && deps.dnsResolver && deps.fetchJson) {
|
|
278
290
|
throw new ArgumentError2("config.transport.dnsServer has no SDK-managed consumer when both dnsResolver and fetchJson are injected");
|
|
279
291
|
}
|
|
@@ -283,10 +295,6 @@ async function createNodeIdentityManager(config, deps = {}) {
|
|
|
283
295
|
if (privateAddressHosts !== void 0 && deps.fetchJson) {
|
|
284
296
|
throw new ArgumentError2("config.transport.privateAddressHosts has no SDK-managed consumer when fetchJson is injected");
|
|
285
297
|
}
|
|
286
|
-
const transportModule = deps.fetchJson && deps.dnsResolver ? null : await loadNodeTransport();
|
|
287
|
-
const fetchJson = deps.fetchJson ?? ((url, fetchOptions) => transportModule.fetchJson(url, { ...fetchOptions, dnsServer, caBundlePath, privateAddressHosts }));
|
|
288
|
-
const dnsResolver = deps.dnsResolver ?? transportModule.createDefaultDnsResolver({ dnsServer });
|
|
289
|
-
return new IdentityManager(core, { ...deps, dnsResolver, fetchJson });
|
|
290
298
|
}
|
|
291
299
|
async function createNodeIdentityVerifier(config = {}, deps = {}) {
|
|
292
300
|
return createNodeIdentityManager(config, deps);
|
|
@@ -523,7 +531,23 @@ function mergeSection(base, overlay) {
|
|
|
523
531
|
}
|
|
524
532
|
var MANAGED_DEFAULTS_MS = 10 * 60 * 1e3;
|
|
525
533
|
async function constructIdentityManager(loaded, deps = {}) {
|
|
526
|
-
validateDnsidConfig2(loaded.dnsid ?? {});
|
|
534
|
+
const config = validateDnsidConfig2(loaded.dnsid ?? {});
|
|
535
|
+
validateInjectedTransport(config.transport ?? {}, deps);
|
|
536
|
+
if (!config.identity && (deps.keyProvider || deps.entityKeyProvider)) {
|
|
537
|
+
throw new ArgumentError3("key providers have no consumer without config.identity");
|
|
538
|
+
}
|
|
539
|
+
if (config.identity && !deps.keyProvider) {
|
|
540
|
+
const source = loaded.keySource;
|
|
541
|
+
if (source) await validateKeySource(source);
|
|
542
|
+
if (source?.generation) throw new ArgumentError3("identity-manager construction opens existing keys only; use managed setup for generation");
|
|
543
|
+
if (!source?.keyRef && !source?.cliDirectory && !source?.keyStorePath) {
|
|
544
|
+
throw new ArgumentError3("deps.keyProvider or a configured operational key source is required when config.identity is set");
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
const entityKeyPath = loaded.dnsid?.identity && !deps.entityKeyProvider ? loaded.keySource?.entityKeyPath : void 0;
|
|
548
|
+
if (entityKeyPath !== void 0 && (typeof entityKeyPath !== "string" || !entityKeyPath.trim())) {
|
|
549
|
+
throw new ArgumentError3("keySource.entityKeyPath must be nonempty");
|
|
550
|
+
}
|
|
527
551
|
const filled = { ...deps };
|
|
528
552
|
if (!filled.logRegistry && loaded.logTrust) {
|
|
529
553
|
filled.logRegistry = await logRegistryFromTrust(loaded.logTrust, loaded.dnsid?.transport);
|
|
@@ -585,9 +609,10 @@ async function operationalKeyProvider(source, domain) {
|
|
|
585
609
|
async function validateKeySource(source = {}) {
|
|
586
610
|
const provider = source.provider ?? "file";
|
|
587
611
|
if (!["file", "aws-kms", "google-kms", "azure-key-vault"].includes(provider)) throw new ArgumentError3(`unknown key provider: ${provider}`);
|
|
588
|
-
for (const field of ["keyRef", "keyStorePath", "cliDirectory"
|
|
612
|
+
for (const field of ["keyRef", "keyStorePath", "cliDirectory"]) {
|
|
589
613
|
if (source[field] !== void 0 && (typeof source[field] !== "string" || !source[field].trim())) throw new ArgumentError3(`keySource.${field} must be nonempty`);
|
|
590
614
|
}
|
|
615
|
+
if (source.settings !== void 0) object(source.settings, "keySource.settings");
|
|
591
616
|
if (source.keyRef && source.generation) throw new ArgumentError3("keyRef and generation are mutually exclusive");
|
|
592
617
|
if ((source.cliDirectory || source.keyStorePath) && (provider !== "file" || source.keyRef || source.generation)) throw new ArgumentError3("CLI/keyStorePath cannot combine with cloud, keyRef, or generation");
|
|
593
618
|
if (source.generation && (typeof source.generation.locator !== "string" || !source.generation.locator.trim() || !["EdDSA", "ES256"].includes(source.generation.algorithm))) throw new ArgumentError3("generation requires a stable locator and supported EdDSA or ES256 algorithm");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dnsid-ai/sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.31.0",
|
|
4
4
|
"description": "Runtime-neutral DNSid SDK entrypoint with optional Node.js convenience helpers.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -39,16 +39,16 @@
|
|
|
39
39
|
"typecheck": "tsc --noEmit"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@dnsid-ai/protocol": "^0.
|
|
43
|
-
"@dnsid-ai/http-signatures": "^0.
|
|
44
|
-
"@dnsid-ai/jose": "^0.
|
|
45
|
-
"@dnsid-ai/log-c2sp-tlog": "^0.
|
|
46
|
-
"@dnsid-ai/registry": "^0.
|
|
47
|
-
"@dnsid-ai/web-bot-auth": "^0.
|
|
42
|
+
"@dnsid-ai/protocol": "^0.31.0",
|
|
43
|
+
"@dnsid-ai/http-signatures": "^0.31.0",
|
|
44
|
+
"@dnsid-ai/jose": "^0.31.0",
|
|
45
|
+
"@dnsid-ai/log-c2sp-tlog": "^0.31.0",
|
|
46
|
+
"@dnsid-ai/registry": "^0.31.0",
|
|
47
|
+
"@dnsid-ai/web-bot-auth": "^0.31.0"
|
|
48
48
|
},
|
|
49
49
|
"peerDependencies": {
|
|
50
|
-
"@dnsid-ai/key-aws": "^0.
|
|
51
|
-
"@dnsid-ai/transport": "^0.
|
|
50
|
+
"@dnsid-ai/key-aws": "^0.31.0",
|
|
51
|
+
"@dnsid-ai/transport": "^0.31.0"
|
|
52
52
|
},
|
|
53
53
|
"peerDependenciesMeta": {
|
|
54
54
|
"@dnsid-ai/key-aws": {
|