@@ -6,6 +6,7 @@ import type { Env } from "./bindings.js";
66import { CloudflareStore } from "./store.js" ;
77import { KVRateLimiter } from "./rateLimit.js" ;
88import { EnclaveClient , EnclaveClientError } from "./enclaveClient.js" ;
9+ import * as workerSigner from "./workerSigner.js" ;
910import { signSessionToken , signTicketToken , verifySessionToken , verifyTicketToken } from "./auth.js" ;
1011import {
1112 challengeRequestSchema ,
@@ -52,6 +53,21 @@ function getEnclave(env: Env): EnclaveClient {
5253 return new EnclaveClient ( env . ENCLAVE_BASE_URL , env . INTERNAL_API_KEY , env . EV_API_KEY || undefined ) ;
5354}
5455
56+ function isWorkerMode ( env : Env ) : boolean {
57+ return env . SIGNER_MODE === "worker" ;
58+ }
59+
60+ function requireMasterKey ( env : Env ) : string {
61+ if ( ! env . WORKER_SEALING_KEY ) throw new HTTPException ( 500 , { message : "WORKER_SEALING_KEY secret not configured for worker mode" } ) ;
62+ return env . WORKER_SEALING_KEY ;
63+ }
64+
65+ // Worker sealed keys are "{24 hex iv}:{variable hex ciphertext+tag}".
66+ // Enclave (Evervault) keys are opaque blobs that don't match this pattern.
67+ function isWorkerSealedKey ( sealedKey : string ) : boolean {
68+ return / ^ [ 0 - 9 a - f ] { 24 } : [ 0 - 9 a - f ] + $ / . test ( sealedKey ) ;
69+ }
70+
5571async function currentUser ( store : CloudflareStore , userId : string ) {
5672 const user = await store . getUserById ( userId ) ;
5773 if ( ! user ) throw new HTTPException ( 401 , { message : "Session user no longer exists" } ) ;
@@ -210,7 +226,6 @@ app.post("/v1/identities", requireAuth, async (c) => {
210226 const auth = c . get ( "auth" ) ;
211227 const store = getStore ( c . env ) ;
212228 const limiter = getLimiter ( c . env ) ;
213- const enclave = getEnclave ( c . env ) ;
214229
215230 const user = await currentUser ( store , auth . sub ) ;
216231 await enforceRate ( limiter , `user:${ user . id } :identity_create` , c . env ) ;
@@ -219,11 +234,21 @@ app.post("/v1/identities", requireAuth, async (c) => {
219234 const identityId = crypto . randomUUID ( ) ;
220235 const alg : SupportedAlg = body . alg ?? "secp256k1" ;
221236
222- const generated = await enclave . generate ( identityId , alg ) ;
223- const exported = await enclave . exportKey ( identityId ) ;
224- await store . putBackup ( { identity_id : identityId , alg : exported . alg , sealed_key : exported . sealed_key } ) ;
237+ let publicKey : string ;
238+ if ( isWorkerMode ( c . env ) ) {
239+ const masterKey = requireMasterKey ( c . env ) ;
240+ const generated = await workerSigner . generateKey ( masterKey ) ;
241+ await store . putBackup ( { identity_id : identityId , alg, sealed_key : generated . sealedKey } ) ;
242+ publicKey = generated . publicKey ;
243+ } else {
244+ const enclave = getEnclave ( c . env ) ;
245+ const generated = await enclave . generate ( identityId , alg ) ;
246+ const exported = await enclave . exportKey ( identityId ) ;
247+ await store . putBackup ( { identity_id : identityId , alg : exported . alg , sealed_key : exported . sealed_key } ) ;
248+ publicKey = generated . public_key ;
249+ }
225250
226- const identity = await store . createIdentity ( { id : identityId , user_id : user . id , alg, public_key : generated . public_key } ) ;
251+ const identity = await store . createIdentity ( { id : identityId , user_id : user . id , alg, public_key : publicKey } ) ;
227252 await store . addAuditEvent ( { user_id : user . id , identity_id : identity . id , action : "identity.create" , metadata : { alg : identity . alg } } ) ;
228253
229254 return c . json ( identity , 201 ) ;
@@ -233,21 +258,35 @@ app.post("/v1/identities/:id/restore", requireAuth, async (c) => {
233258 const auth = c . get ( "auth" ) ;
234259 const identityId = c . req . param ( "id" ) ;
235260 const store = getStore ( c . env ) ;
236- const enclave = getEnclave ( c . env ) ;
237261
238262 const user = await currentUser ( store , auth . sub ) ;
239263
240264 const existing = await store . getIdentity ( identityId ) ;
241265 if ( existing ) {
242266 if ( existing . user_id !== user . id ) throw new HTTPException ( 403 , { message : "Identity does not belong to session user" } ) ;
243- await restoreBackup ( store , enclave , identityId ) ;
267+ if ( isWorkerMode ( c . env ) ) {
268+ const backup = await store . getBackup ( identityId ) ;
269+ if ( backup && ! isWorkerSealedKey ( backup . sealed_key ) ) {
270+ throw new HTTPException ( 409 , { message : "Identity was created in enclave mode and cannot be used in worker mode. Please create a new identity." } ) ;
271+ }
272+ } else {
273+ const enclave = getEnclave ( c . env ) ;
274+ await restoreBackup ( store , enclave , identityId ) ;
275+ }
244276 return c . json ( existing ) ;
245277 }
246278
247279 const backup = await store . getBackup ( identityId ) ;
248280 if ( ! backup ) throw new HTTPException ( 404 , { message : "No backup found for this identity" } ) ;
249281
250- await enclave . importKey ( identityId , backup . alg , stripWrappingQuotes ( backup . sealed_key ) ) ;
282+ if ( isWorkerMode ( c . env ) ) {
283+ if ( ! isWorkerSealedKey ( backup . sealed_key ) ) {
284+ throw new HTTPException ( 409 , { message : "Identity was created in enclave mode and cannot be used in worker mode. Please create a new identity." } ) ;
285+ }
286+ } else {
287+ const enclave = getEnclave ( c . env ) ;
288+ await enclave . importKey ( identityId , backup . alg , stripWrappingQuotes ( backup . sealed_key ) ) ;
289+ }
251290
252291 const body = ( await c . req . json ( ) ) as { public_key ?: string } ;
253292 if ( ! body . public_key ) throw new HTTPException ( 400 , { message : "Missing public_key in request body" } ) ;
@@ -292,7 +331,6 @@ app.post("/v1/identities/:id/sign", requireAuth, async (c) => {
292331 const identityId = c . req . param ( "id" ) ;
293332 const store = getStore ( c . env ) ;
294333 const limiter = getLimiter ( c . env ) ;
295- const enclave = getEnclave ( c . env ) ;
296334
297335 const user = await currentUser ( store , auth . sub ) ;
298336 const identity = await ownedActiveIdentity ( store , identityId , user . id ) ;
@@ -314,14 +352,22 @@ app.post("/v1/identities/:id/sign", requireAuth, async (c) => {
314352 if ( new Date ( ticket . expires_at ) . getTime ( ) <= Date . now ( ) ) throw new HTTPException ( 410 , { message : "Ticket expired" } ) ;
315353
316354 let signature : string ;
317- try {
318- signature = ( await enclave . sign ( identity . id , digest , body . ticket ) ) . signature ;
319- } catch ( error ) {
320- if ( ! ( error instanceof EnclaveClientError ) || error . statusCode !== 404 ) throw error ;
321- if ( ! ( await restoreBackup ( store , enclave , identity . id ) ) ) {
322- throw new HTTPException ( 409 , { message : "Key not present in enclave and no backup available" } ) ;
355+ if ( isWorkerMode ( c . env ) ) {
356+ const masterKey = requireMasterKey ( c . env ) ;
357+ const backup = await store . getBackup ( identity . id ) ;
358+ if ( ! backup ) throw new HTTPException ( 409 , { message : "No key backup found" } ) ;
359+ signature = await workerSigner . signDigest ( backup . sealed_key , masterKey , digest ) ;
360+ } else {
361+ const enclave = getEnclave ( c . env ) ;
362+ try {
363+ signature = ( await enclave . sign ( identity . id , digest , body . ticket ) ) . signature ;
364+ } catch ( error ) {
365+ if ( ! ( error instanceof EnclaveClientError ) || error . statusCode !== 404 ) throw error ;
366+ if ( ! ( await restoreBackup ( store , enclave , identity . id ) ) ) {
367+ throw new HTTPException ( 409 , { message : "Key not present in enclave and no backup available" } ) ;
368+ }
369+ signature = ( await enclave . sign ( identity . id , digest , body . ticket ) ) . signature ;
323370 }
324- signature = ( await enclave . sign ( identity . id , digest , body . ticket ) ) . signature ;
325371 }
326372
327373 await store . markTicketUsed ( ticket . id ) ;
@@ -335,7 +381,6 @@ app.post("/v1/identities/:id/sign-batch", requireAuth, async (c) => {
335381 const identityId = c . req . param ( "id" ) ;
336382 const store = getStore ( c . env ) ;
337383 const limiter = getLimiter ( c . env ) ;
338- const enclave = getEnclave ( c . env ) ;
339384
340385 const user = await currentUser ( store , auth . sub ) ;
341386 const identity = await ownedActiveIdentity ( store , identityId , user . id ) ;
@@ -345,34 +390,55 @@ app.post("/v1/identities/:id/sign-batch", requireAuth, async (c) => {
345390 const ticketTtl = parseInt ( c . env . TICKET_TTL_SECONDS , 10 ) ;
346391 const signatures : string [ ] = [ ] ;
347392
348- for ( const item of body . digests ) {
349- const digest = normalizeDigestHex ( item . digest ) ;
350- const digestHash = await CloudflareStore . digestHash ( digest ) ;
351- const nonce = crypto . randomUUID ( ) ;
352- const ticketId = crypto . randomUUID ( ) ;
353-
354- const ticket = await signTicketToken (
355- { jti : ticketId , sub : user . id , identity_id : identity . id , digest_hash : digestHash , scope : "sign" , nonce } ,
356- c . env . TICKET_SIGNING_SECRET ,
357- ticketTtl ,
358- ) ;
359-
360- const expiresAt = new Date ( Date . now ( ) + ticketTtl * 1000 ) . toISOString ( ) ;
361- await store . createTicket ( { id : ticketId , identity_id : identity . id , digest_hash : digestHash , scope : "sign" , expires_at : expiresAt , nonce } , ticketTtl ) ;
362-
363- let signature : string ;
364- try {
365- signature = ( await enclave . sign ( identity . id , digest , ticket ) ) . signature ;
366- } catch ( error ) {
367- if ( ! ( error instanceof EnclaveClientError ) || error . statusCode !== 404 ) throw error ;
368- if ( ! ( await restoreBackup ( store , enclave , identity . id ) ) ) {
369- throw new HTTPException ( 409 , { message : "Key not present in enclave and no backup available" } ) ;
370- }
371- signature = ( await enclave . sign ( identity . id , digest , ticket ) ) . signature ;
393+ // Worker mode: fetch backup once for all digests
394+ let workerBackupSealedKey : string | null = null ;
395+ if ( isWorkerMode ( c . env ) ) {
396+ const masterKey = requireMasterKey ( c . env ) ;
397+ const backup = await store . getBackup ( identity . id ) ;
398+ if ( ! backup ) throw new HTTPException ( 409 , { message : "No key backup found" } ) ;
399+ workerBackupSealedKey = backup . sealed_key ;
400+ // masterKey captured in closure below
401+ for ( const item of body . digests ) {
402+ const digest = normalizeDigestHex ( item . digest ) ;
403+ const digestHash = await CloudflareStore . digestHash ( digest ) ;
404+ const ticketId = crypto . randomUUID ( ) ;
405+ const expiresAt = new Date ( Date . now ( ) + ticketTtl * 1000 ) . toISOString ( ) ;
406+ await store . createTicket ( { id : ticketId , identity_id : identity . id , digest_hash : digestHash , scope : "sign" , expires_at : expiresAt , nonce : crypto . randomUUID ( ) } , ticketTtl ) ;
407+ const signature = await workerSigner . signDigest ( workerBackupSealedKey , masterKey , digest ) ;
408+ await store . markTicketUsed ( ticketId ) ;
409+ signatures . push ( signature ) ;
372410 }
411+ } else {
412+ const enclave = getEnclave ( c . env ) ;
413+ for ( const item of body . digests ) {
414+ const digest = normalizeDigestHex ( item . digest ) ;
415+ const digestHash = await CloudflareStore . digestHash ( digest ) ;
416+ const nonce = crypto . randomUUID ( ) ;
417+ const ticketId = crypto . randomUUID ( ) ;
418+
419+ const ticket = await signTicketToken (
420+ { jti : ticketId , sub : user . id , identity_id : identity . id , digest_hash : digestHash , scope : "sign" , nonce } ,
421+ c . env . TICKET_SIGNING_SECRET ,
422+ ticketTtl ,
423+ ) ;
424+
425+ const expiresAt = new Date ( Date . now ( ) + ticketTtl * 1000 ) . toISOString ( ) ;
426+ await store . createTicket ( { id : ticketId , identity_id : identity . id , digest_hash : digestHash , scope : "sign" , expires_at : expiresAt , nonce } , ticketTtl ) ;
427+
428+ let signature : string ;
429+ try {
430+ signature = ( await enclave . sign ( identity . id , digest , ticket ) ) . signature ;
431+ } catch ( error ) {
432+ if ( ! ( error instanceof EnclaveClientError ) || error . statusCode !== 404 ) throw error ;
433+ if ( ! ( await restoreBackup ( store , enclave , identity . id ) ) ) {
434+ throw new HTTPException ( 409 , { message : "Key not present in enclave and no backup available" } ) ;
435+ }
436+ signature = ( await enclave . sign ( identity . id , digest , ticket ) ) . signature ;
437+ }
373438
374- await store . markTicketUsed ( ticketId ) ;
375- signatures . push ( signature ) ;
439+ await store . markTicketUsed ( ticketId ) ;
440+ signatures . push ( signature ) ;
441+ }
376442 }
377443
378444 await store . addAuditEvent ( { user_id : user . id , identity_id : identity . id , action : "identity.sign" , metadata : { batch_size : body . digests . length } } ) ;
@@ -385,18 +451,20 @@ app.delete("/v1/identities/:id", requireAuth, async (c) => {
385451 const identityId = c . req . param ( "id" ) ;
386452 const store = getStore ( c . env ) ;
387453 const limiter = getLimiter ( c . env ) ;
388- const enclave = getEnclave ( c . env ) ;
389454
390455 const user = await currentUser ( store , auth . sub ) ;
391456 const identity = await ownedActiveIdentity ( store , identityId , user . id ) ;
392457 await enforceRate ( limiter , `identity:${ identity . id } :destroy` , c . env ) ;
393458
394- try {
395- await enclave . destroy ( identity . id ) ;
396- } catch ( error ) {
397- if ( ! ( error instanceof EnclaveClientError ) || error . statusCode !== 404 ) throw error ;
398- if ( await restoreBackup ( store , enclave , identity . id ) ) {
459+ if ( ! isWorkerMode ( c . env ) ) {
460+ const enclave = getEnclave ( c . env ) ;
461+ try {
399462 await enclave . destroy ( identity . id ) ;
463+ } catch ( error ) {
464+ if ( ! ( error instanceof EnclaveClientError ) || error . statusCode !== 404 ) throw error ;
465+ if ( await restoreBackup ( store , enclave , identity . id ) ) {
466+ await enclave . destroy ( identity . id ) ;
467+ }
400468 }
401469 }
402470
0 commit comments