Files
safetynet/apps/network-broker/policy-authority.cjs
T

39 lines
2.9 KiB
JavaScript

'use strict';
const {createPublicKey,createHash,randomBytes}=require('node:crypto');
const {assertPolicyAssignment}=require('../browser/policy-assignment.cjs');
class PolicyAuthorityError extends Error {
constructor(code){super(code);this.name='PolicyAuthorityError';this.code=code;}
}
function freeze(value){if(value&&typeof value==='object'){for(const item of Object.values(value))freeze(item);Object.freeze(value);}return value;}
/** Trusted bootstrap pins enrollment key/device/minimum version. No policy
* enabling, network requests or fixture classifiers are performed here.
* Persist the accepted minimum version in protected enrollment storage when
* wiring production; this instance cannot supply durable anti-rollback alone.
*/
async function createPolicyAuthority({publicKey,deviceId,minimumVersion=0}) {
let pinned;
try {
if(typeof publicKey!=='string'||publicKey.length>16384||typeof deviceId!=='string'||!/^[A-Za-z0-9][A-Za-z0-9._-]{0,79}$/.test(deviceId)||!Number.isSafeInteger(minimumVersion)||minimumVersion<0)throw Error('invalid');
pinned=createPublicKey(publicKey).export({type:'spki',format:'pem'});
} catch {throw new PolicyAuthorityError('policy-bootstrap-invalid');}
const {verifyPolicy}=await import('../../packages/core/policy.mjs');
const namespace=randomBytes(16).toString('hex');
let generation=0,active=null,floor=minimumVersion,lastDigest=null,lastVersion=null;
const revoke=()=>{generation++;active=null;};
const current=token=>typeof token==='string'&&active!==null&&token===active.token&&Date.parse(active.policy.expiresAt)>Date.now();
function accept(envelope) {
try {
if(!envelope||typeof envelope!=='object'||Array.isArray(envelope)||Object.keys(envelope).sort().join(',')!=='payload,signature'||typeof envelope.payload!=='string'||!envelope.payload||envelope.payload.length>2097152||!/^[A-Za-z0-9_-]+$/.test(envelope.payload)||typeof envelope.signature!=='string'||!envelope.signature||envelope.signature.length>16384||!/^[A-Za-z0-9_-]+$/.test(envelope.signature))throw Error('invalid-envelope');
const policy=assertPolicyAssignment(verifyPolicy({payload:envelope.payload,signature:envelope.signature},pinned,floor),deviceId);
const digest=createHash('sha256').update(envelope.payload).digest('hex');
if(lastVersion===policy.version&&lastDigest!==digest)throw Error('version-equivocation');
generation++;floor=policy.version;lastVersion=policy.version;lastDigest=digest;
active=Object.freeze({token:`${namespace}:${generation}`,generation,version:policy.version,policy:freeze(policy)});
return active;
} catch {revoke();throw new PolicyAuthorityError('policy-rejected');}
}
return Object.freeze({accept,current,revoke,snapshot:()=>active&&current(active.token)?active:null,status:()=>Object.freeze({generation,minimumVersion:floor,active:active!==null&&current(active.token)})});
}
module.exports={createPolicyAuthority,PolicyAuthorityError};