import fs from 'node:fs'; import path from 'node:path'; import os from 'node:os'; import crypto from 'node:crypto'; import {spawnSync} from 'node:child_process'; import {fileURLToPath} from 'node:url'; import {decryptEnvelope,runReadControls} from './decrypt-envelope.mjs'; import {COMPANY,APP,AGENT,HOST,SKILL,PURPOSE,WRAPPER_SHA,DECRYPTOR_SHA,SCOPES,DELIVERY_FILES, sha,encode,need,plain,privateDirectory,readPrivate,readDelivery,writeNew,argsOf,errorReceipt} from './common.mjs'; const self=fileURLToPath(import.meta.url); export function validateDelivery(bundle,manifestSha){ const {files,manifest}=readDelivery(bundle,manifestSha); const release=JSON.parse(files['RELEASE.json']),binding=JSON.parse(files['credential-binding.json']); need(release.schema==='ksf.enterprise-workshop-authorization.v1'&&release.companyObjectId===COMPANY &&release.appId===APP&&release.agentId===AGENT&&release.host===HOST,'DELIVERY_IDENTITY_MISMATCH'); need(/^[A-Za-z0-9_-]{1,80}$/.test(release.credentialId||'') &&release.installDirectory==='/opt/data/ksf-enterprise-auth-'+release.credentialId &&release.skillDirectory===SKILL&&release.wrapperSha256===WRAPPER_SHA,'INVALID_INSTALL_TARGET'); need(JSON.stringify(release.scopes)===JSON.stringify(SCOPES)&&JSON.stringify(release.projectIds)==='["*"]' &&release.formalReportPublicationGranted===false&&release.credentialPlaintextIncluded===false &&release.privateKeyIncluded===false,'DELIVERY_PERMISSION_MISMATCH'); need(binding.version==='ksf.encrypted-entry.v1'&&binding.purpose===PURPOSE&&binding.host===HOST &&/^\/opt\/data\/ksf-key-receive-20261031-[A-Za-z0-9]+$/.test(binding.keyDirectory) &&binding.keyDirectory===release.keyDirectory&&binding.publicKeySha256===release.publicKeySha256,'INVALID_BINDING'); need(binding.envelopeFile===release.installDirectory+'/credential-envelope.json' &&binding.decryptModule===release.installDirectory+'/decrypt-envelope.mjs' &&binding.envelopeSha256===sha(files['credential-envelope.json']) &&binding.decryptModuleSha256===DECRYPTOR_SHA&&sha(files['decrypt-envelope.mjs'])===DECRYPTOR_SHA,'BINDING_HASH_MISMATCH'); const envelope=JSON.parse(files['credential-envelope.json']); need(envelope.header?.recipientHost===HOST&&envelope.header?.recipientDirectory===binding.keyDirectory &&envelope.header?.publicKeySha256===binding.publicKeySha256&&envelope.header?.expiresAt===release.expiresAt,'ENVELOPE_BINDING_MISMATCH'); return{files,manifest,release,binding,envelope}; } export async function install({bundle,manifestSha,apply=false},{dataDir='/opt/data',hostname=os.hostname(), platform=process.platform,spawn=spawnSync,controls=runReadControls}={}){ need(platform==='linux','LINUX_REQUIRED');need(hostname===HOST,'WRONG_CONTAINER'); dataDir=plain(dataDir);const local=target=>path.join(dataDir,...target.slice('/opt/data/'.length).split('/')); const current=JSON.parse(readPrivate(path.join(dataDir,'.fmode/config/enterprise/current.json'))); need(current.appId===APP&¤t.companyObjectId===COMPANY&¤t.provisionStatus==='success','ENTERPRISE_MISMATCH'); const {files,manifest,release,binding,envelope}=validateDelivery(bundle,manifestSha); const skill=local(SKILL),entry=path.join(skill,'scripts/ksf-secure.mjs'); need(sha(fs.readFileSync(entry))===WRAPPER_SHA,'WRAPPER_CHANGED'); need(fs.readFileSync(path.join(skill,'SKILL.md'),'utf8').includes('version: r13'),'SKILL_VERSION_MISMATCH'); const keyDirectory=local(binding.keyDirectory);privateDirectory(keyDirectory); const privateBytes=readPrivate(path.join(keyDirectory,'recipient-private.pem')); let key;try{key=crypto.createPrivateKey(privateBytes);}finally{privateBytes.fill(0);} const payload=decryptEnvelope(envelope,key,{host:HOST,directory:binding.keyDirectory,publicKeySha256:binding.publicKeySha256}); try{ need(payload.credentialId===release.credentialId&&payload.expiresAt===release.expiresAt,'CREDENTIAL_IDENTITY_MISMATCH'); const bindingTarget=path.join(skill,'credential-binding.json'),destination=local(release.installDirectory); const alreadyBound=fs.existsSync(bindingTarget); if(alreadyBound)need(readPrivate(bindingTarget).equals(files['credential-binding.json']),'EXISTING_BINDING_CHANGED'); if(fs.existsSync(destination)){ privateDirectory(destination); need(fs.readdirSync(destination).every(n=>[...DELIVERY_FILES,'FILES.sha256'].includes(n)),'UNKNOWN_INSTALL_FILES'); for(const name of fs.readdirSync(destination))need(readPrivate(path.join(destination,name)).equals( name==='FILES.sha256'?manifest:files[name]),'EXISTING_AUTH_FILE_CHANGED'); } const base={ok:true,planOnly:!apply,credentialId:release.credentialId,host:HOST, scopes:release.scopes,alreadyBound,bundleManifestSha256:manifestSha, privateKeyExported:false,credentialPlaintextWrittenToDisk:false,protectedRuntimeFilesModified:0,businessRecordWrites:0}; if(!apply)return{...base,bindingInstalled:alreadyBound}; fs.mkdirSync(destination,{recursive:true,mode:0o700});privateDirectory(destination); for(const[name,body]of [...Object.entries(files),['FILES.sha256',manifest]]){ const target=path.join(destination,name);if(!fs.existsSync(target))writeNew(target,body); } if(!alreadyBound)writeNew(bindingTarget,files['credential-binding.json']); // Exercise the frozen production entry; do not replace it with a direct API success claim. // Keep verification input outside the sealed authorization directory. const input=path.join(dataDir,'ksf-enterprise-auth-checks',crypto.randomUUID()+'.json'); writeNew(input,Buffer.from('{}\n')); const child=spawn(process.execPath,[entry,'workshop-contract','--input',input,'--request-id','enterprise-auth-'+crypto.randomUUID()], {encoding:'utf8',timeout:120000,maxBuffer:4*1024*1024,windowsHide:true}); need(!String(child.stdout).includes(payload.env.KSF_SERVICE_TOKEN) &&!String(child.stderr).includes(payload.env.KSF_SERVICE_TOKEN),'OUTPUT_CONTAINS_SECRET'); need(!child.error&&child.status===0,'FROZEN_ENTRY_CONTRACT_FAILED'); const result=JSON.parse(child.stdout); need(result.ok===true&&result.wrapperSha256===WRAPPER_SHA&&result.credentialLoadedForChild===true &&result.credentialWrittenToDisk===false&&result.entryResult?.readOnly===true &&result.entryResult?.result?.workshop?.version==='ksf.workshop.v1','FROZEN_ENTRY_CONTRACT_MISMATCH'); const authControls=await controls(payload.env,'enterprise-auth-'+crypto.randomUUID()); need(sha(fs.readFileSync(entry))===WRAPPER_SHA,'WRAPPER_CHANGED'); return{...base,bindingInstalled:true,workshopContractVerified:true,authorizationControls:authControls, researchEnvironmentActivated:false,studentContextVerified:false,saveReadbackVerified:false}; }finally{delete payload.env.KSF_SERVICE_TOKEN;} } if(process.argv[1]&&path.resolve(process.argv[1])===self){ try{ const a=argsOf(process.argv.slice(2),['--bundle','--sha256','--mode']); need(a['--bundle']&&a['--sha256']&&['plan','apply'].includes(a['--mode']),'USAGE'); console.log(JSON.stringify(await install({bundle:a['--bundle'],manifestSha:a['--sha256'],apply:a['--mode']==='apply'}),null,2)); }catch(error){console.error(JSON.stringify(errorReceipt(error)));process.exitCode=1;} }