Files

71 lines
9.7 KiB
JavaScript
Raw Permalink Normal View History

'use strict';
// Signed external manifests avoid embedding the final browser hash in its own
// native helper. This classical P-256 contract is not a post-quantum signature.
const {createPublicKey,createPrivateKey,createHash,sign,verify}=require('node:crypto');
const {isIP}=require('node:net');
const MAX_RELEASE_BYTES=2097152,MAX_DEPLOYMENT_BYTES=262144;
const DOMAINS=Object.freeze({release:'SafetyNet Windows release v1\0',deployment:'SafetyNet Windows deployment v1\0'});
const verifiedReleases=new WeakSet(),verifiedDeployments=new WeakSet();
const MANAGEMENT_PROTOCOL=require('./policy-channel.cjs').VERSION;
const UUID=/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
function fail(code){throw Object.assign(new Error(code),{code});}
function exact(value,keys){return value&&typeof value==='object'&&!Array.isArray(value)&&Object.getPrototypeOf(value)===Object.prototype&&Object.keys(value).sort().join(',')===keys.slice().sort().join(',');}
function positive(value){return Number.isSafeInteger(value)&&value>0;}
function relative(value){return typeof value==='string'&&value.length<=240&&value.split('/').every(part=>/^[A-Za-z0-9_.-]+$/.test(part)&&part!=='.'&&part!=='..'&&!/[. ]$/.test(part)&&!/^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(part));}
function freeze(value){if(value&&typeof value==='object'){for(const item of Object.values(value))freeze(item);Object.freeze(value);}return value;}
function canonical(value){if(Array.isArray(value))return '['+value.map(canonical).join(',')+']';if(value&&typeof value==='object')return '{'+Object.keys(value).sort().map(key=>JSON.stringify(key)+':'+canonical(value[key])).join(',')+'}';return JSON.stringify(value);}
function publicKey(pem,type){
if(typeof pem!=='string'||pem.length>(type==='ml-dsa-87'?20000:4096)||!pem.startsWith('-----BEGIN PUBLIC KEY-----\n'))fail('windows-trust-key-invalid');
const key=createPublicKey(pem);if(key.export({format:'pem',type:'spki'})!==pem||key.asymmetricKeyType!==type||type==='ec'&&key.asymmetricKeyDetails?.namedCurve!=='prime256v1')fail('windows-trust-key-invalid');return key;
}
function releaseSchema(value){
if(!exact(value,['format','releaseId','releaseSequence','serviceName','deploymentTenant','deploymentPublicKey','files'])||value.format!=='safetynet-windows-release-v1'||!UUID.test(value.releaseId)||!UUID.test(value.deploymentTenant)||!positive(value.releaseSequence)||value.serviceName!=='SafetyNetNetworkBroker'||!Array.isArray(value.files)||!value.files.length||value.files.length>10000)fail('windows-release-invalid');
publicKey(value.deploymentPublicKey,'ec');let previous='';const seen=new Set();
for(const file of value.files){if(!exact(file,['path','bytes','sha256'])||!relative(file.path)||!Number.isSafeInteger(file.bytes)||file.bytes<0||!/^[a-f0-9]{64}$/.test(file.sha256)||file.path<=previous||seen.has(file.path.toLowerCase()))fail('windows-release-invalid');previous=file.path;seen.add(file.path.toLowerCase());}
for(const required of ['SafetyNet.exe','safetynet-service-host.exe','resources/app.asar','resources/broker/node.exe','resources/python/python.exe','resources/inspector/server.py'])if(!value.files.some(file=>file.path===required))fail('windows-release-invalid');
if(!value.files.some(file=>file.path.startsWith('resources/semantic/'))||value.files.some(file=>['release-envelope.json','deployment-envelope.json'].includes(file.path)))fail('windows-release-invalid');
return value;
}
function managementSchema(value){
if(!exact(value,['origin','signatureKey','protocol'])||typeof value.origin!=='string'||value.origin.length>2048||[...value.origin].some(character=>character.charCodeAt(0)<=32||character.charCodeAt(0)>=127)||value.protocol!==MANAGEMENT_PROTOCOL)fail('windows-management-invalid');
const url=new URL(value.origin);
if(url.protocol!=='https:'||url.origin!==value.origin||url.username||url.password||url.search||url.hash||url.pathname!=='/'||url.hostname.endsWith('.'))fail('windows-management-invalid');
const hostname=url.hostname.startsWith('[')?url.hostname.slice(1,-1):url.hostname;
if(!isIP(hostname)&&(hostname.length>253||hostname.split('.').some(label=>!/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(label)||/^0x[0-9a-f]+$/i.test(label))))fail('windows-management-invalid');
publicKey(value.signatureKey,'ml-dsa-87');return value;
}
function deploymentSchema(value){
const v2=value?.format==='safetynet-windows-deployment-v2';
if(!exact(value,['format','releaseId','deploymentTenant','deploymentSequence','policyPublicKey','expiresAt','users',...(v2?['management']:[])])||!['safetynet-windows-deployment-v1','safetynet-windows-deployment-v2'].includes(value.format)||!UUID.test(value.releaseId)||!UUID.test(value.deploymentTenant)||!positive(value.deploymentSequence)||!positive(value.expiresAt)||!Array.isArray(value.users)||!value.users.length||value.users.length>1024)fail('windows-deployment-invalid');
if(v2)managementSchema(value.management);
publicKey(value.policyPublicKey,'ed25519');let previous='';const devices=new Set();
for(const user of value.users){if(!exact(user,['sid','deviceId','minimumPolicyVersion'])||typeof user.sid!=='string'||!/^(?:S-1-5-21|S-1-12-1)-(?:(?:0|[1-9][0-9]{0,9})-){3}(?:0|[1-9][0-9]{0,9})$/.test(user.sid)||user.sid.split('-').slice(4).some(part=>Number(part)>4294967295)||user.sid<=previous||typeof user.deviceId!=='string'||!/^[A-Za-z0-9][A-Za-z0-9._-]{0,79}$/.test(user.deviceId)||devices.has(user.deviceId)||!positive(user.minimumPolicyVersion))fail('windows-deployment-invalid');previous=user.sid;devices.add(user.deviceId);}
return value;
}
function decodeEnvelope(envelope,kind,key){
const cap=kind==='release'?MAX_RELEASE_BYTES:MAX_DEPLOYMENT_BYTES;
if(!exact(envelope,['payload','signature'])||typeof envelope.payload!=='string'||envelope.payload.length>Math.ceil(cap*4/3)||! /^[A-Za-z0-9_-]+$/.test(envelope.payload)||typeof envelope.signature!=='string'||! /^[A-Za-z0-9_-]{86}$/.test(envelope.signature))fail('windows-trust-envelope-invalid');
const bytes=Buffer.from(envelope.payload,'base64url'),signature=Buffer.from(envelope.signature,'base64url');
if(bytes.length>cap||bytes.toString('base64url')!==envelope.payload||signature.length!==64||signature.toString('base64url')!==envelope.signature||!verify('sha256',Buffer.concat([Buffer.from(DOMAINS[kind]),bytes]),{key,dsaEncoding:'ieee-p1363'},signature))fail('windows-trust-signature-invalid');
const text=new TextDecoder('utf-8',{fatal:true}).decode(bytes),value=JSON.parse(text);if(canonical(value)!==text)fail('windows-trust-canonical-invalid');
return {value,digest:createHash('sha256').update(bytes).digest('hex')};
}
function checkFloor(sequence,digest,{minimumSequence=0,acceptedDigest}={}){
if(!Number.isSafeInteger(minimumSequence)||minimumSequence<0||acceptedDigest!==undefined&&!/^[a-f0-9]{64}$/.test(acceptedDigest))fail('windows-trust-floor-invalid');
if(sequence<minimumSequence||sequence===minimumSequence&&acceptedDigest!==undefined&&digest!==acceptedDigest)fail('windows-trust-rollback');
}
function verifyReleaseEnvelope(envelope,{publicKey:anchor,floor}={}){
try{const decoded=decodeEnvelope(envelope,'release',publicKey(anchor,'ec'));releaseSchema(decoded.value);checkFloor(decoded.value.releaseSequence,decoded.digest,floor);const result=freeze({manifest:decoded.value,digest:decoded.digest});verifiedReleases.add(result);return result;}catch(error){if(error.code?.startsWith('windows-'))throw error;fail('windows-release-invalid');}
}
function verifyDeploymentEnvelope(envelope,{release,floor,now=Date.now(),authenticatedSid}={}){
try{if(!verifiedReleases.has(release)||!Number.isSafeInteger(now)||now<0)fail('windows-deployment-invalid');const decoded=decodeEnvelope(envelope,'deployment',publicKey(release.manifest.deploymentPublicKey,'ec'));deploymentSchema(decoded.value);const value=decoded.value;if(value.releaseId!==release.manifest.releaseId||value.deploymentTenant!==release.manifest.deploymentTenant||value.expiresAt<=now)fail('windows-deployment-binding-invalid');checkFloor(value.deploymentSequence,decoded.digest,floor);const user=value.users.find(item=>item.sid===authenticatedSid);if(!user)fail('windows-deployment-user-denied');const result=freeze({manifest:value,digest:decoded.digest,bootstrap:{publicKey:value.policyPublicKey,deviceId:user.deviceId,minimumVersion:user.minimumPolicyVersion}});verifiedDeployments.add(result);return result;}catch(error){if(error.code?.startsWith('windows-'))throw error;fail('windows-deployment-invalid');}
}
function verifiedManagementBinding(deployment){
if(!verifiedDeployments.has(deployment)||deployment.manifest.format!=='safetynet-windows-deployment-v2'||deployment.manifest.expiresAt<=Date.now())fail('windows-management-denied');
return deployment.manifest.management;
}
function signManifest(value,privatePem,kind){
try{if(!Object.hasOwn(DOMAINS,kind))fail('windows-trust-kind-invalid');(kind==='release'?releaseSchema:deploymentSchema)(value);const payload=Buffer.from(canonical(value)),key=createPrivateKey(privatePem);if(key.asymmetricKeyType!=='ec'||key.asymmetricKeyDetails?.namedCurve!=='prime256v1')fail('windows-trust-key-invalid');if(payload.length>(kind==='release'?MAX_RELEASE_BYTES:MAX_DEPLOYMENT_BYTES))fail('windows-trust-envelope-invalid');return Object.freeze({payload:payload.toString('base64url'),signature:sign('sha256',Buffer.concat([Buffer.from(DOMAINS[kind]),payload]),{key,dsaEncoding:'ieee-p1363'}).toString('base64url')});}catch(error){if(error.code?.startsWith('windows-'))throw error;fail('windows-trust-signing-invalid');}
}
function publicKeyCoordinates(pem){try{const key=publicKey(pem,'ec'),jwk=key.export({format:'jwk'});return Buffer.concat([Buffer.from(jwk.x,'base64url'),Buffer.from(jwk.y,'base64url')]);}catch{fail('windows-trust-key-invalid');}}
module.exports={verifyReleaseEnvelope,verifyDeploymentEnvelope,signManifest,publicKeyCoordinates,verifiedManagementBinding,DOMAINS};