/** Deployment operator state. Store outside PGDATA, backups and runtime mounts. */ import assert from 'node:assert/strict' import {constants} from 'node:fs' import {lstat,open,rename} from 'node:fs/promises' import path from 'node:path' import {z} from 'zod' import {sourceBindingBytes,sourceBindingCanonical,type DatabaseSourceBindingBody} from '../../src/db/database-source-binding.js' import {protectedDirectory,readProtected,sha,withJournalLease} from './operator-files.mjs' import {readProtectedDatabaseSourceBindingRegistry, type createProtectedDatabaseSourceBindingVerifier} from './protected-source-registry.mjs' const digest=z.string().regex(/^[a-f0-9]{64}$/),uuid=z.string().uuid() const sourcePin=z.object({registryDirectory:z.string().min(1).max(4096),installationId:uuid,publicKeySpkiSha256:digest,currentHeadSha256:digest}).strict() const directoryPath=z.string().min(1).max(4096) const approval=z.object({format:z.literal('database-source-activation-approval-v1'),activationDirectory:directoryPath,sourcePin,tlsSpkiSha256:digest, expectedActivationSha256:digest.nullable()}).strict() const generation=z.object({generation:uuid,headSha256:digest,bindingSha256:digest,tlsSpkiSha256:digest}).strict() const stateSchema=z.object({format:z.literal('database-source-activation-v1'),activationDirectory:directoryPath,installationId:uuid,sourceIdentityId:uuid, publicKeySpkiSha256:digest,status:z.enum(['active','revoked']),generations:z.array(generation).min(1).max(256)}).strict() const maximum=131072, filename='source-activation.json' type State=z.infer const parse=(bytes:Buffer):unknown=>JSON.parse(new TextDecoder('utf8',{fatal:true}).decode(bytes)) async function present(file:string){ try{await lstat(file);return true}catch(error){if((error as NodeJS.ErrnoException).code==='ENOENT')return false;throw error} } // Caller holds the same protected lease used by all activation writers. async function current(directory:string){ const file=path.join(directory,filename) assert(!await present(file+'.pending'),'Unresolved activation transition') if(!await present(file))return undefined const bytes=await readProtected(file,maximum),state=stateSchema.parse(parse(bytes)) assert.equal(state.activationDirectory,directory,'Activation record belongs to another directory') assert(sourceBindingBytes(state).equals(bytes),'Noncanonical activation record') for(const field of ['generation','headSha256','tlsSpkiSha256'] as const){ assert.equal(new Set(state.generations.map(item=>item[field].toLowerCase())).size,state.generations.length,'Reused activation identity') } return {state,activationSha256:sha(bytes)} } async function persist(directory:string,state:State){ await protectedDirectory(directory) const bytes=sourceBindingBytes(stateSchema.parse(state)) assert(bytes.length<=maximum) const file=path.join(directory,filename),pending=file+'.pending' const parent=await open(directory,constants.O_RDONLY|constants.O_DIRECTORY|constants.O_NOFOLLOW) let confirmed=false,cleanupUnconfirmed=false try{ const handle=await open(pending,constants.O_WRONLY|constants.O_CREAT|constants.O_EXCL|constants.O_NOFOLLOW,0o600) try{await handle.writeFile(bytes);await handle.sync()}finally{await handle.close()} try{await rename(pending,file);await parent.sync()} catch{throw Object.assign(Error('Activation terminal requires inspection'),{code:'SOURCE_ACTIVATION_TERMINAL_UNKNOWN'})} confirmed=true }finally{try{await parent.close()}catch{cleanupUnconfirmed=confirmed}} return {activationSha256:sha(bytes),cleanupUnconfirmed} } function receipt(state:State,activationSha256:string,outcome:'committed'|'observed'){ return {event:'database-source-activation',outcome,activationSha256,status:state.status, ordinal:state.generations.length,...state.generations.at(-1),generationAuthenticated:false,readyAuthorized:false,automaticReplay:false} } type SourceVerifier=ReturnType /** Keep revocation and generation changes fenced throughout one read-only SQL preflight. */ export async function withActiveSourcePreflight(input:{directory:string,expectedSha256:string,sourcePin:z.infer,expectedSource:DatabaseSourceBindingBody['source'],tlsSpkiSha256:string,deadlineAt:number}, verifier:SourceVerifier,work:(guardedVerifier:SourceVerifier)=>Promise){ const {directory,deadlineAt}=input const expectedSha256=digest.parse(input.expectedSha256),tlsSpkiSha256=digest.parse(input.tlsSpkiSha256) const pin=sourcePin.parse(input.sourcePin) return withJournalLease(directory,async()=>{ async function readActive(){ assert(Date.now()){ await readActive() const evidence=await verifier.verify(...args) assert(evidence.platformProtectionVerified&&evidence.operatorSignatureVerified&&evidence.currentHeadPinVerified&&evidence.genuineSourceFactsMatched,'Unprotected activation source evidence') assertGeneration(evidence) await readActive() sourceChecks++ return evidence }}) const result=await work(guardedVerifier) assert.equal(sourceChecks,2,'Initial and terminal source evidence required') await readActive() return result }) } export async function sourceActivationMain(args:string[]){ const command=args[0], flags=command==='source-activate'?['directory','approval','approval-sha256']: command==='source-revoke'?['directory','expected-sha256']:['directory'] assert(['source-activate','source-revoke','source-status'].includes(command)&&args.length===1+2*flags.length) const values=new Map() for(let i=1;i{ const before=await current(directory) if(command==='source-status'){ assert(before,'Activation record absent') return receipt(before.state,before.activationSha256,'observed') } if(command==='source-revoke'){ const expected=digest.parse(values.get('expected-sha256')) assert(before?.activationSha256===expected&&before.state.status==='active','Active activation digest changed') const revoked={...before.state,status:'revoked' as const} const saved=await persist(directory,revoked) return {...receipt(revoked,saved.activationSha256,'committed'),cleanupUnconfirmed:saved.cleanupUnconfirmed} } const request=approval.parse(parse(await readProtected(values.get('approval')!,16384,digest.parse(values.get('approval-sha256'))))) assert.equal(request.activationDirectory,directory,'Approval belongs to another activation directory') assert.equal(before?.activationSha256??null,request.expectedActivationSha256,'Activation predecessor changed') const registry=await readProtectedDatabaseSourceBindingRegistry(request.sourcePin,{deadlineAt:Date.now()+120000}) const previous=before?.state.generations??[] assert.equal(registry.head.ordinal,previous.length+1,'Source ordinal changed') assert.equal(registry.head.previousHeadSha256,previous.at(-1)?.headSha256??null,'Signed predecessor differs') if(before){ assert.equal(registry.head.installationId,before.state.installationId) assert.equal(registry.head.sourceIdentityId,before.state.sourceIdentityId) assert.equal(registry.publicKeySpkiSha256,before.state.publicKeySpkiSha256) } assert(!previous.some(item=>item.generation.toLowerCase()===registry.head.generation.toLowerCase()||item.tlsSpkiSha256===request.tlsSpkiSha256),'Generation or TLS key was already used') const next:State={format:'database-source-activation-v1',activationDirectory:directory,installationId:registry.head.installationId, sourceIdentityId:registry.head.sourceIdentityId,publicKeySpkiSha256:registry.publicKeySpkiSha256,status:'active', generations:[...previous,{generation:registry.head.generation,headSha256:registry.headSha256, bindingSha256:registry.head.bindingSha256,tlsSpkiSha256:request.tlsSpkiSha256}]} const saved=await persist(directory,next) return {...receipt(next,saved.activationSha256,'committed'),cleanupUnconfirmed:saved.cleanupUnconfirmed} }) }