package-workshop-encrypted-renewal.mjs 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119
  1. import fs from 'node:fs';
  2. import path from 'node:path';
  3. import crypto from 'node:crypto';
  4. import { fileURLToPath } from 'node:url';
  5. import { sealCredential, sha256 } from './lib/workshop-credential-envelope.mjs';
  6. import { request, invoke, appId, functionsUrl } from '../server/scripts/platform-client.mjs';
  7. const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
  8. const LONGTERM = process.argv[2] === '--longterm';
  9. const RECEIVED = path.join(ROOT, 'work/workshop-auth-envelope-recovery-20261108-r2/received');
  10. const RELEASE_NAME = LONGTERM ? '20261108-r3-longterm' : '20261108-r2';
  11. const PACKAGE = path.join(ROOT, 'artifacts/ksf-workshop-auth-envelope-renewal-' + RELEASE_NAME + '/package');
  12. const must = (condition, code) => { if (!condition) throw Object.assign(new Error(code), { code }); };
  13. const readJson = file => JSON.parse(fs.readFileSync(file, 'utf8'));
  14. const EXPECTED_SCOPES = ['source:write', 'ingest:write', 'data:read', 'report:ingest', 'report:publish', 'noise:write', 'noise:read'];
  15. async function main() {
  16. must(process.argv.length <= 3 && (!process.argv[2] || LONGTERM), 'USAGE');
  17. const manifest = readJson(path.join(RECEIVED, 'manifest.json'));
  18. const tar = fs.readFileSync(path.join(RECEIVED, 'wg-auth-return-5files.tar.gz'));
  19. must(tar.length === 5527 && sha256(tar) === 'c6e54d85bfec49a9a35def804d24d7e056b71e41239f4bb5c55fce7554dfbaad', 'RETURN_ARCHIVE_MISMATCH');
  20. for (const item of manifest.files) {
  21. must(!item.path.includes('..') && !path.isAbsolute(item.path), 'UNSAFE_RETURN_PATH');
  22. const bytes = fs.readFileSync(path.join(RECEIVED, 'files', item.path));
  23. must(bytes.length === item.bytes && sha256(bytes) === item.sha256, 'RETURN_MEMBER_MISMATCH');
  24. }
  25. const recipient = readJson(path.join(RECEIVED, 'files/public-recipient/recipient.json'));
  26. const publicPem = fs.readFileSync(path.join(RECEIVED, 'files/public-recipient/recipient-public.pem'));
  27. const publicKey = crypto.createPublicKey(publicPem);
  28. must(recipient.host === '043bb9231c09' && recipient.publicKeySha256 === '1dbefc093985a077fdd7f7433bb024e93e69bf69e89ae8f7e25f749819025bae'
  29. && sha256(publicKey.export({ type: 'spki', format: 'der' })) === recipient.publicKeySha256, 'RECIPIENT_MISMATCH');
  30. must(recipient.bindingSha256 === 'e2c8455e31525d292d84cb85ea9e2a61434b596788d13ef731b2fd4703ff6758'
  31. && recipient.decryptModuleSha256 === '43e462c21053c2829402e218b05aaa4438aed9552f561d54582675e687a63f79', 'BINDING_EVIDENCE_MISMATCH');
  32. let baseline = { expectedBindingSha256: recipient.bindingSha256,
  33. oldEnvelopeFile: recipient.envelopeFile, oldEnvelopeSha256: recipient.envelopeSha256 };
  34. if (LONGTERM) {
  35. const installedReceiptBytes = fs.readFileSync(path.join(ROOT, 'docs/workshop-auth-recovery-20261108-r2/receipts/agent-auth-receipt-r2.json'));
  36. must(sha256(installedReceiptBytes) === '1c5b466c538e7e62dbda56d2d6df50f89238308a1c64de3aae8f39f48bdc99cd', 'INSTALLED_RECEIPT_MISMATCH');
  37. const installedReceipt = JSON.parse(installedReceiptBytes);
  38. const previousRelease = readJson(path.join(ROOT, 'artifacts/ksf-workshop-auth-envelope-renewal-20261108-r2/package/RELEASE.json'));
  39. must(installedReceipt.ok === true && installedReceipt.agentEncryptedEntryVerified === true && installedReceipt.host === recipient.host
  40. && installedReceipt.bindingSha256 === 'f4dac8ab49032481bcf0ab8115e126bb2563df8ac0e8aeb64ac8a23473347e29', 'PREVIOUS_INSTALL_NOT_VERIFIED');
  41. baseline = { expectedBindingSha256: installedReceipt.bindingSha256,
  42. oldEnvelopeFile: previousRelease.installEnvelope, oldEnvelopeSha256: previousRelease.envelopeSha256 };
  43. }
  44. const env = {};
  45. for (const line of fs.readFileSync('F:/文档/康师傅数字人受限连接.env', 'utf8').split(/\r?\n/)) {
  46. const m = line.match(/^(KSF_SERVICE_TOKEN|KSF_FUNCTIONS_URL|KSF_PARSE_APP_ID)=(.*)$/);
  47. if (m) env[m[1]] = m[2].trim();
  48. }
  49. must(env.KSF_SERVICE_TOKEN && env.KSF_FUNCTIONS_URL === functionsUrl && env.KSF_PARSE_APP_ID === appId, 'SERVICE_CONFIG_MISMATCH');
  50. const result = await request('GET', '/classes/KsfServiceCredential?' + new URLSearchParams({ where: JSON.stringify({ tokenHash: sha256(env.KSF_SERVICE_TOKEN) }), limit: '2' }));
  51. must(result.results?.length === 1, 'CREDENTIAL_IDENTITY_MISMATCH');
  52. const credential = result.results[0], expiresAt = credential.expiresAt?.iso || credential.expiresAt;
  53. must(credential.objectId === 'MqKRUv9JQc' && credential.enabled === true && Date.parse(expiresAt) > Date.now(), 'CREDENTIAL_NOT_ACTIVE');
  54. must(JSON.stringify(credential.scopes) === JSON.stringify(EXPECTED_SCOPES) && JSON.stringify(credential.projectIds) === '["*"]', 'CREDENTIAL_SCOPE_CHANGED');
  55. must(expiresAt === (LONGTERM ? '9999-12-31T00:00:00.000Z' : '2026-11-15T01:48:23.854Z'), 'UNEXPECTED_SERVER_EXPIRY');
  56. const live = await invoke('ksfGetIngestContract', { requestId: 'auth-envelope-package-20261108-r2' }, {
  57. 'Content-Type': 'application/json', 'X-Parse-Application-Id': appId, 'X-Ksf-Service-Token': env.KSF_SERVICE_TOKEN,
  58. });
  59. must(live.http === 200 && live.body.result?.ok === true, 'RESTRICTED_SERVICE_RECHECK_FAILED');
  60. fs.mkdirSync(PACKAGE, { recursive: true });
  61. const envelopePath = path.join(PACKAGE, 'credential-envelope.json');
  62. let envelope;
  63. if (fs.existsSync(envelopePath)) {
  64. envelope = readJson(envelopePath);
  65. must(envelope.header?.recipientHost === recipient.host && envelope.header?.expiresAt === expiresAt
  66. && envelope.header?.publicKeySha256 === recipient.publicKeySha256, 'EXISTING_PACKAGE_CONFLICT');
  67. } else {
  68. envelope = sealCredential({ publicKey: publicPem, host: recipient.host, directory: recipient.keyDirectory,
  69. credentialId: credential.objectId, expiresAt, env });
  70. fs.writeFileSync(envelopePath, JSON.stringify(envelope, null, 2) + '\n', { flag: 'wx', mode: 0o600 });
  71. }
  72. const wrapperSha256 = sha256(fs.readFileSync(path.join(ROOT, 'skills/ksf-workshop-runtime/scripts/ksf-secure.mjs')));
  73. must(wrapperSha256 === manifest.originalExecution.wrapperSha256, 'WRAPPER_BASELINE_MISMATCH');
  74. const release = {
  75. schema: 'ksf.workshop.encrypted-authorization-release.v1', release: RELEASE_NAME, builtAt: new Date().toISOString(),
  76. host: recipient.host, executionUid: 10000, containerNameObserved: manifest.instance.container,
  77. skillDirectory: '/opt/data/skills/ksf-workshop-runtime', wrapperSha256,
  78. ...baseline,
  79. keyDirectory: recipient.keyDirectory, publicKeySha256: recipient.publicKeySha256,
  80. decryptModule: recipient.decryptModule, decryptModuleSha256: recipient.decryptModuleSha256,
  81. credentialId: credential.objectId, expiresAt, envelopeSha256: sha256(fs.readFileSync(envelopePath)),
  82. installDirectory: '/opt/data/ksf-workshop-auth-envelope-' + RELEASE_NAME,
  83. installEnvelope: '/opt/data/ksf-workshop-auth-envelope-' + RELEASE_NAME + '/credential-envelope.json',
  84. bindingFieldsToChange: ['envelopeFile', 'envelopeSha256'],
  85. restrictedServiceProbe: { checkedAt: new Date().toISOString(), http: live.http, ok: true,
  86. allowedClassCount: live.body.result.data?.allowedClasses?.length },
  87. serviceIdentityPreserved: true, tokenPreserved: true, scopesPreserved: true, privateKeyIncluded: false,
  88. plaintextCredentialIncluded: false, businessRecordWrites: 0, serverAuthorizationRecordWritesDuringPackaging: 0, paidCalls: 0,
  89. agentEncryptedEntryVerified: false, actualBusinessWriteVerified: false,
  90. authorizationMode: LONGTERM ? 'long-term-with-explicit-expiry' : 'seven-day-renewal', revocable: true,
  91. };
  92. fs.writeFileSync(path.join(PACKAGE, 'RELEASE.json'), JSON.stringify(release, null, 2) + '\n');
  93. fs.copyFileSync(path.join(ROOT, 'scripts/restore-workshop-credential-binding.mjs'), path.join(PACKAGE, 'restore-workshop-credential-binding.mjs'));
  94. fs.copyFileSync(path.join(ROOT, LONGTERM ? 'docs/workshop-longterm-authorization-20261108-r3/发给运维001-长期授权.md'
  95. : 'docs/workshop-auth-recovery-20261108-r2/发给运维001-加密授权更新.md'), path.join(PACKAGE, 'README.md'));
  96. const verification = readJson(path.join(ROOT, 'work/workshop-auth-envelope-recovery-20261108-r2/'
  97. + (LONGTERM ? 'local-verification-longterm.json' : 'local-verification.json')));
  98. must(verification.ok === true && verification.cryptoTestsPassed === (LONGTERM ? 9 : 8), 'LOCAL_VERIFICATION_REQUIRED');
  99. fs.writeFileSync(path.join(PACKAGE, 'local-verification.json'), JSON.stringify(verification, null, 2) + '\n');
  100. const files = fs.readdirSync(PACKAGE).filter(name => name !== 'FILES.sha256').sort();
  101. const lines = [];
  102. for (const name of files) {
  103. const bytes = fs.readFileSync(path.join(PACKAGE, name));
  104. must(!bytes.includes(Buffer.from(env.KSF_SERVICE_TOKEN)) && !bytes.includes(Buffer.from('-----BEGIN PRIVATE KEY-----')), 'PACKAGE_CONTAINS_PLAINTEXT_SECRET');
  105. lines.push(sha256(bytes) + ' ' + name);
  106. }
  107. fs.writeFileSync(path.join(PACKAGE, 'FILES.sha256'), lines.join('\n') + '\n');
  108. delete env.KSF_SERVICE_TOKEN;
  109. console.log(JSON.stringify({ ok: true, packageDirectory: PACKAGE, files: files.length, envelopeSha256: release.envelopeSha256,
  110. credentialExpiresAt: expiresAt, publicRecipientVerified: true, plaintextSecretScanPassed: true, businessRecordWrites: 0, paidCalls: 0 }, null, 2));
  111. }
  112. main().catch(error => {
  113. console.error(JSON.stringify({ ok: false, code: /^[A-Z][A-Z0-9_]+$/.test(error.code || '') ? error.code : 'ENCRYPTED_RENEWAL_PACKAGE_STOPPED',
  114. message: '封装停止;未输出令牌、管理配置或原始异常。' }));
  115. process.exitCode = 1;
  116. });