- 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.