Cert Spotter: jednoduché sledování vydaných HTTPS certifikátů

4. 4. 2018
Doba čtení: 5 minut

Sdílet

Ilustrační obrázek
Autor: Depositphotos – stori
Ilustrační obrázek
Od 30. dubna musí být všechny nové certifikáty zveřejňovány v systému Certificate Transparency, aby byly platné. Díky tomu je možné sledovat vydávání certifikátů na vybraná doménová jména.

Technologie Certificate Transparency nijak nezabrání neoprávněnému vydání certifikátu, dává ale jistotu, že všechny platné certifikáty jsou zveřejněny. Aby mělo zveřejnění nějaký bezpečnostní efekt, je potřeba seznamy certifikátů monitorovat. Přesně k tomu slouží open-source nástroj Cert Spotter.

Ten dokáže monitorovat známé CT logy a procházet všechny certifikáty, které do nich přibývají. Vybírá z nich přitom jen ty zajímavé, tedy ty vydané na konkrétní doménové jméno, případně subdoménu takového jména. Po dosažení konce si zapamatuje aktuální otisk kořenu daného logu a při dalším spuštění projde jen ty certifikáty, které do logu přibyly od minula.

Instalace a konfigurace

Cert Spotter je napsán v jazyce Go, pro jeho kompilaci je tedy nutné nainstalovat překladač tohoto jazyka. Zbytek instalace zvládneme už jen s uživatelským oprávněním.

# apt install golang-go
$ mkdir ~/go
$ go get software.sslmate.com/src/certspotter/cmd/certspotter 

Nyní musíme připravit seznam sledovaných doménových jmen. Jde o jednoduchý textový soubor s jedním záznamem na řádek. Pokud jméno začíná tečkou, bude sledována jak samotná doména, tak i všechny subdomény. Jinak je sledováno pouze zadané jméno.

$ mkdir ~/.certspotter
$ echo .google.com > ~/.certspotter/watchlist
$ echo www.youtube.cz >> ~/.certspotter/watchlist
$ ~/go/bin/certspotter 

První spuštění utility certspotter pouze zaznamená aktuální kořeny Merkelova stromu (STH) pro všechny sledované logy do adresáře ~/.certspotter. Jedná se tedy o určení okamžiku, od kterého bude zahájeno sledování všech logů. Přepínačem -all_time je možné vynutit průchod celé historie všech logů od úplného začátku; na něco takového si však vyhraďte rychlé připojení a několik týdnů času.

Vlastní provoz

Seznam sledovaných logů je součástí zdrojových kódů a je shodný s logy, které Chrome uznává jako důvěryhodné. Při dalším spuštění jsou všechny logy opět dotázány na STH a následně jsou po nich požadovány všechny certifikáty přidané mezi danými STH. Certifikáty jsou následně filtrovány podle sledovaných doménových jmen. Pro každý certifikát, vydaný na sledované doménové jméno, jsou na standardní výstup vypsány základní informace a certifikát je uložen na disk:

$ time go/bin/certspotter
381a904b02510458ab2a05c26a23125c8164d2ca2151b8af61cc51cafc49a602:
             DNS Name = *.c.docs.google.com
             DNS Name = *.a1.googlevideo.com
             DNS Name = *.c.2mdn.net
             … (redakčně kráceno)
             DNS Name = *.xn--ngstr-lra8j.com
             DNS Name = xn--ngstr-lra8j.com
               Pubkey = a3aee426ca972017285da51122e863338c7b74baa52e04b56ef62104e4d25a6e
               Issuer = C=US, O=Google Inc, CN=Google Internet Authority G2
           Not Before = 2018-03-20 12:45:00 +0000 UTC
            Not After = 2018-06-12 12:45:00 +0000 UTC
            Log Entry = 107874875 @ https://ct.googleapis.com/logs/argon2018 (Certificate)
               crt.sh = https://crt.sh/?sha256=381a904b02510458ab2a05c26a23125c8164d2ca2151b8af61cc51cafc49a602
             Filename = /home/user/.certspotter/certs/38/381a904b02510458ab2a05c26a23125c8164d2ca2151b8af61cc51cafc49a602.cert.pem


real    83m56.438s
user    4m25.846s
sys     1m46.546s 

Jak je vidět na příkladu, běh Cert Spotteru může trvat poměrně dlouho; většinu času přitom zabere čekání na odpovědi z CT logů v různých koutech světa. Vytížení procesoru i spotřeba paměti jsou poměrně malé.

Automatizace cronem

Utilita je přímo navržena pro použití se standardním plánovačem úloh cron. Pokud není žádný certifikát nalezen, certspotter mlčí, při nalezení zajímavého certifikátu vypíše detaily na standardní výstup a cron  pak zařídí, aby se takový standardní výstup odeslal e-mailem administrátorovi. Cert Spotter si také hlídá vícenásobné spuštění – pokud by běh trval déle, než je perioda cronu, odmítne se podruhé spustit.

Nevýhodou posílání e-mailu přímo z cronu je, že všechny takové e-maily mají stejný předmět ve tvaru Cron <user@ubuntu> /home/user/go/certspotter a e-mailová adresa je společná pro všechny úlohy. Oba problémy je možné vyřešit použitím externí utility bsd-mailx, která s přepínačem -E dokáže poslat e-mail pouze v případě, že standardní vstup není prázdný. Příslušný řádek v crontabu, který spustí Cert Spotter ve 23. minutu každé hodiny, může vypadat nějak takto (zalomeno pro přehlednost):

23 * * * * /path/to/go/bin/certspotter 2>> /tmp/cs-stderr.log \
           | mail -E -s 'Certspotter alert' [email protected] 

Na standardní chybový výstup jsou vypisovány nejčastěji chyby komunikace s jednotlivými logy. Takové chyby jsou sice důležité, ale mezi e-maily o vydaných certifikátech způsobují spoustu šumu, proto je lepší je přesměrovat do souboru.

Balíčky jsou k dispozici

Kompilace Cert Spotteru přímo ze zdrojových kódů je sice jednoduchá, ale vyžaduje ruční aktualizace, například kdykoli se změní seznam důvěryhodných logů. Dobrou zprávou je, že minimálně pro Debian a Ubuntu jsou k dispozici balíčky. Odpadá tedy úvodní kompilace a aktualizaci můžeme nechat na správcích distribuce.

Otázkou pouze zůstává, zda balíček s Cert Spotterem ve stabilní distribuci, jakou je Debian, nezastará příliš rychle. Přece jen vývoj webových prohlížečů a s nimi i bezpečnostních požadavků je mnohem agilnější.

bitcoin_smenarna

Nezávislá alternativa k on-line službám

Pro sledování logů Certificate Transparency existuje několik nástrojů. Asi nejznámější je web crt.sh, existuje také nástroj od Facebooku a samotný Cert Spotter je k dispozici také dostupný jako služba. Jedině Cert Spotter je ale svobodným softwarem, který si můžete nainstalovat na svůj server a provádět tak monitoring ve své vlastní režii. Software, který pohání crt.sh, je sice také otevřený, ale vzhledem ke zpracovávání kompletní historie CT logů má netriviální nároky na výkon a kapacitu úložiště.

Trvalé sledování CT logů může pomoci včas odhalit kompromitaci vlastních systémů, je však potřeba dobře odfiltrovat legitimní obnovy certifikátů. Jednou z možností je třeba obnovovat certifikáty kolem nějakého pevného data. Další nepříjemností také může být, že zatímco některé logy se o nově vydaném certifikátu dozví okamžitě od autority, jiné jej zaznamenají až při automatickém robotickém sběru na webu. Důsledkem toho je dvojí informování o vydání téhož certifikátu, což opět zvyšuje úroveň šumu.

Autor článku

Ondřej Caletka vystudoval obor Telekomunikační technika na ČVUT a dnes pracuje ve vzdělávacím oddělení RIPE NCC, mezinárodní asociaci koordinující internetové sítě.

'; document.getElementById('outstream-iframe').onload = function () { setupIframe(); } replayScreen = document.getElementById('iinfoOutstreamReplay'); iinfoOutstreamPosition = document.getElementById('iinfoOutstreamPosition'); outstreamContainer = document.getElementsByClassName('outstream-container')[0]; setupReplayScreen(); } function setupIframe() { outstreamDocument = document.getElementById('outstream-iframe').contentWindow.document; let el = outstreamDocument.createElement('style'); outstreamDocument.head.appendChild(el); el.innerText = "#adContainer>div:nth-of-type(1),#adContainer>div:nth-of-type(1) > iframe { width: 99% !important;height: 99% !important;max-width: 100%;}#videoContent,body{ width:100vw;height:100vh}body{ font-family:'Helvetica Neue',Arial,sans-serif}#videoContent{ overflow:hidden;background:#000}#adMuteBtn{ width:35px;height:35px;border:0;background:0 0;display:none;position:absolute;fill:rgba(230,230,230,1);bottom:-5px;right:25px}"; videoContent = outstreamDocument.getElementById('contentElement'); videoContent.style.display = 'none'; videoContent.volume = 1; videoContent.muted = false; if ( location.href.indexOf('rejstriky.finance.cz') !== -1 || location.href.indexOf('finance-rejstrik') !== -1 || location.href.indexOf('firmy.euro.cz') !== -1 || location.href.indexOf('euro-rejstrik') !== -1 || location.href.indexOf('/rejstrik/') !== -1 || location.href.indexOf('/rejstrik-firem/') !== -1) { outstreamDirectPlayed = true; soundAllowed = true; iinfoVastUrlIndex = 0; } if (!outstreamDirectPlayed) { console.log('OUTSTREAM direct'); setUpIMA(true); } else { if (soundAllowed) { const playPromise = videoContent.play(); if (playPromise !== undefined) { playPromise.then(function () { console.log('OUTSTREAM sound allowed'); setUpIMA(false); }).catch(function () { console.log('OUTSTREAM sound forbidden'); renderBanner(); }); } } else { renderBanner(); } } } function getWrapper() { let articleWrapper = document.querySelector('.rs-outstream-placeholder'); // Outstream Placeholder from RedSys manipulation if (articleWrapper && articleWrapper.style.display !== 'block') { articleWrapper.innerHTML = ""; articleWrapper.style.display = 'block'; } // Don't render OutStream on homepages if (articleWrapper === null) { if (document.querySelector('body.p-index')) { return null; } } if (articleWrapper === null) { articleWrapper = document.getElementById('iinfo-outstream'); } if (articleWrapper === null) { articleWrapper = document.querySelector('.layout-main__content .detail__article p:nth-of-type(6)'); } if (articleWrapper === null) { // Euro, Autobible, Zdravi articleWrapper = document.querySelector('.o-article .o-article__text p:nth-of-type(6)'); } if (articleWrapper === null) { articleWrapper = document.getElementById('sidebar'); } if (!articleWrapper) { console.error("Outstream wrapper of article was not found."); } return articleWrapper; } function setupDimensions() { outstreamWidth = Math.min(iinfoOutstreamPosition.offsetWidth, 480); outstreamHeight = Math.min(iinfoOutstreamPosition.offsetHeight, 320); } /** * Sets up IMA ad display container, ads loader, and makes an ad request. */ function setUpIMA(direct) { google.ima.settings.setDisableCustomPlaybackForIOS10Plus(true); google.ima.settings.setLocale('cs'); google.ima.settings.setNumRedirects(10); // Create the ad display container. createAdDisplayContainer(); // Create ads loader. adsLoader = new google.ima.AdsLoader(adDisplayContainer); // Listen and respond to ads loaded and error events. adsLoader.addEventListener( google.ima.AdsManagerLoadedEvent.Type.ADS_MANAGER_LOADED, onAdsManagerLoaded, false); adsLoader.addEventListener( google.ima.AdErrorEvent.Type.AD_ERROR, onAdError, false); // An event listener to tell the SDK that our content video // is completed so the SDK can play any post-roll ads. const contentEndedListener = function () { adsLoader.contentComplete(); }; videoContent.onended = contentEndedListener; // Request video ads. const adsRequest = new google.ima.AdsRequest(); if (direct) { adsRequest.adTagUrl = directVast; console.log('Outstream DIRECT CAMPAING advert: ' + directVast); videoContent.muted = true; videoContent.volume = 0; outstreamDirectPlayed = true; } else { adsRequest.adTagUrl = iinfoVastUrls[iinfoVastUrlIndex]; console.log('Outstream advert: ' + iinfoVastUrls[iinfoVastUrlIndex]); videoContent.muted = false; videoContent.volume = 1; } // Specify the linear and nonlinear slot sizes. This helps the SDK to // select the correct creative if multiple are returned. // adsRequest.linearAdSlotWidth = outstreamWidth; // adsRequest.linearAdSlotHeight = outstreamHeight; adsRequest.nonLinearAdSlotWidth = 0; adsRequest.nonLinearAdSlotHeight = 0; adsLoader.requestAds(adsRequest); } function setupReplayScreen() { replayScreen.addEventListener('click', function () { iinfoOutstreamPosition.remove(); iinfoVastUrlIndex = 0; outstreamInit(); }); } /** * Sets the 'adContainer' div as the IMA ad display container. */ function createAdDisplayContainer() { // We assume the adContainer is the DOM id of the element that will house // the ads. outstreamDocument.getElementById('videoContent').style.display = 'none'; adDisplayContainer = new google.ima.AdDisplayContainer( outstreamDocument.getElementById('adContainer'), videoContent); } function unmuteAdvert() { adVolume = !adVolume; if (adVolume) { adsManager.setVolume(0.3); outstreamDocument.getElementById('adMuteBtn').innerHTML = ''; } else { adsManager.setVolume(0); outstreamDocument.getElementById('adMuteBtn').innerHTML = ''; } } /** * Loads the video content and initializes IMA ad playback. */ function playAds() { // Initialize the container. Must be done through a user action on mobile // devices. videoContent.load(); adDisplayContainer.initialize(); // setupDimensions(); try { // Initialize the ads manager. Ad rules playlist will start at this time. adsManager.init(1920, 1080, google.ima.ViewMode.NORMAL); // Call play to start showing the ad. Single video and overlay ads will // start at this time; the call will be ignored for ad rules. adsManager.start(); // window.addEventListener('resize', function (event) { // if (adsManager) { // setupDimensions(); // adsManager.resize(outstreamWidth, outstreamHeight, google.ima.ViewMode.NORMAL); // } // }); } catch (adError) { // An error may be thrown if there was a problem with the VAST response. // videoContent.play(); } } /** * Handles the ad manager loading and sets ad event listeners. * @param { !google.ima.AdsManagerLoadedEvent } adsManagerLoadedEvent */ function onAdsManagerLoaded(adsManagerLoadedEvent) { // Get the ads manager. const adsRenderingSettings = new google.ima.AdsRenderingSettings(); adsRenderingSettings.restoreCustomPlaybackStateOnAdBreakComplete = true; adsRenderingSettings.loadVideoTimeout = 12000; // videoContent should be set to the content video element. adsManager = adsManagerLoadedEvent.getAdsManager(videoContent, adsRenderingSettings); // Add listeners to the required events. adsManager.addEventListener(google.ima.AdErrorEvent.Type.AD_ERROR, onAdError); adsManager.addEventListener( google.ima.AdEvent.Type.CONTENT_PAUSE_REQUESTED, onContentPauseRequested); adsManager.addEventListener( google.ima.AdEvent.Type.CONTENT_RESUME_REQUESTED, onContentResumeRequested); adsManager.addEventListener( google.ima.AdEvent.Type.ALL_ADS_COMPLETED, onAdEvent); // Listen to any additional events, if necessary. adsManager.addEventListener(google.ima.AdEvent.Type.LOADED, onAdEvent); adsManager.addEventListener(google.ima.AdEvent.Type.STARTED, onAdEvent); adsManager.addEventListener(google.ima.AdEvent.Type.COMPLETE, onAdEvent); playAds(); } /** * Handles actions taken in response to ad events. * @param { !google.ima.AdEvent } adEvent */ function onAdEvent(adEvent) { // Retrieve the ad from the event. Some events (for example, // ALL_ADS_COMPLETED) don't have ad object associated. const ad = adEvent.getAd(); console.log('Outstream event: ' + adEvent.type); switch (adEvent.type) { case google.ima.AdEvent.Type.LOADED: // This is the first event sent for an ad - it is possible to // determine whether the ad is a video ad or an overlay. if (!ad.isLinear()) { // Position AdDisplayContainer correctly for overlay. // Use ad.width and ad.height. videoContent.play(); } outstreamDocument.getElementById('adContainer').style.width = '100%'; outstreamDocument.getElementById('adContainer').style.maxWidth = '640px'; outstreamDocument.getElementById('adContainer').style.height = '360px'; break; case google.ima.AdEvent.Type.STARTED: window.addEventListener('scroll', onActiveView); // This event indicates the ad has started - the video player // can adjust the UI, for example display a pause button and // remaining time. if (ad.isLinear()) { // For a linear ad, a timer can be started to poll for // the remaining time. intervalTimer = setInterval( function () { // Example: const remainingTime = adsManager.getRemainingTime(); // adsManager.pause(); }, 300); // every 300ms } outstreamDocument.getElementById('adMuteBtn').style.display = 'block'; break; case google.ima.AdEvent.Type.ALL_ADS_COMPLETED: if (ad.isLinear()) { clearInterval(intervalTimer); } if (outstreamLastError === 303) { if (isBanner) { renderBanner(); } else { replayScreen.style.display = 'flex'; } } break; case google.ima.AdEvent.Type.COMPLETE: // This event indicates the ad has finished - the video player // can perform appropriate UI actions, such as removing the timer for // remaining time detection. if (ad.isLinear()) { clearInterval(intervalTimer); } if (isBanner) { renderBanner(); } else { replayScreen.style.display = 'flex'; } break; } } /** * Handles ad errors. * @param { !google.ima.AdErrorEvent } adErrorEvent */ function onAdError(adErrorEvent) { // Handle the error logging. console.log(adErrorEvent.getError()); outstreamLastError = adErrorEvent.getError().getErrorCode(); if (!loadNext()) { renderBanner(); } } function renderBanner() { if (isBanner) { console.log('Outstream: Render Banner'); iinfoOutstreamPosition.innerHTML = ""; iinfoOutstreamPosition.style.height = "330px"; iinfoOutstreamPosition.appendChild(bannerDiv); } else { console.log('Outstream: Banner is not set'); } } function loadNext() { iinfoVastUrlIndex++; if (iinfoVastUrlIndex < iinfoVastUrls.length) { iinfoOutstreamPosition.remove(); outstreamInit(); } else { return false; } adVolume = 1; return true; } /** * Pauses video content and sets up ad UI. */ function onContentPauseRequested() { videoContent.pause(); // This function is where you should setup UI for showing ads (for example, // display ad timer countdown, disable seeking and more.) // setupUIForAds(); } /** * Resumes video content and removes ad UI. */ function onContentResumeRequested() { videoContent.play(); // This function is where you should ensure that your UI is ready // to play content. It is the responsibility of the Publisher to // implement this function when necessary. // setupUIForContent(); } function onActiveView() { if (outstreamContainer) { const containerOffset = outstreamContainer.getBoundingClientRect(); const windowHeight = window.innerHeight; if (containerOffset.top < windowHeight/1 && containerOffset.bottom > 0.0) { if (outstreamPaused) { adsManager.resume(); outstreamPaused = false; } return true; } else { if (!outstreamPaused) { adsManager.pause(); outstreamPaused = true; } } } return false; } let outstreamInitInterval; if (typeof cpexPackage !== "undefined") { outstreamInitInterval = setInterval(tryToInitializeOutstream, 100); } else { const wrapper = getWrapper(); if (wrapper) { let outstreamInitialized = false; window.addEventListener('scroll', () => { if (!outstreamInitialized) { const containerOffset = wrapper.getBoundingClientRect(); const windowHeight = window.innerHeight; if (containerOffset.top < windowHeight / 1 && containerOffset.bottom > 0.0) { outstreamInit(); outstreamInitialized = true; } } }); } } function tryToInitializeOutstream() { const wrapper = getWrapper(); if (wrapper) { const containerOffset = wrapper.getBoundingClientRect(); const windowHeight = window.innerHeight; if (containerOffset.top < windowHeight / 1 && containerOffset.bottom > 0.0) { if (cpexPackage.adserver.displayed) { clearInterval(outstreamInitInterval); outstreamInit(); } } } else { clearInterval(outstreamInitInterval); } } }
OSZAR »