Skip to content

Commit

Permalink
Update CA Certificate report
Browse files Browse the repository at this point in the history
TODO: how to automate this?
  • Loading branch information
James-E-A committed May 30, 2021
1 parent caf7b1d commit 9998ba6
Show file tree
Hide file tree
Showing 6 changed files with 1,667 additions and 1,681 deletions.
3,062 changes: 1,593 additions & 1,469 deletions src/db/IncludedCACertificateReport.json

Large diffs are not rendered by default.

48 changes: 31 additions & 17 deletions src/js/constants.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
const secTypes={
const fp_alg = 'sha256';
const fp_alg_key = fp_alg.replace(/(\w+?)(\d+)/, (m, alg, n) => `${alg.toUpperCase()}-${n} Fingerprint`)

const secTypes = {
Mozilla: 0,
MitM: 1,
aRootKnown: 2,
Expand All @@ -9,45 +12,56 @@ const secTypes={
};
Object.freeze(secTypes);

const sha256fp_host = new Object();
const host_country = new Object();
const sha256fp_host_alt = new Object();
// https://en.wikipedia.org/wiki/Country_code_top-level_domain#ASCII_ccTLDs_not_in_ISO_3166-1
const country_tld_exceptions = new Map([
// AMERICAA
['gov', 'us'],
// Britain owns+uses this one
['uk', 'gb'],
// Ascension Island is part of the British Overseas territory,
// "Saint Helena, Ascension and Tristan da Cunha"
['ac', 'sh'],
// European Union
['eu', null]
]);

const fp_host = new Map();
const host_country = new Map();
const fp_host_alt = new Map();

//TODO TODO TODO TODO TODO TODO TODO
sha256fp_host_alt['07:ED:BD:82:4A:49:88:CF:EF:42:15:DA:20:D4:8C:2B:41:D7:15:29:D7:C9:00:F5:70:92:6F:27:7C:C2:30:C5']='cacert.org';
sha256fp_host_alt['89:4E:BC:0B:23:DA:2A:50:C0:18:6B:7F:8F:25:EF:1F:6B:29:35:AF:32:A9:45:84:EF:80:AA:F8:77:A3:A0:6E']='us';//fpki.gov=us
Object.freeze(sha256fp_host_alt);
fp_host_alt.set('07:ED:BD:82:4A:49:88:CF:EF:42:15:DA:20:D4:8C:2B:41:D7:15:29:D7:C9:00:F5:70:92:6F:27:7C:C2:30:C5', 'cacert.org');
fp_host_alt.set('89:4E:BC:0B:23:DA:2A:50:C0:18:6B:7F:8F:25:EF:1F:6B:29:35:AF:32:A9:45:84:EF:80:AA:F8:77:A3:A0:6E', 'us'); //fpki.gov=us
fp_host_alt.set('B1:07:B3:3F:45:3E:55:10:F6:8E:51:31:10:C6:F6:94:4B:AC:C2:63:DF:01:37:F8:21:C1:B3:C2:F8:F8:63:D2', 'us'); // whytf does the DoD have so many roots, and why was this one never cross-signed?

//Record the SHA-256 Fingerprint -> (reduced) hostname mappings
//As well as non-idiopathic SHA-256 Fingerprint -> country mappings
getAsset("db/IncludedCACertificateReport.json","json")
getAsset("db/IncludedCACertificateReport.json", "json")
.forEach(ca => {
const sha256fp = ca["SHA-256 Fingerprint"].replaceAll(/(\w{2})(?=\w)/g,'$1:');
const fp = ca[fp_alg_key].replaceAll(/(\w{2})(?=\w)/g,'$1:');
//perforate the fingerprints with : every 2 characters
//(i.e. convert from CCADB-format to Firefox-format)

const host = reduceHostname(new URL(ca["Company Website"]).hostname);

sha256fp_host[sha256fp] = host;
fp_host.set(fp, host);

let country = identifyCountry(host);
if( country ) host_country[host] = country;
if( country ) host_country.get(host) = country;
});
Object.freeze(sha256fp_host);

//Apply idiopathic country mappings
Object.entries(getAsset("db/host_cityStateCountry.json","json"))
.forEach(([host,[city,state,country]]) => {
.forEach(([host, [city, state, country]]) => {
if( host in host_country ) {
console.warn("Overwriting country! Database may need to be checked.\nIf you're seeing this, please open an issue on GitHub; include the following:",
{
host: host,
currently: host_country[host],
csc: [city,state,country]
currently: host_country.get(host),
csc: [city, state, country]
}
);
}
host_country[host] = country.toLowerCase();
host_country.set(host, country.toLowerCase());
});
Object.freeze(host_country);

26 changes: 14 additions & 12 deletions src/js/main.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
//Global cache of SecurityDetails objects:
const cachedSecurityDetails=new Object();
const cachedSecurityDetails=new Map();

browser.tabs.onUpdated.addListener(
function onTabUpdatedStatusListener(tabId,changeInfo,tabInfo){
let browserActionSpec = genBrowserActionSpec(secTypes.unknown);
let extraCmds = {};
try {
const url = removeFragment(tabInfo.url);
const securityDetails = cachedSecurityDetails[tabId][url];
const host = new URL(tabInfo.url).host;
const securityDetails = cachedSecurityDetails.get(tabId).get(host);
if( changeInfo.status == 'complete' && securityDetails ) extraCmds.enable = tabId;
browserActionSpec = genBrowserActionSpec(securityDetails.secType,securityDetails.caId);
} finally {
Expand All @@ -21,27 +21,28 @@ browser.tabs.onUpdated.addListener(

browser.tabs.onRemoved.addListener(
async function onTabRemovedListener(tabId,removeInfo){
delete cachedSecurityDetails[tabId];
cachedSecurityDetails.delete(tabId);
}
);

//this is the only point we can getSecurityInfo.
//this is a design flaw IMO, since it allows attackers
//to intercept at least one outbound request (no matter
//how well we code) before detection.
//TODO: pester Mozilla about this
// Until or unless Mozilla fixes https://bugzilla.mozilla.org/show_bug.cgi?id=1499592
// attackers may intercept at least one outbound request
// (no matter how well we code) before detection.
// blocked by the following upstream bug with Priority: P5
// https://bugzilla.mozilla.org/show_bug.cgi?id=1499592
browser.webRequest.onHeadersReceived.addListener(
async function onHeadersReceivedListener(details){
const tabId = details.tabId;
const type = details.type;
const requestId = details.requestId;
const requestUrl = removeFragment(details.url);
const host = new URL(details.url).host;
const securityInfo = await browser.webRequest.getSecurityInfo(requestId,{certificateChain:true,rawDER:true});
switch(type){
case 'main_frame':
if(!( tabId in cachedSecurityDetails )) cachedSecurityDetails[tabId] = new Object();
// TODO
if( !cachedSecurityDetails.has(tabId) ) cachedSecurityDetails.set(tabId, new Map());
const secDetails = new SecurityDetails(details, securityInfo);
return cachedSecurityDetails[tabId][requestUrl] = secDetails;
return cachedSecurityDetails.get(tabId).set(host, secDetails);
default:
//TODO
return;
Expand All @@ -52,3 +53,4 @@ browser.webRequest.onHeadersReceived.addListener(
},
['blocking'] //this has to be blocking, or getSecurityInfo doesn't work
);

56 changes: 28 additions & 28 deletions src/js/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ class SecurityDetails {
//TODO/FIXME: Mozilla doesn't provide
//any access whatsoever to self-signed
//or otherwise nominally-invalid certs
// https://discourse.mozilla.org/t/webrequest-getsecurityinfo-cant-get-self-signed-tofu-exception-certificates/67135
// https://bugzilla.mozilla.org/show_bug.cgi?id=1678492
return secTypes.unknown;
} else if( certChain.length == 1 ) {

Expand All @@ -69,7 +69,7 @@ class SecurityDetails {
return secTypes.MitM;
} else {
//...an alternative Root CA
if(certChain[certChain.length-1].fingerprint.sha256 in sha256fp_host_alt) {
if(certChain[certChain.length-1].fingerprint[fp_alg] in fp_host_alt) {
return secTypes.aRootKnown;
} else {
return secTypes.aRootUnknown;
Expand All @@ -94,14 +94,14 @@ class SecurityDetails {
return 'self';
} else {
const rootCert = certChain[certChain.length-1];
const sha256fp = rootCert.fingerprint.sha256;
if( sha256fp in sha256fp_host){
return sha256fp_host[sha256fp];
} else if( sha256fp in sha256fp_host_alt ){
return sha256fp_host_alt[sha256fp];
const fp = rootCert.fingerprint[fp_alg];
if( fp in fp_host){
return fp_host.get(fp);
} else if( fp_host_alt.has(fp) ){
return fp_host_alt.get(fp);
} else {
console.warn('Unknown CA',certChain);
return sha256fp;
return fp;
}
}
throw {status:'thisShouldNeverHappen',securityInfo:securityInfo};
Expand Down Expand Up @@ -147,19 +147,19 @@ function intDiv(a,b){
return a/b>>0;
}

function genBrowserActionSpec(secType=null,caId=null){
function genBrowserActionSpec(secType=null, caId=null){
switch(secType) {
case secTypes.Mozilla:
return {
Icon: {path: `images/root_icons/${caId}.ico`},
Title: {title: `${flag(host_country[caId]||'XX')} ${caId}\n(Mozilla-trusted Root CA)`},
Title: {title: `${flag(host_country.get(caId)||'XX')} ${caId}\n(Mozilla-trusted Root CA)`},
BadgeText: {text: '\uD83E\uDD8A'},
BadgeBackgroundColor: {color: 'LimeGreen'}
};
case secTypes.MitM:
return {
Icon: {path: `images/Twemoji_1f441.svg`},
Title: {title: "MitM TLS Proxy\n(Your network administrator is inspecting this connection.)"},
Title: {title: "MitM TLS Proxy\n(This network's administrator is eavesdropping your connection; you have no reasonable expectation of privacy here.)"},
BadgeText: {text: '\u2013'},
BadgeBackgroundColor: {color: 'Fuchsia'}
};
Expand Down Expand Up @@ -197,7 +197,7 @@ function genBrowserActionSpec(secType=null,caId=null){

function isItMitM(cert){
//TODO check with the user about this
if( cert.fingerprint.sha256 in sha256fp_host || cert.fingerprint.sha256 in sha256fp_host_alt ) {
if( fp_host.has(cert.fingerprint[fp_alg]) || fp_host_alt.has(cert.fingerprint[fp_alg]) ) {
//The cert was in EITHER database
//therefore it is legitimate,
//i.e. NOT a MitM:
Expand Down Expand Up @@ -267,23 +267,26 @@ function match0(s,r){
}
}

function identifyCountry(hostname,only_gov=false){
const exceptionRe = /^(?:uk|ac|eu)$/ ; //https://en.wikipedia.org/wiki/Country_code_top-level_domain#ASCII_ccTLDs_not_in_ISO_3166-1
function identifyCountry(hostname, only_gov=false){
const h = hostname.split ? hostname.split('.') : hostname;
const len = h.length;
const tld = len >= 1 ? h[len-1] : null;
const sld = len >= 2 ? h[len-2] : null;

if( tld.length == 2 ) {
if( only_gov && sld != 'gov' ) return null;
switch( match0(tld,exceptionRe) ) {
case 'uk':
//Britain owns+uses this one
return 'gb';
case 'ac':
//Ascension Island is part of the British Overseas territory
//"Saint Helena, Ascension and Tristan da Cunha"
return 'sh';
if( country_tld_exceptions.has(tld) ) return country_tld_exceptions.get(tld);
else return null;
if( only_gov && sld != 'gov' ) return null;
if
switch( match0(tld,exceptionRe) ) {
case 'uk':

return 'gb';
case 'ac':

return 'sh';
case 'gov':

return 'us';
case null:
//2-letter TLD *not* in the exception list;
//it's a valid ccTLD corresponding to its country
Expand All @@ -293,9 +296,6 @@ function identifyCountry(hostname,only_gov=false){
//it's not a valid ccTLD and we don't know the country
return null;
}
} else if( tld == 'gov' ) {
//AMERICAAA
return 'us';
} else {
return null;
}
Expand All @@ -321,7 +321,7 @@ function reduceHostname(hostname){
if( h[0] == 'pki') h.splice(0,1);

// 3.
const govCountry = identifyCountry(h,true);
const govCountry = identifyCountry(h, true);
if( govCountry ) return govCountry;

return h.join('.');
Expand Down
2 changes: 1 addition & 1 deletion src/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
],

"background": {
"scripts": ["js/util.js","js/constants.js","js/main.js"]
"scripts": ["js/util.js","js/constants.js","js/oop.js","js/main.js"]
},

"browser_action": {
Expand Down
Loading

0 comments on commit 9998ba6

Please sign in to comment.