86 lines
15 KiB
JavaScript
86 lines
15 KiB
JavaScript
// Compile an owned native SCM helper; never installs services or Windows rules.
|
|
import {spawn,execFileSync} from 'node:child_process';
|
|
import {readFile,writeFile,mkdir,lstat,realpath} from 'node:fs/promises';
|
|
import {createHash,generateKeyPairSync,sign} from 'node:crypto';
|
|
import {createRequire} from 'node:module';
|
|
import path from 'node:path';
|
|
import {fileURLToPath} from 'node:url';
|
|
const root=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'..');
|
|
const args=process.argv.slice(2),compileOnly=args.length===1&&args[0]==='--compile-only',selfTest=args.length===1&&(args[0]==='--self-test'||compileOnly);
|
|
const hash=bytes=>createHash('sha256').update(bytes).digest('hex');
|
|
const fail=()=>{throw Error('native-service-build-invalid');};
|
|
const {signManifest,publicKeyCoordinates}=createRequire(import.meta.url)('../packages/core/windows-trust.cjs');
|
|
const {encodeAcceptanceState}=createRequire(import.meta.url)('../apps/network-broker/service-acceptance.cjs');
|
|
async function safe(directory){const info=await lstat(directory);if(info.isSymbolicLink()||await realpath(directory)!==directory)fail();}
|
|
async function run(executable,arguments_,expectedCode=0){const started=performance.now();return await new Promise((resolve,reject)=>{const child=spawn(executable,arguments_,{cwd:root,windowsHide:true,stdio:['ignore','pipe','pipe'],shell:false});let bytes=0;for(const stream of [child.stdout,child.stderr])stream.on('data',chunk=>{bytes+=chunk.length;if(bytes>65536){child.kill();reject(Error('native-build-output-limit'));}else process.stdout.write(chunk);});const timer=setTimeout(()=>{child.kill();reject(Error('native-build-timeout'));},60000);child.once('error',error=>{error.nativeStage=path.basename(executable);reject(error);});child.once('exit',code=>{clearTimeout(timer);(code===expectedCode)?resolve({code,milliseconds:performance.now()-started}):reject(Error('native-build-failed'));});});}
|
|
async function main(){
|
|
if(process.platform!=='win32'||!selfTest)fail();
|
|
const output=path.join(root,'artifacts/native-service-host');await mkdir(output,{recursive:true});await safe(path.join(root,'artifacts'));await safe(output);
|
|
const vswhere='C:/Program Files (x86)/Microsoft Visual Studio/Installer/vswhere.exe';
|
|
const installation=execFileSync(vswhere,['-latest','-products','*','-requires','Microsoft.VisualStudio.Component.VC.Tools.x86.x64','-property','installationPath'],{encoding:'utf8',windowsHide:true,timeout:10000}).trim();if(!installation)fail();
|
|
const version=(await readFile(path.join(installation,'VC/Auxiliary/Build/Microsoft.VCToolsVersion.default.txt'),'utf8')).trim();if(!/^\d+\.\d+\.\d+$/.test(version))fail();
|
|
const tool=path.join(installation,'VC/Tools/MSVC',version),sdk='C:/Program Files (x86)/Windows Kits/10',sdkVersion='10.0.26100.0';
|
|
const vendor=generateKeyPairSync('ec',{namedCurve:'prime256v1'}),customer=generateKeyPairSync('ec',{namedCurve:'prime256v1'}),policy=generateKeyPairSync('ed25519');
|
|
const publicPem=key=>key.export({format:'pem',type:'spki'}),privatePem=key=>key.export({format:'pem',type:'pkcs8'});
|
|
const header='static constexpr char RELEASE_ANCHOR_XY[]="'+publicKeyCoordinates(publicPem(vendor.publicKey)).toString('hex')+'";';
|
|
await writeFile(path.join(output,'service-pins.h'),header);
|
|
const source=path.join(root,'apps/network-broker/native/service-host.cpp'),nativeHeader=path.join(root,'apps/network-broker/native/signed-manifest.h'),stateHeader=path.join(root,'apps/network-broker/native/acceptance-state.h'),topologyHeader=path.join(root,'apps/network-broker/native/wfp-topology.h'),credentialHeader=path.join(root,'apps/network-broker/native/management-credential.h'),beforeCredentialHeader=hash(await readFile(credentialHeader)),before=hash(await readFile(source)),beforeHeader=hash(await readFile(nativeHeader)),beforeStateHeader=hash(await readFile(stateHeader)),beforeTopologyHeader=hash(await readFile(topologyHeader));
|
|
const executable=path.join(output,'safetynet-service-host-test.exe');
|
|
const compiler=path.join(tool,'bin/Hostx64/x64/cl.exe');
|
|
const arguments_=['/nologo','/std:c++17','/W4','/WX','/EHsc','/O2','/MT','/guard:cf','/DUNICODE','/D_UNICODE','/DSAFETYNET_NATIVE_SELF_TEST',... [output,path.join(tool,'include'),...['ucrt','um','shared'].map(v=>path.join(sdk,'Include',sdkVersion,v))].map(v=>'/I'+v),'/Fo'+path.join(output,'service-host.obj'),'/Fe'+executable,source,'/link','/DYNAMICBASE','/NXCOMPAT','/HIGHENTROPYVA',... [path.join(tool,'lib/x64'),...['ucrt','um'].map(v=>path.join(sdk,'Lib',sdkVersion,v,'x64'))].map(v=>'/LIBPATH:'+v)];
|
|
const stages={};stages.compile=await run(compiler,arguments_);process.stdout.write('native.compile.completed\n');const compiledStat=await lstat(executable);process.stdout.write('native.compile.output.bytes='+compiledStat.size+'\n');const compiledHash=hash(await readFile(executable));process.stdout.write('native.compile.output.sha256='+compiledHash+'\n');if(hash(await readFile(source))!==before||hash(await readFile(nativeHeader))!==beforeHeader||hash(await readFile(stateHeader))!==beforeStateHeader||hash(await readFile(topologyHeader))!==beforeTopologyHeader||hash(await readFile(credentialHeader))!==beforeCredentialHeader)fail();if(compileOnly){await writeFile(path.join(output,'current-compile-only-proof.json'),JSON.stringify({compiled:true,selfTestExecuted:false,installed:false,wfpActivated:false,helperSha256:compiledHash,sourceSha256:before,nativeHeaderSha256:beforeHeader,stateHeaderSha256:beforeStateHeader,topologyHeaderSha256:beforeTopologyHeader,credentialHeaderSha256:beforeCredentialHeader,stages},null,2));return;}process.stdout.write('native.selftest.starting\n');stages.pipeJob=await run(executable,['--self-test']);stages.credentialSelection=await run(executable,['--verify-credential-selection']);
|
|
const helper=await readFile(executable),releaseId='11111111-1111-1111-1111-111111111111',deploymentTenant='22222222-2222-2222-2222-222222222222';
|
|
const fixturePaths=['SafetyNet.exe','safetynet-service-host.exe','resources/app.asar','resources/broker/node.exe','resources/python/python.exe','resources/inspector/server.py','resources/semantic/synthetic.bin'];
|
|
const files=[{path:'safetynet-service-host-test.exe',bytes:helper.length,sha256:hash(helper)}];
|
|
for(const name of fixturePaths){const destination=path.join(output,name);await mkdir(path.dirname(destination),{recursive:true});const synthetic=Buffer.from('fixed native signed inventory fixture');await writeFile(destination,synthetic);files.push({path:name,bytes:synthetic.length,sha256:hash(synthetic)});}
|
|
files.sort((a,b)=>a.path<b.path?-1:a.path>b.path?1:0);
|
|
const release={format:'safetynet-windows-release-v1',releaseId,releaseSequence:2,serviceName:'SafetyNetNetworkBroker',deploymentTenant,deploymentPublicKey:publicPem(customer.publicKey),files};
|
|
const deployment={format:'safetynet-windows-deployment-v1',releaseId,deploymentTenant,deploymentSequence:2,policyPublicKey:publicPem(policy.publicKey),expiresAt:Date.now()+60000,users:[{sid:'S-1-5-21-1-2-3-1001',deviceId:'synthetic-native',minimumPolicyVersion:3}]};
|
|
const releaseEnvelope=signManifest(release,privatePem(vendor.privateKey),'release'),deploymentEnvelope=signManifest(deployment,privatePem(customer.privateKey),'deployment');
|
|
await writeFile(path.join(output,'release-envelope.json'),JSON.stringify(releaseEnvelope));await writeFile(path.join(output,'deployment-envelope.json'),JSON.stringify(deploymentEnvelope));
|
|
stages.trustPositive=await run(executable,['--verify-fixture']);
|
|
// Native negatives are genuinely customer-signed, intentionally bypassing
|
|
// JS schema validation so the independent C++ parser must reject them.
|
|
const canonical=value=>Array.isArray(value)?'['+value.map(canonical).join(',')+']':value&&typeof value==='object'?'{'+Object.keys(value).sort().map(key=>JSON.stringify(key)+':'+canonical(value[key])).join(',')+'}':JSON.stringify(value);
|
|
const rawDeployment=value=>{const bytes=Buffer.from(canonical(value));return {payload:bytes.toString('base64url'),signature:sign('sha256',Buffer.concat([Buffer.from('SafetyNet Windows deployment v1\0'),bytes]),{key:customer.privateKey,dsaEncoding:'ieee-p1363'}).toString('base64url')};};
|
|
const pq=generateKeyPairSync('ml-dsa-87'),wrongPq=generateKeyPairSync('ml-dsa-65');
|
|
const management={origin:'https://management.example',signatureKey:publicPem(pq.publicKey),protocol:'safetynet-policy-mlkem1024-mldsa87-v1'};
|
|
const deploymentV2={...deployment,format:'safetynet-windows-deployment-v2',management};
|
|
stages.managementV2={};
|
|
for(const [index,origin] of ['https://management.example','https://management.example:8443','https://127.0.0.1','https://[::1]','https://[::ffff:c0a8:1]'].entries()){
|
|
await writeFile(path.join(output,'deployment-envelope.json'),JSON.stringify(signManifest({...deploymentV2,management:{...management,origin}},privatePem(customer.privateKey),'deployment')));
|
|
stages.managementV2['positive'+index]=await run(executable,['--verify-fixture']);
|
|
}
|
|
const invalidManagement=[...['http://management.example','https://management.example/','https://management.example/path','https://user:[email protected]','https://management.example?query=1','https://management.example#fragment','https://MANAGEMENT.example','https://management.example:443','https://management.example.','https://management.example:','https://[::1]:','https://[0:0:0:0:0:0:0:1]','https://[::ffff:192.168.0.1]','https://127.1','https://bad_host.example'].map(origin=>({...management,origin})),{...management,protocol:'safetynet-policy-mlkem768-mldsa65-v1'},{...management,signatureKey:publicPem(wrongPq.publicKey)},{...management,signatureKey:publicPem(policy.publicKey)},{...management,signatureKey:management.signatureKey.trim()},{...management,token:'synthetic-denied'}];
|
|
for(const [index,value] of invalidManagement.entries()){
|
|
await writeFile(path.join(output,'deployment-envelope.json'),JSON.stringify(rawDeployment({...deploymentV2,management:value})));
|
|
stages.managementV2['invalid'+index]=await run(executable,['--verify-fixture'],86);
|
|
}
|
|
await writeFile(path.join(output,'deployment-envelope.json'),JSON.stringify(rawDeployment({...deploymentV2,format:'safetynet-windows-deployment-v3'})));stages.managementV2.unknownFormat=await run(executable,['--verify-fixture'],86);
|
|
await writeFile(path.join(output,'deployment-envelope.json'),JSON.stringify(rawDeployment({...deploymentV2,format:'safetynet-windows-deployment-v1'})));stages.managementV2.v1ExtraCapability=await run(executable,['--verify-fixture'],86);
|
|
const signedV2=signManifest(deploymentV2,privatePem(customer.privateKey),'deployment');
|
|
await writeFile(path.join(output,'deployment-envelope.json'),JSON.stringify({...signedV2,payload:Buffer.from(canonical({...deploymentV2,management:{...management,origin:'https://attacker.example'}})).toString('base64url')}));stages.managementV2.signatureTamper=await run(executable,['--verify-fixture'],86);
|
|
await writeFile(path.join(output,'deployment-envelope.json'),JSON.stringify(deploymentEnvelope));stages.managementV2.v1HistoricalPreserved=await run(executable,['--verify-fixture']);
|
|
const changedPayload=JSON.parse(Buffer.from(releaseEnvelope.payload,'base64url'));changedPayload.releaseSequence=3;
|
|
await writeFile(path.join(output,'release-envelope.json'),JSON.stringify({...releaseEnvelope,payload:Buffer.from(JSON.stringify(changedPayload)).toString('base64url')}));stages.signatureTamper=await run(executable,['--verify-fixture'],86);
|
|
await writeFile(path.join(output,'release-envelope.json'),JSON.stringify(signManifest({...release,deploymentPublicKey:publicPem(vendor.publicKey)},privatePem(vendor.privateKey),'release')));stages.wrongDelegatedKey=await run(executable,['--verify-fixture'],86);
|
|
await writeFile(path.join(output,'release-envelope.json'),JSON.stringify(releaseEnvelope));
|
|
stages.uninstalledSnapshotRejected=await run(executable,['--trust-snapshot'],5);
|
|
stages.uninstalledCommitRejected=await run(executable,['--commit-acceptance'],5);
|
|
await writeFile(path.join(output,'deployment-envelope.json'),JSON.stringify(signManifest({...deployment,expiresAt:Date.now()-1},privatePem(customer.privateKey),'deployment')));stages.expiredDeployment=await run(executable,['--verify-fixture'],86);
|
|
await writeFile(path.join(output,'deployment-envelope.json'),JSON.stringify(deploymentEnvelope));stages.uninstalledClientRejected=await run(executable,['--client-stdio'],5);
|
|
stages.sameSequenceDigestRejected=await run(executable,['--verify-equivocation'],86);
|
|
const manyFiles=[...files,...Array.from({length:500},(_,i)=>({path:`resources/semantic/synthetic-${String(i).padStart(4,'0')}.bin`,bytes:0,sha256:'0'.repeat(64)}))].sort((a,b)=>a.path<b.path?-1:a.path>b.path?1:0);
|
|
await writeFile(path.join(output,'release-envelope.json'),JSON.stringify(signManifest({...release,files:manyFiles},privatePem(vendor.privateKey),'release')));stages.largeSignedInventory=await run(executable,['--verify-fixture']);
|
|
await writeFile(path.join(output,'release-envelope.json'),JSON.stringify(releaseEnvelope));
|
|
const oldState={format:'safetynet-service-floors-v1',release:{minimumSequence:2,acceptedDigest:hash(Buffer.from(releaseEnvelope.payload,'base64url'))},deployment:{tenant:deploymentTenant,minimumSequence:2,acceptedDigest:hash(Buffer.from(deploymentEnvelope.payload,'base64url'))},policies:[{sid:'S-1-5-21-1-2-3-1001',deviceId:'synthetic-native',policyKeySha256:hash(policy.publicKey.export({format:'der',type:'spki'})),minimumVersion:3,acceptedDigest:'a'.repeat(64)}]};
|
|
await writeFile(path.join(output,'acceptance-old.json'),encodeAcceptanceState(oldState));
|
|
const nextState=structuredClone(oldState);nextState.policies[0].minimumVersion=4;nextState.policies[0].acceptedDigest='b'.repeat(64);
|
|
await writeFile(path.join(output,'acceptance-candidate.json'),encodeAcceptanceState(nextState));stages.monotonicState=await run(executable,['--verify-state-fixture']);
|
|
await writeFile(path.join(output,'acceptance-candidate.json'),encodeAcceptanceState({...nextState,policies:[]}));stages.policyDropRejected=await run(executable,['--verify-state-fixture'],86);
|
|
const lowered=structuredClone(nextState);lowered.policies[0].minimumVersion=2;await writeFile(path.join(output,'acceptance-candidate.json'),encodeAcceptanceState(lowered));stages.policyRollbackRejected=await run(executable,['--verify-state-fixture'],86);
|
|
const equivocation=structuredClone(oldState);equivocation.policies[0].acceptedDigest='b'.repeat(64);await writeFile(path.join(output,'acceptance-candidate.json'),encodeAcceptanceState(equivocation));stages.policyEquivocationRejected=await run(executable,['--verify-state-fixture'],86);
|
|
await writeFile(path.join(output,'proof.json'),JSON.stringify({sourceSha256:before,nativeHeaderSha256:beforeHeader,stateHeaderSha256:beforeStateHeader,topologyHeaderSha256:beforeTopologyHeader,credentialHeaderSha256:beforeCredentialHeader,helperSha256:hash(await readFile(executable)),selfTest:true,installed:false,wfpActivated:false,msvc:version,sdk:sdkVersion,stages},null,2));
|
|
}
|
|
main().catch(error=>{process.stderr.write('native-service-build-failed'+(typeof error?.nativeStage==='string'&&/^[a-zA-Z0-9.-]+$/.test(error.nativeStage)?':'+error.nativeStage:'')+(typeof error?.code==='string'&&/^[A-Z_]+$/.test(error.code)?':'+error.code:'')+(typeof error?.syscall==='string'&&/^[a-z_]+$/.test(error.syscall)?':'+error.syscall:'')+'\n');process.exitCode=1;});
|