Page MenuHomePhabricator

Test trust tokens as a captcha alternative for Wikimedia
Open, Needs TriagePublic

Description

Trust tokens (WICG explainer, web.dev, spec, tutorial) are a new web API (a part of Google's privacy sandbox initiative) that lets the browser determine whether the user can be trusted (ie. not a spammer or similar bad actor), without the browser and the website exchanging private information. Instead, the browser transmits attestations (trust tokens) from issuers (such as Google) which apply whatever invasive tracking they would apply anyway to distinguish humans from spambots, to publishers (such as Wikipedia) with a cryptographic protocol that ensures the issuer and the publisher don’t learn about each other’s identity or the user’s identity on the other one’s website.

Given Wikimedia's deeply disfunctional current antispam system (see T241921: Fix Wikimedia captchas), we should look into this option. While - based on past discussions - there might be some community concerns around pressuring users to opt into invasive tracking by creating a more pleasant registration experience for them if they do, and creating a bias against those who don’t, on the net this still seems like a great replacement for captchas. For users with trust tokens enabled (currently a random 10% of Chrome users, in the future maybe most users) the antispam verification process would be completely invisible; for others we’d fall back to the current captchas.

Trust tokens are currently supported in Chrome in origin trial mode, with full release expected in May. We should run a controlled experiment to see how well they filter spambots in practice.

Event Timeline

Rough guess of the work needed:

  • research the Trust Tokens specification, and plan out how it can be integrated with MediaWiki
  • maybe run some lightweight RfC or community discussion to see if there are strong objections to the idea
  • refactor ConfirmEdit so it’s possible to have a fallback chain of captchas T179635: Allow captchas to be stacked
  • implement trust tokens as a ConfirmEdits captcha variant
  • maybe improve logging for captchas (we track crude success/failure rate, but we might want something more informative for the experiment)
  • enable on (some?) Wikimedia wikis for a short period of time, see if spambot problems get worse or if it's otherwise problematic

As noted within the web.dev article, from the explainer:

This API proposes a new per-origin storage area for "Privacy Pass" style cryptographic tokens, which are accessible in third party contexts.

It doesn't seem immediately obvious to me what Trust Tokens would provide over say, the WMF and/or Community running a Privacy Pass server and integrating that with ConfirmEdit. Or potentially integrating with a third party service like hCaptcha (T250227) which already supports Privacy Pass via Cloudflare, assuming such an integration could be both adequately resourced and would satisfy any privacy/third party concerns.

Running our own Privacy Pass server doesn't really help with the problem that we can't reliably determine which devices can be trusted; at best it would be a way to decouple captchas from MediaWiki (which is nice in theory but not really worth the effort) or to provide anonymous trust assesments to third parties about already-vetted Wikimedia users, ie. the opposite direction to what's proposed here.

Wrt. integrating with some Privacy Pass provider, AIUI (but I haven't done much reading yet) it would be roughly equivalent but it would require a browser extension, while the Trust Tokens API will just work for a large fraction of our userbase. If the two are similar enough in practice, no reason not to support both, but Trust Tokens seems distinctly more useful (conditional on it actually coming out of the origin trial soonish).

The questions I'd want answered before starting the coding part of the work:

  • what are the exact steps an implementation has to do? (from my skimming of some docs: do a special fake AJAX request to Google / the issuer, make a special AJAX request to the Wikipedia servers, on the server side read some headers from that and send the values to Google / the issuer for confirmation - is that correct?)
  • what is the exact state of browser support?
  • what is the exact state of issuer support (are there other issuers than Google)?
  • the spec references PrivacyPass a lot. Is it actually compatible with PrivacyPass (to the point that a PrivacyPass issuer is automatically a trust token issuer as well) or did it reimplement PrivacyPass in an incompatible way? Either case, what work would be needed to support PrivacyPass as well?
  • what are the exact privacy properties? (After an attempted trust token based user registration, what information does Wikipedia learn about the user? What information does Google learn about the user, given especially that user registrations are public with a timestamp that's accurate to the second?
  • are there any effectiveness concerns? E.g. if someone pays a mechanical turk to solve captchas to mine trust tokens, will that be more cost-effective than paying someone to solve Wikipedia captchas directly? (How many tokens do you get per solved captcha?)
  • are there any accessibility concerns?
  • are there any security concerns? I can't imagine there would be, but maybe the spec has some warnings.
  • are there any nontrivial legal implications (e.g. terms of use for the services involved)?

PrivacyPass is being standardized by an IETF working group; there are currently three draft documents: protocol, architecture, API. The protocol is based on VOPRFs, a mathematical construct where a client and a server collaboratively calculate f(x,y) for some hard-to-reverse function f in such a way that the client provides x, the server y, neither learn anything about what value the other provided, and the server doesn't learn anything about the result of the computation. Thus, the server can issue a token to the client without knowing the value of that token, but can still verify the token's authenticity when it's returned for redeeming.

This can be implemented with various ciphersuites; the original implementation used in the PrivacyPass browser extension uses blinding based on elliptic curves; the math is sketched up here.

Communication between the parties happens via a Sec-Privacy-Pass: type=<action>, body=<message> HTTP header added to web requests (whose content is otherwise irrelevant). The body is a byte stream, described here. From the point of view of a verifier (in our case, Google or hCaptcha would be the server, the browser the client and Wikipedia the verifier), either the client hands the token over to the verifier, the verifier calls the redemption API of the server, and the server tells whether the token was valid or not; or the client calls the server directly, the server returns a signed attestation, and the client passes that to the verifier which checks the signature. (Confusingly, the API draft calls the first variant direct redemption and the second one delegated redemption, while the protocol draft calls the first delegated verifier and the second async verifier.)

The spec discusses some privacy considerations but they are all about the server's ability to deanonymize users by segmenting them with varied crypto parameters (different public keys, crypto suites), which is publicly detectable so probably not a realistic threat for an actor with a valuable brand such as Google. The document also warns against having too many servers (in which case the verifier can profile users by checking which servers they have tokens from), so while in theory the protocol is decentralized, in practice it's expected to only have a very small number of organizations issuing tokens.

There is no discussion of the privacy implications of communicating with the server (e.g. in the async verifier version, the server can correlate the timing of issuing the attestation with Wikipedia account creation times. It has no way to tell whether the attestation is used on Wikipedia or some other website, so this is not too bad, but still a nontrivial amount of information leak.)

There are a few implementations of the PrivacyPass protocol that are unrealted to captchas (e.g. it's used for the Brave browser's Basic Attention Token) but only two relevant ones: a browser extension used for communicating trust between CloudFlare and hCaptcha (originally built for Tor-enabled browsers, to reduce the number of times they get captcha challanges without deanonymizing them) and Chrome's Trust Tokens API.

The PrivacyPass browser extension (available for Chrome and Firefox) seems to use a different communication protocol from the IETF draft, according to its documentation here (e.g. uses base64 encoding, uses request/response body instead of headers for issuance). The browser extensions and the server that issues tokens are opensource (BSD; the server is written in Go). I didn't find any public code for the verifier API (which is the part Wikipedia would have to implement), only the aforementioned communication protocol doc and the description of CloudFlare's API.

The extension supports two issuers, hCaptcha and CloudFlare (which is also using hCaptcha but issues its own tokens). CloudFlare issues 30 tokens per solved captcha, hCaptcha 5 tokens.

The Trust Tokens API seems to mostly be in line in the IETF draft (except the header names are different). The verifier, e.g. Wikipedia, makes a special web request like

fetch( '<issuer>/.well-known/trust-token', {
    trustToken: {
        type: 'token-redemption',
        issuer: <issuer>
    }
} );

If there are no trust tokens available from the issuer, the request fails immediately. Otherwise, it is sent to the issuer with the appropriate headers, the token gets redeemed, and an attestation returned (ie. this is the "async verifier" version of the protocol). Crucially, this means every captcha check would result in the browser sending a web request to Google or whatever issuer we use, although unlike traditional third-party captchas the issuer would have no way to tell whether the browser is making the request in the context of Wikipedia or some other site. In other words, this would only preserve anonymity efficiently if Wikipedia is only a tiny fraction of all trust token redemption traffic, given our public user registration timestamps.

The server will return with a "redemption record", a proof that the token was valid. This is inaccessible to normal Javascript but can be sent to the verifier site with another custom fetch:

fetch( <some-Wikipedia-url>, {
  trustToken: {
    type: 'send-redemption-record',
    issuers: [ <issuer>, ... ]
  }
} );

This request will have a Sec-Redemption-Record header containing the redemption record. The spec leaves it to the implementers what the redemption record contains and how it's supposed to be verified. Google's version is described here.

The Trust Token API is only implemented by Chrome, in Origin Trial mode (ie. either the web page has to opt in, or the user has to opt in via chrome::/flags), enabled for 50% of Dev/Canary/Beta Chrome users and 10% of Stable Chrome users. The experiment was slated to end just about now, but seems like it's being extended (Chrome Platform status currently specifies the experiment endpoint as Chrome 94, which is ~July). The current list of issuers can be seen here. Most of them are experiments or adtech; the only one really relevant for us is reCAPTCHA. (hCaptcha doesn't seem to be participating in the trial currently.)

A test site is available at https://trusttoken.dev/ .

I tried my best to get this working with reCAPTCHA but failed:

fetch('https://recaptcha.google.com/.well-known/trust-token', {trustToken: {type: 'token-request' }});

results in a response with some trust token data, but it causes an error, and the developer toolbar's trust token tab says: "The servers response was malformed or otherwise invalid." Not sure if I'm doing something wrong or the API is meant for an earlier version of the spec or otherwise incorrect. (The reCAPTCHA trial was internal, with no public documentation on how to communicate with the issuer.) https://trusttoken.dev/ works fine so it's not a client issue.

Solving reCAPTCHA captchas (via https://www.google.com/recaptcha/api2/demo ) doesn't do anything special either.

Browsing around a bit, including some sites that require reCAPTCHA, I see I have collected trust tokens from adservice.google.com, but not any other Google web property. It gives the same error though when I try to trigger it manually, both for issuance and redemption (despite the response data seemingly containing a token/assertion). So I'm probably doing something wrong.

After some experimentation, adservice.google.com works with a fetch as long as it's credentialed (which makes sense since otherwise it can't determine the user is authentic, although the error message is still confusing). recaptcha.google.com doesn't.

fetch('https://adservice.google.com/tt/i', {trustToken: {type: 'token-request'}, credentials:"include"});

adds 25 tokens. Then

fetch('https://adservice.google.com/tt/r', {trustToken: {type: 'token-redemption' }}).finally(function () {
    fetch('https://en.wikipedia.org/', {trustToken: {type: 'send-redemption-record', issuers: ['https://adservice.google.com'] }});
});

will attach a Sec-Redemption-Record: "https://adservice.google.com";redemption-record=:...: header to the inner request. The record base64-decodes to body=:...: , signature=:...: which are base64 encoded binary data. Since the format is not documented (I'm not sure it's even meant for public consumption), not sure how to verify that.

I'm confused at how this should work. The demos are not much help either. https://trusttoken.dev/ simply adds a Sec-Redemption-Record header with the base64-encoded value {"public_metadata": 0, "private_metadata": 0} (some kind of test presumably, the spec does talk about public and private metadata, they aren't really relevant for our use case), I don't see how that gives any assurance the the verifying server when it can just be added to any request manually. https://trust-token-demo.glitch.me/ uses the signRequestData: 'headers' option, which will result in a Sec-Signature header which contains a public key and a signature verifying the redemption record. The site helpfully publishes its server-side verification code so it's easy to see the exact method of validating the headers (this is well-described in the spec in any case). But how do I verify the public key? The spec talks about a key registry, but that seems to be a different type of key (and it definitely has a different size). The explainer gives the impression that request signing is for something entirely different (passing a redemption record through an untrusted middle-man such as a CDN - although I don't see how it would help either). The IETF spec, meanwhile, does imply that the issuing server's public key is involved somehow in the signature, but doesn't really explain how.


Playing around with the response from adservice.google.com, the body is apparently a CBOR with contents like {'metadata': {'public': 0, 'private': 0, 'keyset-version': 10}, 'client-data': {'key-hash': b'\x94\x8a\x87\xa3\xd0\x88\xb9\xde\xf6\xa7\x81\xfd\xb8\x9a\r\xb1\xed\x0e\xf0c\x1f\x1c\xc9z\xdb\xb5\x03\n\x87\x8e\x834', 'redeeming-origin': 'https://www.google.com', 'redemption-timestamp': 1621453150}, 'expiry-timestamp': 0}. keyset-version maybe corresponds to the key in https://adservice.google.com/tt/k (the published key commitment URL for the issuer)? The numbers don't quite match though.

This W3C TAG issue on captchas has some discussion on making it possible to use trust tokens in HTML <input> attributes, which would be nice. (Currently they can only be used in fetch, XMLHTTPRequest and <iframe>, and shielded from normal Javascript, so we couldn't submit an attestation in the same request in which the registration form is submitted, unless we submit the whole form with AJAX).

https://trust-token-demo.glitch.me/ uses the signRequestData: 'headers' option, which will result in a Sec-Signature header which contains a public key and a signature verifying the redemption record.... The explainer gives the impression that request signing is for something entirely different (passing a redemption record through an untrusted middle-man such as a CDN - although I don't see how it would help either).

I think signing is not useful for our use case. But FWIW, it's done via ecdsa_secp256r1_sha256, and there's apparently a decent security-audited PHP implementation, PHPECC, so if we do need to use and verify it, it won't be too hard.

As for the redemption record, the authors confirmed that the method of validating it will be implementation-specific. Google seems to us the reference server implementation, but it's C++ and not very readable, and in any case not meant for public consumption.

I feel I learned enough to wrap this up for now. The Trust Token API looks interesting although not a silver bullet - it comes with privacy tradeoffs and we'll have to think hard whether they are worth it. In any case, it's experimental and not meant to be used right now. The origin trial is probably meant for orgs who are publishers and issuers at the same time. No one seems to be participating in the trial with the intent to issue tokens for others to use. We should check again in a few months.

In theory we could use the PrivacyPass browser extension, but the protocol is way more complex (with the Trust Token API, Chrome does most of the crypto), largely undocumented, and probably will be replaced by either the WICG or the IETF standard eventually, so not worth the effort.


  • what are the exact steps an implementation has to do?

On the client side, do two special fetch commands to to redeem a token, and send the proof to some Wikipedia API. Currently, the latter needs to be an AJAX request, so instead of submitting the proof with the form, we need to submit it to some API, send our own proof (a signed JWT or some such) back, and submit that replacement proof with the form. This will probably be improved over time.
What the proof is and how it can be verified by publishers like Wikipedia is left entirely to the issuer. Google's reference implementation uses some sort of signature scheme.

  • what is the exact state of browser support?

Chrome only, site needs to opt into origin trial, then it will be enabled for 10% of users. If it comes out of the trial, Chrome support will mean ~60% of the userbase which is decent.

  • what is the exact state of issuer support (are there other issuers than Google)?

Google's ad service is the only one participating in the origin trial, and even that doesn't seem to be intended for public consumption.

  • the spec references PrivacyPass a lot. Is it actually compatible with PrivacyPass (to the point that a PrivacyPass issuer is automatically a trust token issuer as well) or did it reimplement PrivacyPass in an incompatible way? Either case, what work would be needed to support PrivacyPass as well?

It's the same math but completely different implementation. Also, with Trust Tokens the required cryptography happens in the browser. With PrivacyPass, we'd have to implement a signficiant chunk of it.

  • what are the exact privacy properties? (After an attempted trust token based user registration, what information does Wikipedia learn about the user? What information does Google learn about the user, given especially that user registrations are public with a timestamp that's accurate to the second?

Wikipedia doesn't learn much other than the user is known to the issuer. The issuer learns the user's IP, which can be correlated with registration / posting timestamps on various websites. In most cases this is completely infeasible for an issuer with nontrivial traffic, but when the IP can be narrowed down via other means, it could be a problem. (Hypothetical scenario: someone edits a Wikipedia article about a small town, the police is looking for them. If the issuer gets subpoenaed, looking at all the token redemptions which correlated with the given user registering on Wikipedia and which come from an IP associated with that town might give police the exact IP.)

There are other privacy threats if Wikipedia colludes with the issuer to get information about a user, not sure how much we care about technical defenses against such a scenario though. In any case this is a problem the spec authors are trying to fix (while I think they consider the one mentioned above out of scope). Also some threats around underspecified / flexible parts of the spec (e.g. how does the issuer prove they are using the same public key for everyone?) but again those might get fixed by the time the spec is final.

  • are there any effectiveness concerns? E.g. if someone pays a mechanical turk to solve captchas to mine trust tokens, will that be more cost-effective than paying someone to solve Wikipedia captchas directly? (How many tokens do you get per solved captcha?)

This is issuer-specific and there is no issuer open to public testing ATM. That said, the spec suggests giving around a large number of tokens after a solved challenge, for performance and usability reasons; Google for example issues 25 at a time. So it comes down to whether their captcha logic is at least 25 times stronger than ours, otherwise it's not worth it. It's likely to be magnitudes stronger, though.

  • are there any accessibility concerns?

In general, none that I can think of. The specific way of hiding the normal captcha initially and showing it when we need to fall back to it might have some accessibility impact.

  • are there any security concerns?

None that applies to us.

  • are there any nontrivial legal implications (e.g. terms of use for the services involved)?

There are no issuers yet, so no terms of service.

+1: are there any ethical implications?

While trust tokens (and more generally, Google's privacy sandbox) will improve user privacy, they are also a massive power grab by Google: user segmentation will only be possible for the browser to do, as it won't leak the required information, so user targeting (now done by a fairly diverse adtech industry) could only be done by Google. That seems like an upset comparable to how Big Tech captured most of the ad revenue that previously went to newspapers.