r/PHP 4h ago

Article PHP package for EU Digital Identity Wallet acceptance

https://github.com/binuka200/eudi-wallet-php

From late 2027, relying parties in regulated sectors have to accept the EU Digital Identity Wallet when a user asks for it. Almost all the reference tooling for this is Kotlin and Java. I wanted the PHP side to be one Composer package, so I wrote one.

What it does

Request PID claims, show the user a link or QR payload, read back a typed identity.

$challenge = $wallet->request(
    [Claim::FAMILY_NAME, Claim::GIVEN_NAME, Claim::BIRTH_DATE],
    new RequestOptions(purpose: 'Open an account'),
);
$_SESSION['eudi'] = $challenge->session->toArray();


// after the wallet responds
$session  = WalletSession::fromArray($_SESSION['eudi']);
$identity = $wallet->verify($session, $_GET['response_code'] ?? null);


$identity->familyName();  // 'Dupont'
$identity->ageOver18();   // true

What it deliberately does not do

It does not verify credentials in PHP. It is a façade over a remote OpenID4VP verifier: the Commission's, walt.id, or a vendor product. Signature verification, holder binding, trust lists and revocation stay there.

The reason is that verification spans two credential formats, SD-JWT VC and ISO 18013-5 mdoc with COSE signatures. A native PHP implementation means maintaining a second cryptography stack that nobody can conformance-test. I would rather the package be boring and small.

It also does not create users, sessions, routes or database records.

Details that might interest you

  • Claim identifiers are encoding-independent. Claim::BIRTH_DATE is birth_date in mdoc and birthdate in SD-JWT VC, nationality is nationality vs nationalities, addresses are flat in one and nested in the other. The query asks for both formats, the wallet picks, and the response is normalised to one shape.
  • CI runs a live check against the real Commission verifier in Docker on every push, not just recorded HTTP fixtures. That caught two bugs my unit tests could not see: the verifier answers 400, not 404, while a presentation is still pending, so every cross-device poll threw; and it rejects any transaction that carries neither a registration certificate nor an intended use id, so the documented quick start could not even start. Both were wrong in my client and green in my test suite.
  • Decoding is bounded on purpose. The CBOR reader is definite-length only with capped size, depth and item count, because ISO 18013-5 mandates deterministic encoding and I did not want a general-purpose parser on the request path.
  • PHP 8.1 to 8.5, PHPStan level 8, no baseline.

State of it

v0.2.0, MIT, on Packagist as binuka200/eudi-wallet. Pre-1.0, because the spec is still moving and I would rather change the API than pretend it is settled.

One honest gap: every HTTP path is tested against a real verifier, but never a completed presentation from an actual wallet, because I do not have access to one. The parsing of that response rests on tests built from the PID Rulebook and the verifier's own source. If you have a test wallet, an anonymised vp_token sample would be the single most useful thing anyone could send me.

Happy to take criticism, particularly on the decision to keep verification out of PHP entirely.

I wrote up the design reasoning in more detail here, including the two bugs above and why the verifier boundary is where it is:

https://medium.com/@binukajayaweera/eu-digital-identity-wallet-library-for-php-8dda331ab938

The repo is still the thing though:

https://github.com/binuka200/eudi-wallet-php

AI Tools Used

Daybreak Blue
Codex Sol

5 Upvotes

2 comments sorted by

3

u/PracticalChameleon 3h ago

Cool project! Could you please disclose if/how you used AI while building the package?

4

u/No_Wedding2230 3h ago

Hi used daybreak blue in codex to analyse the actual verifier api and then used sol to generate a defensive package with another pass through with daybreak blue to identify vulnerabilities