feat(saas): enforce signed release evidence and verified deployments

This commit is contained in:
2026-09-11 12:11:33 +08:00
parent 139386e8cd
commit d0605ab8f8
9 changed files with 562 additions and 118 deletions

View File

@@ -3,7 +3,7 @@
// Starts an already-built, frozen directory. There is intentionally no build,
// codegen, Git checkout, publish, or fallback-to-RC operation in this entrypoint.
const { createHash } = require('node:crypto');
const { readdirSync, lstatSync, readFileSync } = require('node:fs');
const { readdirSync, lstatSync, readFileSync, realpathSync } = require('node:fs');
const path = require('node:path');
const { spawnSync } = require('node:child_process');
@@ -96,31 +96,39 @@ async function fetchReleaseEvidence(environment, fetcher = fetch) {
return body.data;
}
function verifyStableEvidence({ release, lock, generated, digest, commit, releaseId }) {
function verifyStableEvidence({ release, lock, generated, digest, commit, releaseId, mode = 'production' }) {
const build = release?.build;
const artifact = release?.frozenArtifact;
ensure(release?.id === Number(releaseId) && release.releaseType === 'stable' && release.status === 'published' && release.lifecycleVersion === 1, 'target is not a published lifecycle Stable release');
ensure(STABLE.test(release.version || '') && release.serviceVersion === release.version, 'Stable version identity is invalid');
ensure(build && artifact && Number(release.promotedFromReleaseId) === Number(artifact.rcReleaseId) && Number(release.frozenArtifactId) === Number(artifact.id), 'Stable does not reference its source RC frozen artifact');
const stable = mode === 'production';
ensure(['production', 'frozen'].includes(mode), 'unsupported artifact runtime mode');
ensure(release?.id === Number(releaseId) && release.status === 'published' && release.lifecycleVersion === 1 &&
(stable ? release.releaseType === 'stable' : release.releaseType === 'rc' && ['frozen', 'accepted', 'promoted'].includes(release.candidateStatus) && release.releaseEvidenceVersion === 2), 'target is not a published lifecycle Stable release or explicit V2 frozen RC');
const targetVersion = stable ? release.version : release.targetVersion;
ensure(STABLE.test(targetVersion || '') && release.serviceVersion === release.version, 'Stable version identity is invalid');
ensure(build && artifact && Number(stable ? release.promotedFromReleaseId : release.id) === Number(artifact.rcReleaseId) && Number(release.frozenArtifactId) === Number(artifact.id), 'Stable does not reference its source RC frozen artifact');
ensure(Number(build.id) === Number(release.buildSnapshotId) && Number(build.moduleId) === Number(release.moduleId) && Number(artifact.moduleId) === Number(release.moduleId), 'Build/module/artifact association differs');
ensure(Number(build.revision) === Number(lock.buildRevision) && Number(release.buildRevision) === Number(lock.buildRevision) && Number(artifact.buildRevision) === Number(lock.buildRevision), 'Build Revision differs from the Candidate lock');
ensure(SHA256.test(build.buildChecksum || '') && build.buildChecksum === lock.buildChecksum && build.schemaInputChecksum === lock.schemaInputChecksum, 'Build checksums differ from the Candidate lock');
ensure(lock.candidateVersion === `${release.version}-rc.${lock.buildRevision}` && lock.branchName === `release/${lock.candidateVersion}` && artifact.sourceBranch === lock.branchName && artifact.tagName === `v${lock.candidateVersion}`, 'source Candidate branch/version/tag differs');
ensure(lock.candidateVersion === `${targetVersion}-rc.${lock.buildRevision}` && lock.branchName === `release/${lock.candidateVersion}` && artifact.sourceBranch === lock.branchName && artifact.tagName === `v${lock.candidateVersion}` && (stable || release.version === lock.candidateVersion), 'source Candidate branch/version/tag differs');
ensure(candidateLockChecksum(lock) === artifact.generatedLockChecksum, 'generated Candidate lock checksum differs');
ensure(COMMIT.test(artifact.sourceCommit || '') && artifact.sourceCommit === commit, 'source commit differs from the Frozen Artifact');
ensure(/^sha256:[a-f0-9]{64}$/.test(artifact.artifactDigest || ''), 'Frozen Artifact has no compiled digest; old Git-only evidence cannot be upgraded or forged here');
ensure(artifact.artifactDigest === digest, 'compiled artifact digest differs from the Frozen Artifact');
ensure(SHA256.test(artifact.artifactIdentityChecksum || '') && SHA256.test(artifact.implementationManifestChecksum || ''), 'frozen implementation identity is incomplete');
ensure(generated.moduleCode === lock.moduleCode && generated.revision === lock.buildRevision && generated.schemaInputChecksum === lock.schemaInputChecksum, 'compiled generated identity differs from the immutable Build');
ensure(!generated.releaseVersion || generated.releaseVersion === release.version, 'generated runtime embeds a different Release version');
ensure(!generated.releaseVersion || generated.releaseVersion === targetVersion, 'generated runtime embeds a different Release version');
ensure(!generated.implementationManifestChecksum || generated.implementationManifestChecksum === artifact.implementationManifestChecksum, 'compiled implementation checksum differs');
const manifest = release.schemaSnapshot?.actionManifest;
const contracts = release.schemaSnapshot?.runtimeContracts;
ensure(manifest && contracts?.implementationManifestChecksum === artifact.implementationManifestChecksum && manifest.artifactDigest === artifact.artifactDigest, 'Stable release envelope differs from the frozen implementation/artifact');
// A frozen RC keeps its original immutable envelope. Its actual bytes are
// selected by the separately signed Frozen Artifact; Stable's new envelope
// must explicitly reuse that digest.
ensure(manifest && contracts?.implementationManifestChecksum === artifact.implementationManifestChecksum &&
(stable ? manifest.artifactDigest === artifact.artifactDigest : /^sha256:[a-f0-9]{64}$/.test(manifest.artifactDigest || '')), 'Stable release envelope differs from the frozen implementation/artifact');
ensure(generated.runtimeContractChecksum === contracts.runtimeContractChecksum && SHA256.test(contracts.runtimeContractChecksum || ''), 'compiled Runtime contract checksum differs');
ensure(generated.schemaVersion === release.schemaVersion && build.schemaVersion === release.schemaVersion, 'compiled Schema version differs');
return {
SAAS_RUNTIME_MODE: 'production', RPC_REQUIRE_EXACT_RELEASE: 'true',
SAAS_RUNTIME_MODE: mode, RPC_REQUIRE_EXACT_RELEASE: 'true',
RPC_MODULE_CODE: lock.moduleCode, RPC_RELEASE_ID: String(release.id),
RPC_RELEASE_VERSION: release.version, RPC_SERVICE_VERSION: release.serviceVersion,
SAAS_BUILD_REVISION: String(build.revision), SAAS_BUILD_CHECKSUM: build.buildChecksum,
@@ -150,22 +158,62 @@ function verifyBootstrapSession(session, exact) {
releaseId: 'RPC_RELEASE_ID', releaseVersion: 'RPC_RELEASE_VERSION', serviceVersion: 'RPC_SERVICE_VERSION',
runtimeMode: 'SAAS_RUNTIME_MODE', buildRevision: 'SAAS_BUILD_REVISION', buildChecksum: 'SAAS_BUILD_CHECKSUM',
schemaInputChecksum: 'SAAS_SCHEMA_INPUT_CHECKSUM', implementationManifestChecksum: 'RPC_IMPLEMENTATION_MANIFEST_CHECKSUM',
artifactDigest: 'RPC_ARTIFACT_DIGEST', artifactIdentityChecksum: 'RPC_ARTIFACT_IDENTITY_CHECKSUM'
artifactDigest: 'RPC_ARTIFACT_DIGEST', artifactIdentityChecksum: 'RPC_ARTIFACT_IDENTITY_CHECKSUM',
schemaChecksum: 'RPC_SCHEMA_CHECKSUM', actionManifestChecksum: 'RPC_ACTION_MANIFEST_CHECKSUM', runtimeContractChecksum: 'RPC_RUNTIME_CONTRACT_CHECKSUM'
};
for (const [field, key] of Object.entries(mapping)) ensure(String(session.release?.[field]) === exact[key], `authenticated bootstrap ${field} differs from Stable evidence`);
}
async function verifyTrustedReleaseBundle(projectDir, environment, release, fetcher = fetch) {
const file = String(environment.SAAS_RELEASE_TRUST_FILE || '');
ensure(path.isAbsolute(file) && lstatSync(file, {throwIfNoEntry:false})?.isFile() && !lstatSync(file).isSymbolicLink(), 'V2 startup needs an external build trust configuration');
const relative = path.relative(realpathSync(projectDir), realpathSync(file));
ensure(path.isAbsolute(relative) || relative === '..' || relative.startsWith('..' + path.sep), 'build trust must be supplied outside the signed deployment');
const trust = JSON.parse(readFileSync(file, 'utf8'));
const artifact = release.frozenArtifact;
const keyId = artifact?.buildEvidence?.keyId;
const builder = trust.builders?.[keyId];
ensure(builder && builder.enabled !== false && builder.moduleIds?.includes(release.moduleId) && builder.repositories?.includes(artifact.sourceRepository), 'build signer is not trusted for this module and repository');
const helper = require('./release-artifact');
const claims = helper.verifyReleaseEvidence('build', artifact.buildEvidence, {[keyId]:builder.publicKey});
ensure(release.releaseEvidenceVersion === 2 && artifact.releaseEvidenceVersion === 2 && claims.version === 2, 'release evidence version differs');
for (const [field, expected] of Object.entries({
releaseId: artifact.rcReleaseId, moduleId: release.moduleId, buildRevision: release.buildRevision, buildChecksum: release.build.buildChecksum,
sourceRepository: artifact.sourceRepository, sourceBranch: artifact.sourceBranch, sourceCommit: artifact.sourceCommit, tagName: artifact.tagName,
generatedLockChecksum: artifact.generatedLockChecksum, implementationManifestChecksum: artifact.implementationManifestChecksum,
dependencyLockChecksum: artifact.dependencyLockChecksum, bundleDigest: artifact.bundleDigest, artifactDigest: artifact.artifactDigest,
})) ensure(claims[field] === expected, `build signature ${field} differs from the frozen artifact`);
const base = new URL(environment.SAAS_CONTROL_PLANE_URL || environment.COOL_SAAS_CONTROL_PLANE_URL || 'http://127.0.0.1:8080');
const url = new URL('/admin/saas/module_release/artifact-bundle',base); url.searchParams.set('releaseId',String(release.id));
const response = await fetcher(url,{headers:{Authorization:`Bearer ${environment.COOL_SAAS_TOKEN}`},redirect:'error',signal:AbortSignal.timeout(30000)});
ensure(response.ok, `trusted artifact is unavailable or revoked (${response.status})`);
const body = await response.json();
ensure(body?.code === 1000 && body.data, 'trusted artifact response is invalid');
const evidence = body.data;
ensure(evidence.bundleDigest === artifact.bundleDigest && evidence.artifactDigest === artifact.artifactDigest &&
helper.releaseJsonChecksum(evidence.buildEvidence) === helper.releaseJsonChecksum(artifact.buildEvidence), 'stored artifact and signed source differ');
const inspected = helper.verifyLocalReleaseArtifact(projectDir,evidence.bundle);
ensure(inspected.artifactDigest === artifact.artifactDigest && inspected.bundleDigest === artifact.bundleDigest, 'deployed bytes differ from the trusted artifact');
ensure(createHash('sha256').update(inspected.files.get('pnpm-lock.yaml')).digest('hex') === artifact.dependencyLockChecksum, 'signed dependency lock differs');
return { recheck: () => helper.verifyLocalReleaseArtifact(projectDir,evidence.bundle) };
}
async function startStable(options = {}) {
const projectDir = path.resolve(options.projectDir || __dirname);
const environment = options.environment || process.env;
const release = await fetchReleaseEvidence(environment, options.fetch);
const mode = options.mode || 'production';
const lock = JSON.parse(readFileSync(path.join(projectDir, '.cool-saas-candidate.json'), 'utf8'));
const digest = calculateCompiledArtifactDigest(projectDir);
const commit = sourceCommit(projectDir, environment);
// Do not execute generated JavaScript before its compiled digest is verified.
ensure(digest === release.frozenArtifact?.artifactDigest, 'compiled artifact digest differs from the Frozen Artifact');
ensure(release.releaseEvidenceVersion == null || [1, 2].includes(release.releaseEvidenceVersion), 'unknown release evidence version');
const trusted = release.releaseEvidenceVersion === 2
? await verifyTrustedReleaseBundle(projectDir,environment,release,options.fetch)
: null;
const generated = (options.loadGenerated || (() => require(path.join(projectDir, 'dist/saas/generated')).GeneratedSaasModule))();
const exact = verifyStableEvidence({ release, lock, generated, digest, commit, releaseId: environment.RPC_RELEASE_ID });
const exact = verifyStableEvidence({ release, lock, generated, digest, commit, releaseId: environment.RPC_RELEASE_ID, mode });
applyExactEnvironment(environment, exact);
environment.SAAS_CONTROL_PLANE_URL ||= environment.COOL_SAAS_CONTROL_PLANE_URL || 'http://127.0.0.1:8080';
delete environment.COOL_SAAS_TOKEN;
@@ -175,6 +223,7 @@ async function startStable(options = {}) {
return start({ verifySession: session => {
verifyBootstrapSession(session, exact);
ensure(calculateCompiledArtifactDigest(projectDir) === digest, 'compiled bytes changed during bootstrap');
trusted?.recheck();
} });
}
@@ -183,4 +232,4 @@ if (require.main === module) startStable().catch(error => {
process.exitCode = 1;
});
module.exports = { startStable, verifyStableEvidence, verifyBootstrapSession, applyExactEnvironment, calculateCompiledArtifactDigest, candidateLockChecksum, fetchReleaseEvidence, sourceCommit };
module.exports = { startStable, verifyStableEvidence, verifyBootstrapSession, applyExactEnvironment, calculateCompiledArtifactDigest, candidateLockChecksum, fetchReleaseEvidence, sourceCommit, verifyTrustedReleaseBundle };