This commit is contained in:
commit
a7a433e271
15 files changed
+259
-12
No files matched your search
@@ -60,8 +60,15 @@ scripts/testing/gate-lane.sh prepare-image --lane a --candidate FULL_SHA \
|
||||
|
||||
Preparation holds both lanes and the exclusive image lock. Its build log,
|
||||
receipt, count and wall time are separate from warm gate timings. The current
|
||||
Containerfile uses a mutable FROM tag and apt indexes. A missing base digest
|
||||
refuses provenance publication. No credential file is copied or printed.
|
||||
Containerfile uses one literal digest-pinned FROM reference; apt indexes remain
|
||||
mutable. Preparation records that exact declared reference as `baseDigest`, and
|
||||
verification refuses a different receipt reference. It never borrows the digest
|
||||
of a cached mutable tag. Mutable, indirect and multistage FROM inputs refuse
|
||||
before build dispatch. This binds source/build inputs, not an independently
|
||||
measured base binary or a layer-level reconstruction. Admitted image validation
|
||||
must still check the built image's Node version (`v24.22.2`) and provenance.
|
||||
Retained registry config metadata and its limits are in
|
||||
`docs/security/q284-node-base/`. No credential file is copied or printed.
|
||||
|
||||
Run records are immutable JSON files published by link after fsync. Every step
|
||||
has a log, UTC/monotonic times, exit code, pass/fail/skip counts and log hash.
|
||||
|
||||
@@ -3,6 +3,14 @@ import { readFileSync, readdirSync, lstatSync, writeFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { refuse } from './lane-state.mjs';
|
||||
// This is the declared build input, not an observation of a running binary.
|
||||
function pinnedBase(checkout){
|
||||
const lines=readFileSync(join(checkout,'Containerfile'),'utf8').split('\n').filter(line=>/^\s*FROM(?:\s|$)/i.test(line));
|
||||
if(lines.length!==1)refuse('image requires exactly one pinned FROM');
|
||||
const fields=lines[0].trim().split(/\s+/);
|
||||
if(fields.length!==2||!/^[A-Za-z0-9][A-Za-z0-9._:/-]*@sha256:[a-f0-9]{64}$/.test(fields[1]))refuse('image requires one literal digest-pinned FROM');
|
||||
return fields[1];
|
||||
}
|
||||
export function imageFingerprint(checkout){
|
||||
const dockerfile=readFileSync(join(checkout,'Containerfile'),'utf8');
|
||||
const copies=dockerfile.split('\n').filter(l=>/^\s*(COPY|ADD)\s/i.test(l)).map(l=>l.trim());
|
||||
@@ -18,6 +26,7 @@ export function inspectImage(tag,docker=process.env.MOSAIC_GATE_REAL_DOCKER??'do
|
||||
}
|
||||
export function verifyImage(receipt,checkout,inspect=inspectImage){
|
||||
if(!receipt||!/^sha256:[a-f0-9]{64}$/.test(receipt.imageId??'')||!/^.+@sha256:[a-f0-9]{64}$/.test(receipt.baseDigest??'')||!/^linux\/[a-z0-9_]+$/.test(receipt.platform??'')||!/^mosaic-poc-agent:[A-Za-z0-9_.-]+$/.test(receipt.tag??''))refuse('image provenance receipt invalid');
|
||||
if(receipt.baseDigest!==pinnedBase(checkout))refuse('image base differs from pinned Containerfile');
|
||||
if(receipt.inputDigest!==imageFingerprint(checkout))refuse('image inputs differ');
|
||||
const actual=inspect(receipt.tag);
|
||||
if(actual.Id!==receipt.imageId)refuse('pinned image ID differs');
|
||||
@@ -29,6 +38,7 @@ export function verifyImage(receipt,checkout,inspect=inspectImage){
|
||||
// Called by the CLI only while both lanes and the image lock are exclusive.
|
||||
export function prepareImage(checkout,{grant,tempRoot,out},invoke){
|
||||
if(!grant)refuse('missing image preparation grant');
|
||||
const baseDigest=pinnedBase(checkout);
|
||||
const inputDigest=imageFingerprint(checkout);
|
||||
const pkg=JSON.parse(readFileSync(join(checkout,'package.json'),'utf8'));
|
||||
const release=readFileSync(join(checkout,'RELEASE'),'utf8').trim();
|
||||
@@ -38,7 +48,6 @@ export function prepareImage(checkout,{grant,tempRoot,out},invoke){
|
||||
const start=performance.now();let buildOutput;try{buildOutput=run(['compose','build']);}catch(e){if(out)writeFileSync(join(out,'image-build.log'),String(e.stdout??'')+String(e.stderr??''),{mode:0o600,flag:'wx'});throw e;}if(out)writeFileSync(join(out,'image-build.log'),buildOutput,{mode:0o600,flag:'wx'});const buildWallMs=performance.now()-start;
|
||||
if(inputDigest!==imageFingerprint(checkout))refuse('image inputs changed while building');
|
||||
const actual=JSON.parse(run(['image','inspect',tag,'--format','{{json .}}']));
|
||||
const base=JSON.parse(run(['image','inspect','node:24-bookworm-slim','--format','{{json .}}']));
|
||||
const receipt={inputDigest,imageId:actual.Id,tag,baseDigest:base.RepoDigests?.find(x=>/@sha256:[a-f0-9]{64}$/.test(x)),platform:`${actual.Os}/${actual.Architecture}`,grant,builds:1,buildWallMs,at:new Date().toISOString()};
|
||||
const receipt={inputDigest,imageId:actual.Id,tag,baseDigest,platform:`${actual.Os}/${actual.Architecture}`,grant,builds:1,buildWallMs,at:new Date().toISOString()};
|
||||
verifyImage(receipt,checkout,()=>actual);return receipt;
|
||||
}
|
||||
Reference in new issue
Block a user