| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119 |
- import fs from 'node:fs';
- import path from 'node:path';
- import crypto from 'node:crypto';
- import {fileURLToPath} from 'node:url';
- import {sealCredential} from '../lib/workshop-credential-envelope.mjs';
- import {COMPANY,APP,AGENT,HOST,SKILL,PURPOSE,WRAPPER_SHA,DECRYPTOR_SHA,SCOPES,sha,encode,need,plain,readPrivate,readDelivery,privateDirectory,writeNew,recipientInfo,argsOf,errorReceipt} from './common.mjs';
- const self=fileURLToPath(import.meta.url);
- const expiry='9999-12-31T00:00:00.000Z';
- const expiredAt=row=>row.expiresAt?.iso||row.expiresAt;
- const publicRow=row=>({credentialId:row.objectId,label:row.label,enabled:row.enabled,expiresAt:expiredAt(row),scopes:row.scopes,projectIds:row.projectIds});
- function saveJournal(output,value){
- const dest=path.join(output,'issuance-state.json'),temp=dest+'.'+crypto.randomUUID();
- writeNew(temp,encode(value));fs.renameSync(temp,dest);
- }
- function checkRow(row,expected){
- need(row?.objectId&&row.label===expected.label&&row.tokenHash===expected.tokenHash
- &&JSON.stringify(row.scopes)===JSON.stringify(SCOPES)&&JSON.stringify(row.projectIds)==='["*"]'
- &&expiredAt(row)===expiry,'ISSUED_CREDENTIAL_READBACK_MISMATCH');
- }
- export async function issue({recipientFile,output,apply=false},{request,invoke}={}){
- recipientFile=plain(recipientFile);output=plain(output);
- need(!recipientFile.startsWith(output+path.sep)&&recipientFile!==output,'OUTPUT_OVERLAPS_RECIPIENT');
- const recipientBytes=fs.readFileSync(recipientFile),recipient=recipientInfo(JSON.parse(recipientBytes));
- const label='ksf-enterprise:'+COMPANY+':'+AGENT+':'+recipient.publicKeySha256;
- const query=async()=> (await request('GET','/classes/KsfServiceCredential?'+new URLSearchParams({where:JSON.stringify({label}),limit:'2'}))).results;
- const rows=await query();need(Array.isArray(rows)&&rows.length<=1,'DUPLICATE_SERVICE_CREDENTIAL');
- const journalPath=path.join(output,'issuance-state.json');
- if(rows.length){
- need(fs.existsSync(journalPath),'EXISTING_SERVICE_CREDENTIAL_REQUIRES_RECONCILIATION');
- privateDirectory(output);const state=JSON.parse(readPrivate(journalPath));
- need(state.phase==='complete'&&state.recipientSha256===sha(recipientBytes),'INCOMPLETE_ISSUANCE_REQUIRES_RECONCILIATION');
- checkRow(rows[0],state);need(rows[0].enabled===true,'ISSUED_CREDENTIAL_DISABLED');
- const bundle=path.join(output,'delivery');
- readDelivery(bundle,state.bundleManifestSha256);
- return{ok:true,alreadyIssued:true,...publicRow(rows[0]),deliveryDirectory:bundle,
- bundleManifestSha256:state.bundleManifestSha256,
- credentialPlaintextWrittenToDisk:false,authorizationRecordWrites:0,businessRecordWrites:0};
- }
- const plan={schema:'ksf.enterprise-workshop-issuance-plan.v1',companyObjectId:COMPANY,appId:APP,
- host:HOST,agentId:AGENT,label,recipientSha256:sha(recipientBytes),publicKeySha256:recipient.publicKeySha256,
- scopes:SCOPES,projectIds:['*'],expiresAt:expiry,formalReportPublicationGranted:false};
- if(!apply)return{ok:true,planOnly:true,...plan,authorizationRecordWrites:0,businessRecordWrites:0};
- need(!fs.existsSync(output),'NEW_PRIVATE_OUTPUT_REQUIRED');
- fs.mkdirSync(output,{recursive:true,mode:0o700});
- privateDirectory(output);
- const bytes=crypto.randomBytes(32),token=bytes.toString('base64url');bytes.fill(0);
- const expected={label,tokenHash:sha(token)},state={...plan,...expected,phase:'prepared'};
- saveJournal(output,state);
- const env={KSF_FUNCTIONS_URL:'https://kangshifu.fmode.cn/api/functions',KSF_PARSE_APP_ID:APP,KSF_SERVICE_TOKEN:token};
- try{
- const created=await request('POST','/classes/KsfServiceCredential',{
- ...expected,scopes:SCOPES,projectIds:['*'],enabled:false,expiresAt:{__type:'Date',iso:expiry},ACL:{}});
- need(/^[A-Za-z0-9_-]{1,80}$/.test(created.objectId||''),'CREDENTIAL_CREATE_RESULT_UNCERTAIN');
- state.credentialId=created.objectId;state.phase='disabled-record-created';saveJournal(output,state);
- let row=await request('GET','/classes/KsfServiceCredential/'+created.objectId);checkRow(row,expected);
- need(row.enabled===false,'CREDENTIAL_ENABLED_TOO_EARLY');
- const envelope=sealCredential({publicKey:recipient.publicKeyPem,host:HOST,directory:recipient.keyDirectory,
- credentialId:created.objectId,expiresAt:expiry,env});
- const envelopeBytes=encode(envelope);
- const decryptor=fs.readFileSync(path.join(path.dirname(self),'decrypt-envelope.mjs'));
- need(sha(decryptor)===DECRYPTOR_SHA,'DECRYPTOR_CHANGED');
- const installDirectory='/opt/data/ksf-enterprise-auth-'+created.objectId;
- const binding={version:'ksf.encrypted-entry.v1',purpose:PURPOSE,host:HOST,
- envelopeFile:installDirectory+'/credential-envelope.json',envelopeSha256:sha(envelopeBytes),
- keyDirectory:recipient.keyDirectory,publicKeySha256:recipient.publicKeySha256,
- decryptModule:installDirectory+'/decrypt-envelope.mjs',decryptModuleSha256:sha(decryptor)};
- const release={schema:'ksf.enterprise-workshop-authorization.v1',companyObjectId:COMPANY,appId:APP,
- agentId:AGENT,host:HOST,credentialId:created.objectId,scopes:SCOPES,projectIds:['*'],expiresAt:expiry,
- installDirectory,skillDirectory:SKILL,wrapperSha256:WRAPPER_SHA,
- recipientSha256:sha(recipientBytes),keyDirectory:recipient.keyDirectory,publicKeySha256:recipient.publicKeySha256,
- credentialPlaintextIncluded:false,privateKeyIncluded:false,formalReportPublicationGranted:false};
- const files={'credential-envelope.json':envelopeBytes,'credential-binding.json':encode(binding),
- 'decrypt-envelope.mjs':decryptor,'RELEASE.json':encode(release)};
- const bundle=path.join(output,'delivery');
- for(const[n,body]of Object.entries(files)){
- need(!body.includes(Buffer.from(token))&&!body.includes(Buffer.from('-----BEGIN PRIVATE KEY-----')),'SECRET_IN_DELIVERY');
- writeNew(path.join(bundle,n),body);
- }
- writeNew(path.join(bundle,'FILES.sha256'),Buffer.from(Object.entries(files).map(([n,b])=>sha(b)+' '+n+'\n').join('')));
- state.bundleManifestSha256=sha(fs.readFileSync(path.join(bundle,'FILES.sha256')));
- readDelivery(bundle,state.bundleManifestSha256);
- state.phase='encrypted-delivery-prepared';saveJournal(output,state);
- // The recoverable encrypted delivery is on disk before activating the new credential.
- await request('PUT','/classes/KsfServiceCredential/'+created.objectId,{enabled:true});
- state.phase='enabled-awaiting-controls';saveJournal(output,state);
- row=await request('GET','/classes/KsfServiceCredential/'+created.objectId);checkRow(row,expected);
- need(row.enabled===true,'CREDENTIAL_ENABLE_READBACK_FAILED');
- const headers={'Content-Type':'application/json','X-Parse-Application-Id':APP,'X-Ksf-Service-Token':token};
- const contract=await invoke('ksfGetIngestContract',{},headers);
- need(contract.http===200&&contract.body.result?.ok===true
- &&contract.body.result.data?.workshop?.version==='ksf.workshop.v1','ISSUED_CONTRACT_FAILED');
- const probe=await invoke('ksfGetRecord',{className:'KfSource',objectId:'auth-check-'+crypto.randomUUID()},headers);
- need(probe.http===400&&probe.body.result?.errors?.some(e=>e.code==='REFERENCE_NOT_FOUND'),'ISSUED_AUTH_CONTROL_FAILED');
- for(const controlToken of [null,'invalid-control-'+crypto.randomBytes(24).toString('base64url')]){
- const controlHeaders={'Content-Type':'application/json','X-Parse-Application-Id':APP};
- if(controlToken)controlHeaders['X-Ksf-Service-Token']=controlToken;
- const control=await invoke('ksfGetRecord',{className:'KfSource',objectId:'auth-check-'+crypto.randomUUID()},controlHeaders);
- need(control.http===401&&control.body.result?.errors?.some(e=>e.code==='AUTH_REQUIRED'),'ISSUED_AUTH_CONTROL_FAILED');
- }
- state.phase='complete';saveJournal(output,state);
- const receipt={ok:true,alreadyIssued:false,...publicRow(row),deliveryDirectory:bundle,
- bundleManifestSha256:state.bundleManifestSha256,
- contractVerified:true,readAuthorizationVerified:true,authorizationRecordWrites:2,businessRecordWrites:0,
- credentialPlaintextWrittenToDisk:false,targetBindingInstalled:false};
- writeNew(path.join(output,'issuance-receipt.json'),encode(receipt));
- return receipt;
- }catch(error){state.lastErrorCode=/^[A-Z][A-Z0-9_]+$/.test(error.code||'')?error.code:'ISSUANCE_STOPPED';
- saveJournal(output,state);throw error;
- }finally{delete env.KSF_SERVICE_TOKEN;}
- }
- if(process.argv[1]&&path.resolve(process.argv[1])===self){
- try{
- const a=argsOf(process.argv.slice(2),['--recipient','--out','--mode']);
- need(a['--recipient']&&a['--out']&&['plan','apply'].includes(a['--mode']),'USAGE');
- const client=await import('../../server/scripts/platform-client.mjs');
- console.log(JSON.stringify(await issue({recipientFile:a['--recipient'],output:a['--out'],apply:a['--mode']==='apply'},client),null,2));
- }catch(error){console.error(JSON.stringify(errorReceipt(error)));process.exitCode=1;}
- }
|