> and the service would become suspicious if it saw lots of users verifying from the same IP address
This implodes under CGNAT, cafe internet, hotel internet, etc.
We do need to get people onto ipv6 so CGNAT can die. Restricted services could potentially disallow signups or require more knowledge (e.g. full ID) if coming from shared IPs as a risk mitigation strategy, depending on how liable we want to hold them to properly validate age. If you've already signed up for facebook at home, obviously you don't need to validate your age again at the cafe.
Fake IDs exist in the real world. The system doesn't have to be perfect, and we can say that there's some standard of reasonable verification that they should do for these sorts of cases.
Personally I'm more in favor of an approach where sites label their content in a way where parents can configure filters (ideally using labels that are descriptive enough that we don't get into fights over what's "adult", and instead leave that decision to individual families), but if we're going to go an ID-based route, there are at least more private ways we could do it, and I think technologists should be discussing that, and perhaps someone at one of these big companies can propose it.
- Have the service generate the request as above, but now the request is "Please encrypt a response with key A for the user coming from ip X.Y.Z.W".
- Service calls a standard browser API with the request, telling the browser it would like to know the user is in the over 16 bucket. Browser prompts the user to verify that they want to let the service know they are over 16. Browser sends the request to the verifier.
- Verifier responds with a token for each bucket the user is in. So a 22 year old gets an over-16 token, an over-18 token, and an over-21 token.
- Browser selects the appropriate response token and gives it back to the service.
So the service only ever learns you are over the age limit they care about, and the verifier only ever learns that you asked for some token, but not which one.
- Have your backend generate a temporary AES key, and create a request to the verifier saying "please encrypt a response using AES key A indicating that the user coming from ip X.Y.Z.W is over 16". Encrypt it with a known public key for the verifier. Save the temporary AES key to the user's session store.
- Hand that request to the user, who hands it to the verifier. The verifier authenticates the user and gives them the encrypted okay response.
- User gives the response back to your backend.
Potentially the user could still get someone to auth for them, but it'd at least have to be coming from the same IP address that the user tried to use to log into the service. The verifier could become suspicious if it sees lots of requests for the same user coming from different IP addresses, and the service would become suspicious if it saw lots of users verifying from the same IP address, so reselling wouldn't work. You could still find an over-16 friend and have them authenticate you without raising suspicions though, much like you can find an over-21 friend to buy you beer and cigarettes.
Since you use a different key with each user request, the verifier can't identify the requesting service. Both the service and the verifier know the user's IP, so that's not sensitive. If you used this scheme for over-16 vs. over-18 vs. over-21 services, the verifier does learn what level of service you are trying to access (i.e. are you buying alcohol, looking at porn, or signing up for social media). Harmonizing all age-restricted vices to a single age of majority can mitigate that. Or, you could choose to reveal the age bucket to the service instead of the verifier by having the verifier always send back the maximum bucket you qualify for instead of the service asking whether the user is in a specific bucket.