39 lines
2.9 KiB
JavaScript
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&¤t(active.token)?active:null,status:()=>Object.freeze({generation,minimumVersion:floor,active:active!==null&¤t(active.token)})});
|
|
}
|
|
module.exports={createPolicyAuthority,PolicyAuthorityError};
|