This is an automated email from the git hooks/post-receive script.
richard pushed a commit to branch master in repository tor-browser-bundle-testsuite.
The following commit(s) were added to refs/heads/master by this push: new c2ae1d6 Bug 40035: Added a fonts fingerprinting test c2ae1d6 is described below
commit c2ae1d62243560bd8c5fe289b4fcaa2d774dd5a7 Author: Pier Angelo Vendrame pierov@torproject.org AuthorDate: Tue Mar 1 11:22:09 2022 +0100
Bug 40035: Added a fonts fingerprinting test
Added a test to check that fingerprinting can only detect the fonts we bundle with Tor Browser. The code is readapted from TorZilla Print: https://arkenfox.github.io/TZP/tests/fontlists.html The list should be updated to match the bundled fonts. Also, we ship different fonts in different platforms, so the test is enabled only for Linux.
Added also a test to detect which glyph the browser can render and their size. The code was originally developed by David Fifield and Serge Egelman: https://www.bamsoftware.com/talks/fc15-fontfp/fontfp.html#demo The expected result of the test comes directly from a run on Tor Browser, and we want it to be persistent, rather than having some deisred metrics to match. --- TBBTestSuite/TestSuite/BrowserBundleTests.pm | 7 + marionette/tor_browser_tests/test_fp_fonts.py | 62 +++++++ test-data/fonts/fonts.html | 32 ++++ test-data/fonts/fonts.js | 253 ++++++++++++++++++++++++++ 4 files changed, 354 insertions(+)
diff --git a/TBBTestSuite/TestSuite/BrowserBundleTests.pm b/TBBTestSuite/TestSuite/BrowserBundleTests.pm index 8cf64ce..ae3a484 100644 --- a/TBBTestSuite/TestSuite/BrowserBundleTests.pm +++ b/TBBTestSuite/TestSuite/BrowserBundleTests.pm @@ -374,6 +374,13 @@ our @tests = ( type => 'marionette', descr => 'Check that navigator properties are as expected', }, + { + name => 'fp_fonts', + type => 'marionette', + use_net => 1, + descr => 'Check that we are using only bundled fonts', + enable => sub { $OSNAME eq 'linux' }, + }, { name => 'play_videos', type => 'marionette', diff --git a/marionette/tor_browser_tests/test_fp_fonts.py b/marionette/tor_browser_tests/test_fp_fonts.py new file mode 100644 index 0000000..f0beda9 --- /dev/null +++ b/marionette/tor_browser_tests/test_fp_fonts.py @@ -0,0 +1,62 @@ +import hashlib +import json + +from marionette_harness import MarionetteTestCase + +class Test(MarionetteTestCase): + def test_fp_fonts(self): + m = self.marionette + with m.using_context('content'): + m.set_pref("network.proxy.allow_hijacking_localhost", False) + m.navigate(self.marionette.absolute_url("fonts/fonts.html")) + fonts = m.find_element('id', 'fonts-list').text.strip() + glyphs = m.find_element('id', 'glyphs').text.strip() + + font_list = set(json.loads(fonts)) + bundled_fonts = set([ + "Arimo", + "Cousine", + "Noto Emoji", + "Noto Naskh Arabic", + "Noto Sans Armenian", + "Noto Sans Bengali", + "Noto Sans Buginese", + "Noto Sans Canadian Aboriginal", + "Noto Sans Cherokee", + "Noto Sans Devanagari", + "Noto Sans Ethiopic", + "Noto Sans Georgian", + "Noto Sans Gujarati", + "Noto Sans Gurmukhi", + "Noto Sans Hebrew", + "Noto Sans JP Regular", + "Noto Sans Kannada", + "Noto Sans Khmer", + "Noto Sans KR Regular", + "Noto Sans Lao", + "Noto Sans Malayalam", + "Noto Sans Mongolian", + "Noto Sans Myanmar", + "Noto Sans Oriya", + "Noto Sans SC Regular", + "Noto Sans Sinhala", + "Noto Sans Tamil", + "Noto Sans TC Regular", + "Noto Sans Telugu", + "Noto Sans Thaana", + "Noto Sans Thai", + "Noto Sans Tibetan", + "Noto Sans Yi", + "Noto Serif Armenian", + "Noto Serif Khmer", + "Noto Serif Lao", + "Noto Serif Thai", + "STIX Math", + "Tinos", + "Twemoji Mozilla", + ]) + self.assertEqual(font_list, bundled_fonts) + + glyphs_hash = hashlib.sha256(glyphs.encode('utf-8')).hexdigest() + expected_hash = "5e185a7bd097ecf482fd6a4d8228a9e25974cfbb4bc5f07751b9d09bdebc0f67" + self.assertEqual(glyphs_hash, expected_hash) diff --git a/test-data/fonts/fonts.html b/test-data/fonts/fonts.html new file mode 100644 index 0000000..410b191 --- /dev/null +++ b/test-data/fonts/fonts.html @@ -0,0 +1,32 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8"> + <title>Font fingerprinting test</title> + <style type="text/css"> +#stage-div { + text-align: center; +} + +#stage-span { + font-size: 500%; +} + </style> + </head> + <body> + <div id="stage-div"> + <span id="stage-span"> + <span id="stage-slot"></span> + </span> + </div> + <div id="fonts-list"></div> + <div id="glyphs"></div> + <script src="fonts.js"></script> + <script> + const fonts = JSON.stringify(getFonts()); + const glyphs = JSON.stringify(getUnicode()); + document.querySelector('#fonts-list').textContent = fonts; + document.querySelector('#glyphs').textContent = glyphs; + </script> + </body> +</html> diff --git a/test-data/fonts/fonts.js b/test-data/fonts/fonts.js new file mode 100644 index 0000000..35d9ed2 --- /dev/null +++ b/test-data/fonts/fonts.js @@ -0,0 +1,253 @@ +/** + * Try to fingerprint fonts from a browser. + * + * Original enumeration code from TorZillaPrint and released under the + * MIT License: https://github.com/arkenfox/TZP + * Original Unicode glyphs fingerprint code from + * https://www.bamsoftware.com/talks/fc15-fontfp/fontfp.html#demo + */ + +const fntStrA = 'mmmLLLmmmWWWwwwmmmllliii' + +const fntOther = { + android: ['Droid Sans', 'Droid Sans Mono', 'Droid Serif', 'Noto Color Emoji', 'Noto Emoji', 'Noto Kufi Arabic', 'Noto Mono', 'Noto Naskh Arabic', 'Noto Nastaliq Urdu', 'Noto Sans', 'Noto Sans Adlam', 'Noto Sans Adlam Unjoined', 'Noto Sans Anatolian Hieroglyphs', 'Noto Sans Arabic', 'Noto Sans Armenian', 'Noto Sans Avestan', 'Noto Sans Balinese', 'Noto Sans Bamum', 'Noto Sans Batak', 'Noto Sans Bengali', 'Noto Sans Brahmi', 'Noto Sans Buginese', 'Noto Sans Buhid', 'Noto Sans CJK JP', 'N [...] + linux: ['AR PL UKai CN', 'AR PL UKai HK', 'AR PL UKai TW', 'AR PL UKai TW MBE', 'AR PL UMing CN', 'AR PL UMing HK', 'AR PL UMing TW', 'AR PL UMing TW MBE', 'Abyssinica SIL', 'Aharoni CLM', 'AlArabiya', 'AlBattar', 'AlHor', 'AlManzomah', 'AlYarmook', 'Amiri', 'Amiri Quran', 'Amiri Quran Colored', 'Ani', 'AnjaliOldLipi', 'Arab', 'Arial', 'Arimo', 'Bitstream Charter', 'C059', 'Caladea', 'Caladings CLM', 'Cantarell', 'Cantarell Extra Bold', 'Cantarell Light', 'Cantarell Thin', 'Carlito', ' [...] + mac: ['American Typewriter Condensed', 'American Typewriter Condensed Light', 'American Typewriter Light', 'American Typewriter Semibold', 'Apple Braille Outline 6 Dot', 'Apple Braille Outline 8 Dot', 'Apple Braille Pinpoint 6 Dot', 'Apple Braille Pinpoint 8 Dot', 'Apple LiGothic Medium', 'Apple LiSung Light', 'Apple SD Gothic Neo Heavy', 'Apple SD Gothic Neo Light', 'Apple SD Gothic Neo Medium', 'Apple SD Gothic Neo SemiBold', 'Apple SD Gothic Neo UltraLight', 'Apple SD GothicNeo Extr [...] + windows: ['Aharoni Bold', 'Aldhabi', 'Andalus', 'Angsana New', 'AngsanaUPC', 'Aparajita', 'Arabic Typesetting', 'Arial Nova', 'Arial Nova Cond', 'Arial Nova Cond Light', 'Arial Nova Light', 'Arial Unicode MS', 'BIZ UDGothic', 'BIZ UDMincho', 'BIZ UDMincho Medium', 'BIZ UDPGothic', 'BIZ UDPMincho', 'BIZ UDPMincho Medium', 'Batang', 'BatangChe', 'Browallia New', 'BrowalliaUPC', 'Cordia New', 'CordiaUPC', 'DFKai-SB', 'DaunPenh', 'David', 'DengXian', 'DengXian Light', 'DilleniaUPC', 'Dille [...] +} +const fntBase = { + android: [], + linux: [], + mac: ['Al Bayan', 'Al Nile', 'Al Tarikh', 'American Typewriter', 'Andale Mono', 'Apple Braille', 'Apple Chancery', 'Apple Color Emoji', 'Apple SD Gothic Neo', 'Apple Symbols', 'AppleGothic', 'AppleMyungjo', 'Arial', 'Arial Black', 'Arial Hebrew', 'Arial Hebrew Scholar', 'Arial Narrow', 'Arial Rounded MT Bold', 'Arial Unicode MS', 'Avenir', 'Avenir Black', 'Avenir Black Oblique', 'Avenir Book', 'Avenir Heavy', 'Avenir Light', 'Avenir Medium', 'Avenir Next', 'Avenir Next Demi Bold', 'Ave [...] + windows: ['AlternateGothic2 BT', 'Arial', 'Arial Black', 'Arial Narrow', 'Bahnschrift', 'Bahnschrift Light', 'Bahnschrift SemiBold', 'Bahnschrift SemiLight', 'Calibri', 'Calibri Light', 'Calibri Light Italic', 'Cambria', 'Cambria Math', 'Candara', 'Candara Light', 'Comic Sans MS', 'Consolas', 'Constantia', 'Corbel', 'Corbel Light', 'Courier New', 'Ebrima', 'Franklin Gothic Medium', 'Gabriola', 'Gadugi', 'Georgia', 'HoloLens MDL2 Assets', 'Impact', 'Javanese Text', 'Leelawadee UI', 'Lee [...] +} +const fntAlways = { + // note: add mozilla bundled fonts here: ToDo: make sure they are not bundled with mac/android + android: [], + linux: ['EmojiOne Mozilla', 'Twemoji Mozilla'], + mac: [], + windows: ['Courier', 'EmojiOne Mozilla', 'Helvetica', 'MS Sans Serif', 'MS Serif', 'Roman', 'Small Fonts', 'Times', 'Twemoji Mozilla', '宋体', '微软雅黑', '新細明體', '細明體', '굴림', '굴림체', '바탕', 'MS ゴシック', 'MS 明朝', 'MS Pゴシック', 'MS P明朝'] +} +const fntTB = { + android: [], + linux: [], + // mac ToDo: move bundled items to fntTBBundled + mac: ['AppleGothic', 'Apple Color Emoji', 'Arial', 'Arial Black', 'Arial Narrow', 'Courier', 'Geneva', 'Georgia', 'Heiti TC', 'Helvetica', 'Helvetica Neue', '.Helvetica Neue DeskInterface', 'Hiragino Kaku Gothic ProN', 'Hiragino Kaku Gothic ProN W3', 'Hiragino Kaku Gothic ProN W6', 'Lucida Grande', 'Monaco', 'Noto Sans Armenian', 'Noto Sans Bengali', 'Noto Sans Buginese', 'Noto Sans Canadian Aboriginal', 'Noto Sans Cherokee', 'Noto Sans Devanagari', 'Noto Sans Ethiopic', 'Noto Sans Guj [...] + windows: ['Arial', 'Arial Black', 'Arial Narrow', 'Batang', 'Cambria Math', 'Courier New', 'Euphemia', 'Gautami', 'Georgia', 'Gulim', 'GulimChe', 'Iskoola Pota', 'Kalinga', 'Kartika', 'Latha', 'Lucida Console', 'MS Gothic', 'MV Boli', 'Malgun Gothic', 'Malgun Gothic Semilight', 'Mangal', 'Meiryo', 'Meiryo UI', 'Microsoft Himalaya', 'Microsoft JhengHei', 'Microsoft JhengHei UI', 'Microsoft JhengHei UI Light', 'Microsoft YaHei', 'Microsoft YaHei Light', 'Microsoft YaHei UI', 'Microsoft Y [...] +} +const fntTBBundled = { + android: [], + linux: [], + mac: [], + windows: ['Noto Sans Buginese', 'Noto Sans Khmer', 'Noto Sans Lao', 'Noto Sans Myanmar', 'Noto Sans Yi'] +} +const fntList = [fntOther, fntBase, fntAlways, fntTB, fntTBBundled].map(lists => { + return Object.values(lists).reduce((all, list) => all.concat(list), []) +}).reduce((all, list) => all.concat(list), []) + +const createLieDetector = () => { + // https://github.com/abrahamjuliot/creepjs + const invalidDimensions = [] + return { + getInvalidDimensions: () => invalidDimensions, + compute: ({ + width, + height, + transformWidth, + transformHeight, + perspectiveWidth, + perspectiveHeight, + sizeWidth, + sizeHeight, + scrollWidth, + scrollHeight, + offsetWidth, + offsetHeight, + clientWidth, + clientHeight + }) => { + const invalid = ( + width !== transformWidth || + width !== perspectiveWidth || + width !== sizeWidth || + width !== scrollWidth || + width !== offsetWidth || + width !== clientWidth || + height !== transformHeight || + height !== perspectiveHeight || + height !== sizeHeight || + height !== scrollHeight || + height !== offsetHeight || + height !== clientHeight + ) + if (invalid) { + invalidDimensions.push({ + width: [width, transformWidth, perspectiveWidth, sizeWidth, scrollWidth, offsetWidth, clientWidth], + height: [height, transformHeight, perspectiveHeight, sizeHeight, scrollHeight, offsetHeight, clientHeight] + }) + } + } + } +} + +const getFonts = () => { + /* https://github.com/abrahamjuliot/creepjs */ + const detectLies = createLieDetector() + const doc = document // or iframe.contentWindow.document + const id = 'font-fingerprint' + const div = doc.createElement('div') + div.setAttribute('id', id) + doc.body.appendChild(div) + doc.getElementById(id).innerHTML = ` + <style> + #${id}-detector { + --font: ''; + position: absolute !important; + left: -9999px!important; + font-size: 256px !important; + font-style: normal !important; + font-weight: normal !important; + letter-spacing: normal !important; + line-break: auto !important; + line-height: normal !important; + text-transform: none !important; + text-align: left !important; + text-decoration: none !important; + text-shadow: none !important; + white-space: normal !important; + word-break: normal !important; + word-spacing: normal !important; + /* in order to test scrollWidth, clientWidth, etc. */ + padding: 0 !important; + margin: 0 !important; + /* in order to test inlineSize and blockSize */ + writing-mode: horizontal-tb !important; + /* for transform and perspective */ + transform-origin: unset !important; + perspective-origin: unset !important; + } + #${id}-detector::after { + font-family: var(--font); + content: '` + fntStrA + `'; + } + </style> + <span id="${id}-detector"></span>` + + const span = doc.getElementById(`${id}-detector`) + const pixelsToInt = pixels => Math.round(+pixels.replace('px', '')) + const originPixelsToInt = pixels => Math.round(2 * pixels.replace('px', '')) + const allFonts = new Set() + const detectedViaPixel = new Set() + const detectedViaPixelSize = new Set() + const detectedViaScroll = new Set() + const detectedViaOffset = new Set() + const detectedViaClient = new Set() + const detectedViaTransform = new Set() + const detectedViaPerspective = new Set() + const baseFonts = ['monospace', 'sans-serif', 'serif'] + const style = getComputedStyle(span) + + const getDimensions = (span, style) => { + const transform = style.transformOrigin.split(' ') + const perspective = style.perspectiveOrigin.split(' ') + const dimensions = { + width: pixelsToInt(style.width), + height: pixelsToInt(style.height), + transformWidth: originPixelsToInt(transform[0]), + transformHeight: originPixelsToInt(transform[1]), + perspectiveWidth: originPixelsToInt(perspective[0]), + perspectiveHeight: originPixelsToInt(perspective[1]), + sizeWidth: pixelsToInt(style.inlineSize), + sizeHeight: pixelsToInt(style.blockSize), + scrollWidth: span.scrollWidth, + scrollHeight: span.scrollHeight, + offsetWidth: span.offsetWidth, + offsetHeight: span.offsetHeight, + clientWidth: span.clientWidth, + clientHeight: span.clientHeight + } + return dimensions + } + const base = baseFonts.reduce((acc, font) => { + span.style.setProperty('--font', font) + const dimensions = getDimensions(span, style) + detectLies.compute(dimensions) + acc[font] = dimensions + return acc + }, {}) + + const families = fntList.reduce((acc, font) => { + baseFonts.forEach(baseFont => acc.push(`'${font}', ${baseFont}`)) + return acc + }, []) + + families.forEach(family => { + span.style.setProperty('--font', family) + const basefont = /, (.+)/.exec(family)[1] + const style = getComputedStyle(span) + const dimensions = getDimensions(span, style) + detectLies.compute(dimensions) + const font = /'(.+)'/.exec(family)[1] + if (dimensions.width !== base[basefont].width || + dimensions.height !== base[basefont].height) { + detectedViaPixel.add(font) + allFonts.add(font) + } + if (dimensions.sizeWidth !== base[basefont].sizeWidth || + dimensions.sizeHeight !== base[basefont].sizeHeight) { + detectedViaPixelSize.add(font) + allFonts.add(font) + } + if (dimensions.scrollWidth !== base[basefont].scrollWidth || + dimensions.scrollHeight !== base[basefont].scrollHeight) { + detectedViaScroll.add(font) + allFonts.add(font) + } + if (dimensions.offsetWidth !== base[basefont].offsetWidth || + dimensions.offsetHeight !== base[basefont].offsetHeight) { + detectedViaOffset.add(font) + allFonts.add(font) + } + if (dimensions.clientWidth !== base[basefont].clientWidth || + dimensions.clientHeight !== base[basefont].clientHeight) { + detectedViaClient.add(font) + allFonts.add(font) + } + if (dimensions.transformWidth !== base[basefont].transformWidth || + dimensions.transformHeight !== base[basefont].transformHeight) { + detectedViaTransform.add(font) + allFonts.add(font) + } + if (dimensions.perspectiveWidth !== base[basefont].perspectiveWidth || + dimensions.perspectiveHeight !== base[basefont].perspectiveHeight) { + detectedViaPerspective.add(font) + allFonts.add(font) + } + }) + + return Array.from(allFonts.values()) +} + +const getUnicode = () => { + // code based on work by David Fifield (dcf) and Serge Egelman (2015) + // https://www.bamsoftware.com/talks/fc15-fontfp/fontfp.html#demo + + const styles = ['default', 'sans-serif', 'serif', 'monospace', 'cursive', 'fantasy'] + const codepoints = [0x20B9, 0x2581, 0x20BA, 0xA73D, 0xFFFD, 0x20B8, 0x05C6, 0x1E9E, 0x097F, 0xF003, 0x1CDA, 0x17DD, 0x23AE, 0x0D02, 0x0B82, 0x115A, 0x2425, 0x302E, 0xA830, 0x2B06, 0x21E4, 0x20BD, 0x2C7B, 0x20B0, 0xFBEE, 0xF810, 0xFFFF, 0x007F, 0x10A0, 0x1D790, 0x0700, 0x1950, 0x3095, 0x532D, 0x061C, 0x20E3, 0xFFF9, 0x0218, 0x058F, 0x08E4, 0x09B3, 0x1C50, 0x2619] + + const stageDiv = document.getElementById('stage-div') + const stageSpan = document.getElementById('stage-span') + const stageSlot = document.getElementById('stage-slot') + + const results = {} + for (const cp of codepoints) { + results[cp] = {} + for (const style of styles) { + stageSlot.style.fontFamily = style === 'default' ? '' : style + stageSlot.textContent = String.fromCodePoint(cp) + results[cp][style] = { width: stageSpan.offsetWidth, height: stageDiv.offsetHeight } + } + } + + return results +}