arkenfox / TZP

https://arkenfox.github.io/TZP
MIT License
8 stars 0 forks source link

webgl #234

Open Thorin-Oakenpants opened 1 year ago

Thorin-Oakenpants commented 1 year ago

@abrahamjuliot - is this uptodate? https://gist.github.com/abrahamjuliot/7baf3be8c451d23f7a8693d7e28a35e2

I don't want to re-invent the wheel, so want to use your code (accredited) if that's OK - please advise

ATM, I only want to collect the parameters, extensions, vendor, etc - not any actual rendering


webgl

so some questions

Q1

https://gist.github.com/abrahamjuliot/7baf3be8c451d23f7a8693d7e28a35e2#file-webgl-js-L91-L94

        let canvas = {}
        let context = {}
        try {
            canvas = document.createElement('canvas')
            context = canvas.getContext(type) || canvas.getContext('experimental-' + type)

so here, I wanted to use an already created canvas (I think it saves time)

        let canvas = {}
    let context = {}
    try {
        canvas = dom.webglcanvas
        context = canvas.getContext(type) || canvas.getContext('experimental-' + type)

but I get errors for webgl2 (webgl is fine)

errors

I also don't see where you remove and cleanup the document.createElement('canvas') - remember, TZP allows section reruns so I don't want to end up with dozens of canvas elements


Q2

what is newmethods meant to signify in webgl2 ?


Q3

for data collection/entropy - I am not webgl savvy - but remember in the old TZP repo we discussed, and I mocked up, a webgl section that would return webgl, webgl2, experimental results - a bit like

how is this all reflected in the objects in the console? I'm a bit lost


Q4

how/what are you doing with this data to only show one set of parameters on creepy - I can see that some are identical in webgl 1 vs 2 - and there is a diffs output. Are you merging these?


Q5?

I guess I'll have to play to catch errors and return that - you seem to just console them and return undefined (I guess I could do that)

Thorin-Oakenpants commented 1 year ago

Q1 - answered: I just needed two canvas elements

        <div>
            <canvas id="webglcanvas"></canvas>
            <canvas id="webgl2canvas"></canvas>
        </div>

is there some benefit to creating the canvas on the fly?

abrahamjuliot commented 1 year ago

The gist is good. Always welcome... All credit for that one goes to https://github.com/CesiumGS/webglreport. My refactoring is minimal.

There are a few techniques not in there, but worth checking out. These are mostly experimental ideas that can be used to determine trustworthy vs. suspicious GPUs.

// Highlight common GPU brands
// gpu can be a string of the renderer or both the vendor and renderer
function getGpuBrand(gpu) {
  if (!gpu) return
  const gpuBrandMatcher = /(adreno|amd|apple|intel|llvm|mali|microsoft|nvidia|parallels|powervr|samsung|swiftshader|virtualbox|vmware)/i

  const brand = (
    /radeon/i.test(gpu) ? 'AMD' :
    /geforce/i.test(gpu) ? 'NVIDIA' :
    ( (gpuBrandMatcher.exec(gpu) || [])[0] || 'Other' )
  )

  return brand
}

// Takes the parameter object and generate a fingerprint of sorted numeric values
// This can be used to create a known good hash table and known brands can be labelled for each hash.
function generateParamFingerprint(parameters) {
  if (!parameters) return
  return '' + [
    ...new Set(Object.values(parameters)
      .filter((val) => val && typeof val != 'string')
      .flat()
      .map((val) => Number(val))),
  ].sort((a, b) => (a - b))
}
abrahamjuliot commented 1 year ago

Q1

A premade canvas in the HTML can sometimes bypass scripts observing createElement('canvas'). Optionally, one context is good. webgl2 has more parameters. Since we are not appending the element to the page, a new document.createElement('canvas') on repeat re-runs should be fine.

abrahamjuliot commented 1 year ago

Q2

newMethods is just functions in WebGLRenderingContext.prototype. It's not that great for fingerprinting (compared to CSS and the Window). It just current browser features in the prototype.

abrahamjuliot commented 1 year ago

Q3

I forgot to add the sections like "Vertex Shader" and "Rasterizer", but these can be determined based on the property names. We just need to create a map.

parameterType = {
  'Vertex Shader': [
     'MAX_VERTEX_ATTRIBS',
     'MAX_VERTEX_TEXTURE_IMAGE_UNITS',
     'MAX_VERTEX_UNIFORM_VECTORS',
  ]
}

if (ParameterType['Vertex Shader'].includes(propertyName)) {
  // add to Vertex Shader section
}
abrahamjuliot commented 1 year ago

Q4

I merge as a final simplification, but also check and flag mismatch behind the scenes. I recall an incident where a mismatch was valid, so I just treat it as questionable.

abrahamjuliot commented 1 year ago

Q5

We can trap the error and push to a global collection. There are a few places different errors might get thrown due to users disabling or blocking the API, or parts of the API.

Thorin-Oakenpants commented 1 year ago

I merge as a final simplification

hmmm, so I'm not sure how I want to present all this info - where is all the experimental stuff? I mean we can see webgl and webgl2. e.g. here's just checking for support. https://browserleaks.com/webgl has THREE toggles. Is this info in there (sorry for being a lazy bum but not into learning about webgl right now, lol) or it something we need to add

e.g.

let types = ["webgl", "webgl2", "experimental-webgl"]
types.forEach(function(type){
    try {
        let canvas = window.document.createElement("canvas")
        try {
            var context = canvas.getContext(type)
            if (!context) {
                throw new Error()
            }
            console.log(type, "supported")
        }   catch(e) {
            console.log(type, "not supported")
        }
    } catch(e) {
        console.log("canvas failed")
    }
})

I think I can just take the objects and pull things out - I need to check various things as RFP protected, so they need to be separate.

Since we are not appending the element to the page,

doh. nothing to remove. nice. BTW not creating the canvas is a tiny perf win :) woo!

Thorin-Oakenpants commented 1 year ago

https://github.com/arkenfox/TZP/issues/234#issuecomment-1495196774

can you add that to your gist so I can copy it, thanks

abrahamjuliot commented 1 year ago

Refactored: https://gist.github.com/abrahamjuliot/7baf3be8c451d23f7a8693d7e28a35e2

Thorin-Oakenpants commented 1 year ago

awesome, thx

Thorin-Oakenpants commented 1 year ago

the gpuBrand is redundant (but nice) - I can drop that

the gpuHash doesn't make much sense to me - sorting values could/would create collisions ? for example on it's own

This doesn't make a good fingerprint for max entropy, but I get it, it's a nice wee string. Unless I'm missing something

Thorin-Oakenpants commented 1 year ago

gpu doesn't handle an empty string, returns ,

    let gpuV = cleanFn(parameters.UNMASKED_VENDOR_WEBGL),
        gpuR = cleanFn(parameters.UNMASKED_RENDERER_WEBGL)
    const gpu = String([gpuV, gpuR])

edit: well, it does handle it, it's just not very reader friendly :)

Thorin-Oakenpants commented 1 year ago

I'm confused. Top is TB (which has RFP webgl mitigations - you get the same result in FF with RFP enabled), bottom is nightly with no RFP.

The bottom test looks fine, but at first glance seem to have duplication, but the top test - why are we not returning a debugRendererInfo object with undefined items (and extensions that mimic this RFP behavior). Why is webglcontext.renderer different to gpuRenderer ?

https://bugzilla.mozilla.org/show_bug.cgi?id=1337157 - lemme look at this. I know it's supposed to return undefined or blanks and/or Mozilla

i-am-a-bit-confused

    const Categories = {
        'debugRendererInfo': [
            'UNMASKED_VENDOR_WEBGL',
            'UNMASKED_RENDERER_WEBGL',
        ], // snip
    }

    parameters = { // snip
        UNMASKED_VENDOR_WEBGL: getUnmasked(context, 'UNMASKED_VENDOR_WEBGL'),
        UNMASKED_RENDERER_WEBGL: getUnmasked(context, 'UNMASKED_RENDERER_WEBGL')
    }
    parameters.DIRECT_3D = /Direct3D|D3D(\d+)/.test(parameters.UNMASKED_RENDERER_WEBGL)

    gpuVendor = parameters.UNMASKED_VENDOR_WEBGL
    gpuRenderer = parameters.UNMASKED_RENDERER_WEBGL
Thorin-Oakenpants commented 1 year ago

so gpu (which I split into gpuVendor and gpuRenderer) is redundant then? right? It's a direct lookup of debuginfo? right?

abrahamjuliot commented 1 year ago

Yeah, gpu is non-essential. I forgot. GPU showing up under render is this issue. It can be ignored, but the way to around is to feature detect the version and then use renderer instead of debug info, but its only necessary to remove the console warning.

https://github.com/mdn/content/issues/12689

abrahamjuliot commented 1 year ago

gpuHash

This too is not needed.

It can be useful to put known good fingerprints into a lookup table (similar to known good audio sums). Clashing is likely and okay, as long random WebGL fingerprints find it hard to fit in. I'm guessing, based on limited data on CreepJS, the table size would require no more than 100 hashes. Everything unknown can be questioned until it is established trustworthy. This is more of a test concept, I suppose. It would require many samples.

Thorin-Oakenpants commented 1 year ago

tis all cool. The RFP notation should be pretty simple now I refreshed my memory as to what we were doing with it - there's more to it than just vendor/renderer. But easy to check the few places it shows.

but its only necessary to remove the console warning

I log all errors as part of the error entropy :)

gpu/gpuHash/gpuBrand

no worries. I get it was experimental - but this is gecko and all I care about is Tor Browser and RFP. If I check prototype lies (I'll need to check) then I can just return the whole thing as useless (might revisit later)

when we collect data from surveys (via a Tor Browser annual or bi-annual FP drive - 100% opt-in with one test per profile), we can just reject collecting tainted data based on prototype lies alone - i.e either it is all empty or it matches NoScripts signature

abrahamjuliot commented 1 year ago

There's also these anti-detection browsers that fake the GPUs at engine level. No prototype lies. For Firefox, I think they use what is called "Stealthfox Browser". The only way I'm aware of to detect them is by using a look-up table of known good hashes. WebGL is too high entropy for a local lookup, but this lower entropy hash works. I have not used it on CreepJS, but I have the data visually from last 60 days of bad traffic.

Thorin-Oakenpants commented 1 year ago

gummy bear browsers :) lols ... I'm not too worried about it TBH, I need to focus on actual TB and FF + RFP users

abrahamjuliot commented 1 year ago

Something I discovered, many of them have a frozen max stack size fingerprint. Something with the way it's compiled, I guess.

Thorin-Oakenpants commented 1 year ago

I'm not entirely sure what you mean by that, tell me more :) - you had me at "something"

Yeah, math PoCs are great, like known pixel tests, joining chars vs individual chars in textmetrics width, domrect etc. Enumerating goodness is hard and no bulletproof - so I understand the mini simplified hash - like the simplified (less measurements) of https://arkenfox.github.io/TZP/tests/domrectspoofratio.html that covers multiple chrome results

I just had to drop enumerating goodness for audio in FF (TZP 2.0 is gecko only) - math library changes on android, and they will change again, and then RFP is also doing something with it (or it will apply for all) - https://bugzilla.mozilla.org/show_bug.cgi?id=1358149#c26

Thorin-Oakenpants commented 1 year ago

you had me at "something"

do you mean recursion, stack depth, rather than some webgl thing?

abrahamjuliot commented 1 year ago

Yeah, recursion, stack depth. I have not tested for Firefox, but it's somewhat of an unbeatable fingerprint for custom Chrome builds that are slow to update.

Thorin-Oakenpants commented 1 year ago

OT: but I had an email exchange with a moz dev about this, and it's a good (fuzzy) FP for determining ion and jit etc