EU Age Verification App Hacked?

EU age verification app hacked in 2 minutes?! Details and evaluation

The President of the European Commission announced that EU age verification is now available.

On April 15th, EU Commission President Ursula von der Leyen announced that the EU Age Verification App was now available.

However, shortly after that, reports began to appear claiming that it could be "hacked in two minutes."

Attack with collaborators

The first is an attack carried out by the person themselves with the help of an accomplice. The key point is,

  1. Once you receive age verification credentials, you can use them an unlimited number of times.
  2. No PIN or biometric information is required for use.
  3. (There's also information suggesting these credentials aren't bound to the hardware or the App Instance, and can be transferred to other smartphones... but in the case of an attack with an accomplice, the accomplice can simply receive the credentials on the attacker's smartphone, so this itself isn't very important.)

Therefore, an attacker under 18 who obtains an accomplice who is 18 or older can receive credentials that they are 18 or older and use them without restriction. This would require things like rooting the phone, but since the person themselves would do that, it would be possible. So, from a threat modeling perspective, the question is what to do assuming that neither the person, the phone, nor the wallet app instance can be trusted, but it seems that the version released this time overlooks that point due to the principle of proportionality and so on.

Verification implementation errors by the verifier

Another report that came in suggested that age verification could be bypassed by the verifier. But I'm not sure about that... The "issuer" being used is a sample issuer, and the "verifier" is also a sample. The process is as follows:

  1. As a sample issuer, I will receive an mdoc/sd-jwt for age verification.
  2. Using this, one could log in to sample websites that require age verification.

Here it is. Please see the demo below.

However, it seems that both the issuer and the verifier are simply demonstrating how it will work when successful. There doesn't seem to be any need to verify identity to receive an mdoc/sd-jwt. And judging from the publicly available code, the verifier doesn't seem to be properly verifying anything either. Specifically,DocumentValidator.kt So, they do perform signature verification, and it seems they also verify whether the issuer is on the trust list, but even if that fails, they fill in the information that was in the credentials. trust_info It returns a data structure such as this, and within age_over_18 It appears that if such a claim is included, it will be considered as successful age verification.

However, this is only on a demo app. Of course, it would be a problem if you reused the code from this demo app to create a production site, but it seems like some people are overreacting.

However, I would like to ask those who are considering implementing this to please be careful.

  1. Make sure to properly verify the signature.
  2. We also verify the trust chain all the way back to a reliable issuer.
  3. This result will be reflected in access management.

Please don't forget this. This is the Digital Agency's"Expert Meeting on Clarifying Issues in Attribute Verification"But that's something I've been saying all along.

Furthermore, if we want to implement Chairman von der Leyen's principle that "it is the parents, not the platform, who protect children," we need proof of parent-child relationships, but age verification alone is not sufficient.

One more point. The age verification app discussed here is different from the "age protection framework" as defined in ISO/IEC 27566, etc. It is the "age verification" component of the "age protection framework."

Chappy analyzed the source code as of April 16th, and I've included his analysis below as an appendix. I haven't verified whether the content is correct. (I only looked at the beginning a little.) If any engineers find anything strange, I would appreciate it if you could let me know.

Appendix A. Verification of the source code for the OpenID4VP processing section using ChatGPT

  1. Wallet posts the response to /wallet/direct_post.
    The repo docs identify /wallet/direct_post as the wallet-response endpoint. The backend path that processes that response is PostWalletResponseLive.invoke at PostWalletResponse.kt:223-233, which calls doInvoke(...) at 235-265.
  2. The response is submitted and each vp_token The item has been validated.
    In PostWalletResponse.kt:318-334, submit(...) converts the wallet payload with responseObject.toDomain(...). Inside AuthorisationResponseTO.verifiablePresentations(...), each VP element is passed to validateVerifiablePresentation(...).bind() at PostWalletResponse.kt:100-155, specifically 136-145.
  3. The mso_mdoc, the backend takes the MSO mdoc validator path and stores trust info.
    In ValidateSdJwtVcOrMsoMdocVerifiablePresentation.kt:92-101, The Format.MsoMdoc Branch Call validator.validateMsoMdocVerifiablePresentation(...) and then addTrustInfo(transactionId, trustInfo)The trust-info store helpers are at 54-68.
  4. The backend does perform real chain and issuer-signature checks.
    In DocumentValidator.kt:80-105, ensureValidWithTrustInfo(document) runs the document validation sequence. The issuer signature check is ensureValidIssuerSignature(...) at 137-146The chain-trust check is ensureValidChain(...) at 218-226.Trust metadata is assembled in buildTrustInfoFromResults(...) at 234-263.
  5. But trust/signature failure is downgraded to trust_info, not enforced as rejection.
    The frequency code is DeviceResponseValidator.kt:95-125The comment at 95-98 says the method “does not fail due to trust issues.” 104-118, if documentValidator.ensureValidWithTrustInfo(document) Returns Left, the code creates defaultTrust with issuerInTrustedList=false, issuerNotExpired=falseand signatureValid=false, then still cannot DocumentWithTrust(document, defaultTrust). At 122-125, it returns a successful DocumentValidationResult.
  6. The presentation validator then accepts the VP anyway unless issuerAuth is missing.
    In ValidateSdJwtVcOrMsoMdocVerifiablePresentation.kt:159-182, validateMsoMdocVerifiablePresentation(...) calls ensureValidWithTrustInfo(...) at 166-171, extracts documents and trustInfos at 173-174, and then only enforces that document.issuerSigned.issuerAuth is present at 176-179It does not require signatureValid, issuerInTrustedList, Or isFullyTrusted to be true before returning success at 182.
  7. Because of that, the wallet response is stored and the transaction moves to Submitted state.
    Back in PostWalletResponse.kt, submit(...) returns to Submitted presentation at 318-334and doInvoke(...) store it at 249-252. So the verifier backend accepts and stores the wallet response even when trust/signature failed in the permissive mdoc path above.
  8. When the verifier UI polls /ui/presentations/{transactionId}, the backend attaches trust_info to the response.
    The repo docs identify GET /ui/presentations/{transactionId} as the verifier's wallet-response endpoint. GetWalletResponse.kt:119-132, found(...) gets the stored trust info with ValidateSdJwtVcOrMsoMdocVerifiablePresentation.getTrustInfo(...), copies it into the returned wallet response, then clears the store.
  9. The frontend polls that endpoint and receives vp_token Plus trust_info.
    In presentation.ts:68-114, GetPresentationState(transactionID) fetches GET /ui/presentations/${transactionID}.
  10. The frontend sets trust_info, but independently decodes proof_of_age and uses its attributes as the success source.
    In App.tsx:178-191, if data.trust_info exists it is stored, but the code then decodes data.vp_token.proof_of_age and sets verifiedData from firstAttestation.attributesThen at App.tsx:211-221, isAgeOver18 is computed only from whether verifiedData contains age_over_18=true.
  11. The success message is driven by verifiedData, while trust is rendered separately.
    verification-texts.tsx:19-25 shows “You have successfully proven your age” purely from the eu.europa.ec.av.1:age_over_18 value. Separately, App.tsx:246-253 renders TrustInfoDisplay only as an additional component. trust-info.tsx:78-145, that component shows a scorecard; it does not gate the success message.

Leave a comment

This site uses Akismet to reduce spam.For details of how to process comment data, please click here.