'use strict'; const {X509Certificate}=require('node:crypto'); const {isIP}=require('node:net'); const fail=()=>{throw Error('broker-certificate-pin-rejected');}; function authorityFacts(value){ if(typeof value!=='string'||value.length>320||/[^\x21-\x7e]|[@/\\?#%]/.test(value))fail(); const match=/^(\[[^\]]+\]|[^:]+):([1-9][0-9]{0,4})$/.exec(value);if(!match||Number(match[2])>65535)fail(); let url;try{url=new URL('https://'+value+'/');}catch{fail();} const hostname=url.hostname.startsWith('[')?url.hostname.slice(1,-1):url.hostname; if(!isIP(hostname)&&hostname.split('.').some(label=>!/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(label)))fail(); if(`${isIP(hostname)===6?'['+hostname+']':hostname}:${Number(match[2])}`!==value)fail(); return Object.freeze({hostname,authority:value}); } /** For one managed Electron session only. Electron's verifier has NO port and * caches results: CONNECT/SNI/inner-Host gates must still bind the port. Revoke * must also close existing connections/stop the old broker before replacement. * Management sessions retain the platform verifier and receive no such pins. */ function createBrokerCertificatePins({generation,policyVersion,current,authorizedAuthority,maxPins=128}={}){ if(typeof generation!=='string'||!generation||!Number.isSafeInteger(policyVersion)||policyVersion<1||typeof current!=='function'||typeof authorizedAuthority!=='function'||!Number.isInteger(maxPins)||maxPins<1||maxPins>1024)fail(); let revoked=false;const pins=new Map(); const active=()=>{try{return !revoked&¤t()===true;}catch{return false;}}; const authorized=authority=>{try{return authorizedAuthority(authority)===true;}catch{return false;}}; return Object.freeze({ accept(input){ if(!active()||!input||input.generation!==generation||input.policyVersion!==policyVersion||typeof input.cert!=='string'||Buffer.byteLength(input.cert)>16384||!/^-----BEGIN CERTIFICATE-----\r?\n[A-Za-z0-9+/=\r\n]+\r?\n-----END CERTIFICATE-----\s*$/.test(input.cert))fail(); const facts=authorityFacts(input.authority);if(!authorized(facts.authority))fail(); let certificate;try{certificate=new X509Certificate(input.cert);}catch{fail();} const now=Date.now(),until=Date.parse(certificate.validTo),from=Date.parse(certificate.validFrom); const matches=isIP(facts.hostname)?certificate.checkIP(facts.hostname):certificate.checkHost(facts.hostname,{subject:'never',wildcards:false}); if(certificate.ca||!certificate.subjectAltName||certificate.subjectAltName.includes(',')||!matches||!certificate.verify(certificate.publicKey)||!Number.isFinite(until)||!Number.isFinite(from)||from>now||until<=now||until>now+3601000||input.fingerprint256!==certificate.fingerprint256)fail(); for(const [key,pin]of pins)if(pin.expiresAt<=now)pins.delete(key); const key=facts.authority; if(!pins.has(key)&&pins.size>=maxPins)fail(); if(!active()||!authorized(key))fail(); pins.set(key,Object.freeze({...facts,fingerprint:certificate.fingerprint256,expiresAt:until}));return true; }, verify(request,callback){ let result=-3; try{ // Electron exposes Certificate.data/fingerprint, not Node's fingerprint256. // Derive our fingerprint from the actual presented PEM, never a claim. const data=request?.certificate?.data,host=request?.hostname; if(typeof data!=='string'||Buffer.byteLength(data)>16384)throw Error('certificate-data-unavailable'); const fingerprint=new X509Certificate(data).fingerprint256; const match=[...pins.values()].find(pin=>pin.hostname===host&&pin.fingerprint===fingerprint); if(match)result=active()&&match.expiresAt>Date.now()&&authorized(match.authority)?0:-2; }catch{result=-3;} callback(result); }, revoke(){revoked=true;}, status(){return Object.freeze({active:active(),pins:pins.size});} }); } module.exports={createBrokerCertificatePins,authorityFacts};