Swiss E-ID Beta, a First Look

This post initiates a technical examination of the public beta for the Swiss E-ID (electronic identity). We implement a minimal PoC for local verification. This post is the first in a series:
Part 1: Trying out the Swiss E-ID Beta
To try out the Swiss E-ID do the following:
- Install the swiyu wallet application and request a beta E-ID credential via the official portal: https://www.eid.admin.ch/en/public-beta-e.
- Validate the issued credential against the Beta Credential Service (BCS) verification endpoint: https://www.bcs.admin.ch/bcs-web/#/beta-id/verification.

Deconstructing the Request Object
Inspecting the QR code generated by the BCS reveals a link that points to a Request Object:
https://bcs.admin.ch/bcs-web/verifier-agent/oid4vp/api/request-object/7c7c33e5-e51c-4e63-b2fd-2b1f06cdffb6
This Request Object is a JSON Web Token (JWT) of the type `oauth-authz-req+jwt`, compliant with the OpenID for Verifiable Presentations (OID4VP) specification. The Request Object's payload outlines the required verifiable credential data using a presentation_definition. Crucially, it includes a filter to mandate the correct Verifiable Credential Type (`vct`):
{
"path": [
"$.vct"
],
"filter": {
"type": "string",
"const": "betaid-sdjwt"
}
}
The betaid-sdjwt value explicitly identifies the verifiable credential issued by the Beta Credential Service (BCS), distinguishing it from other potential credentials (e.g., driving licenses) the Swiss E-ID wallet may hold.
BCS Request Object (Decoded)
The complete decoded JWT body demonstrates the required format (`vc+sd-jwt`) and the requested credential claims:
{
"kid": "did:tdw:QmPEZPhDFR4nEYSFK5bMnvECqdpf1tPTPJuWs9QrMjCumw:identifier-reg.trust-infra.swiyu-int.admin.ch:api:v1:did:9a5559f0-b81c-4368-a170-e7b4ae424527#assert-key-b720c10d-34be-4e2e-9d87-cecdf8e6a00f",
"typ": "oauth-authz-req+jwt",
"alg": "ES256"
}.{
"response_uri": "https://bcs.admin.ch/bcs-web/verifier-agent/oid4vp/api/request-object/7c7c33e5-e51c-4e63-b2fd-2b1f06cdffb6/response-data",
"client_id_scheme": "did",
"iss": "did:tdw:QmPEZPhDFR4nEYSFK5bMnvECqdpf1tPTPJuWs9QrMjCumw:identifier-reg.trust-infra.swiyu-int.admin.ch:api:v1:did:9a5559f0-b81c-4368-a170-e7b4ae424527",
"response_type": "vp_token",
"presentation_definition": {
"id": "myRequestId",
"name": "myName",
"purpose": "myPurpose",
"format": {},
"input_descriptors": [
{
"id": "myInputDescriptorID",
"name": "BCS",
"format": {
"vc+sd-jwt": {
"sd-jwt_alg_values": [
"ES256"
],
"kb-jwt_alg_values": [
"ES256"
]
}
},
"constraints": {
"format": {},
"fields": [
// ... (omitted claims for brevity)
{
"path": [
"$.vct"
],
"filter": {
"type": "string",
"const": "betaid-sdjwt"
}
}
]
}
}
]
},
"nonce": "Ac5c/JojM/t43YCeNPyghgPe4HCGU+fG",
"version": "1.0",
"client_id": "did:tdw:QmPEZPhDFR4nEYSFK5bMnvECqdpf1tPTPJuWs9QrMjCumw:identifier-reg.trust-infra.swiyu-int.admin.ch:api:v1:did:9a5559f0-b81c-4368-a170-e7b4ae424527",
"client_metadata": {
// ... (omitted client_metadata for brevity)
"vp_formats": {
"jwt_vp": {
"alg": [
"ES256"
]
}
},
"client_name": "swiyu Beta Credential Service (BCS)"
},
"response_mode": "direct_post"
}.[Signature]
Part 2: Implementing a Local Verifier Service PoC
To validate credentials, we deploy the SWIYU Verifier Service locally. This requires a Decentralized Identifier (DID) configured in the SWIYU ecosystem.
DID Registration
curl https://raw.githubusercontent.com/swiyu-admin-ch/swiyu-verifier/refs/heads/main/sample.compose.yml > sample.compose.yml
curl https://raw.githubusercontent.com/swiyu-admin-ch/swiyu-verifier/refs/heads/main/.env > .env
The Verifier Service must be registered in the SWIYU Trust Infrastructure. This involves several prerequisite steps using the SWIYU platform and command-line tools.
Prerequisite Access:
- Create an AGOV Login: https://www.me.agov.admin.ch/registration
- Register as a Business Partner: https://portal.trust-infra.swiyu-int.admin.ch/ui/organizations
- Obtain a `swiyucorebusiness_trust` access token: https://selfservice.api.admin.ch/api-selfservice/apis
All done you can set the following local environement variables:
export SWIYU_IDENTIFIER_REGISTRY_ACCESS_TOKEN="<Your-Access-Token>"
export SWIYU_PARTNER_ID="<Your-Partner-ID>"
export SWIYU_IDENTIFIER_REGISTRY_URL="https://identifier-reg-api.trust-infra.swiyu-int.admin.ch"
Create DID Space: Register a new identifier entry for the DID log:
curl \
-H "Authorization: Bearer $SWIYU_IDENTIFIER_REGISTRY_ACCESS_TOKEN" \
-X POST "$SWIYU_IDENTIFIER_REGISTRY_URL/api/v1/identifier/business-entities/$SWIYU_PARTNER_ID/identifier-entries"
Response:
{"id":"0a0e0186-1f95-4c4f-a63d-768d183bc499","identifierRegistryUrl":"https://identifier-reg.trust-infra.swiyu-int.admin.ch/api/v1/did/0a0e0186-1f95-4c4f-a63d-768d183bc499"}
Store the `id` from the response as `$IDENTIFIER_REGISTRY_ID`.
Generate and publish DID Keys
Generate DID Log: Use the didtoolbox utility to generate the keys and the DID log file (didlog.jsonl).
curl https://repo1.maven.org/maven2/io/github/swiyu-admin-ch/didtoolbox/1.3.1/didtoolbox-1.3.1-jar-with-dependencies.jar > didtoolbox.jar
java -jar didtoolbox.jar create --identifier-registry-url $IDENTIFIER_REGISTRY_URL
Upload the generated log to the public registry:
curl --data-binary @didlog.jsonl \
-H "Authorization: Bearer $SWIYU_IDENTIFIER_REGISTRY_ACCESS_TOKEN" \
-H "Content-Type: application/jsonl+json" \
-X PUT "https://identifier-reg-api.trust-infra.swiyu-int.admin.ch/api/v1/identifier/business-entities/$SWIYU_PARTNER_ID/identifier-entries/$IDENTIFIER_REGISTRY_ID"
Expected response: `200 OK`
Running the Verifier Service
Now we are ready to validate the beta id. We use the provided Docker Compose files and expose the local service to the internet using ngrok to enable the mobile wallet to reach it.
curl https://raw.githubusercontent.com/swiyu-admin-ch/swiyu-verifier/refs/heads/main/sample.compose.yml > sample.compose.yml
curl https://raw.githubusercontent.com/swiyu-admin-ch/swiyu-verifier/refs/heads/main/.env > .env

You need to set the .env variables with the generates DID values.
ngrok http 8083
Start the service:
docker compose -f sample.compose.yml up
### Requesting a Credential
The local verifier is instructed to request a presentation for only the given_name and family_name, filtered for the betaid-sdjwt credential. For that we store the request to the servifier service in a local file: `vs_request.json`
{
"accepted_issuer_dids": [],
"jwt_secured_authorization_request": true,
"presentation_definition": {
"id": "myRequestId",
"name": "Test Verification",
"purpose": "We want to test a new Verifier",
"input_descriptors": [
{
"id": "myInputDescriptorID",
"name": "BCS",
"format": {
"vc+sd-jwt": {
"sd-jwt_alg_values": [
"ES26"
],
"kb-jwt_alg_values": [
"ES256"
]
}
},
"constraints": {
"fields": [
{ "path": [ "$.family_name" ] },
{ "path": [ "$.given_name" ] },
{
"path": [ "$.vct" ],
"filter": {
"type": "string",
"const": "betaid-sdjwt"
}
}
]
}
}
]
}
}
Send the request to local verifier service:
curl -X POST http://localhost:8083/management/api/verifications -H "Content-Type: application/json" --data-binary @vs_request.json > vs_request_response.json
The response contains the necessary OID4VP Request Object URL exposed via ngrok.
{
// ...
"verification_url": "https://bf83b10ab928.ngrok-free.app/oid4vp/api/request-object/e267ddcd-145f-487e-9b69-e9ac2e102b2e",
"verification_deeplink": "swiyu-verify://?client_id=..."
// ...
}
Now we generate a QR code of the request url and scan it with our SWIYU app
qrencode -t ANSIUTF8 $(jq -r '.verification_url' vs_request_response.json)
The QR code is scanned by the swiyu app, which returns a Verifiable Presentation (VP) to the verifier's response_uri.
Poll for Result
To get the result of the verification there we can check the status of the request with the verifier service. It contains the presented values in the response.
curl "http://localhost:8083/management/api/verifications/$(jq -r '.id' vs_request_response.json)" > vs_result.json
jq -r ".wallet_response.credential_subject_data.given_name" vs_result.json
The final vs_result.json confirms "state": "SUCCESS" and reveals the selectively disclosed claims:
vs_result.json
{
// ...
"state": "SUCCESS",
"wallet_response": {
"credential_subject_data": {
"family_name": "National",
"given_name": "Helvetia",
"vct": "betaid-sdjwt",
"iss": "did:tdw:QmPEZPhDFR4nEYSFK5bMnvECqdpf1tPTPJuWs9QrMjCumw:identifier-reg.trust-infra.swiyu-int.admin.ch:api:v1:did:9a5559f0-b81c-4368-a170-e7b4ae424527",
// ...
}
}
// ...
}
Wallet-Verifier Communication Analysis
The network traffic logged by ngrok (http://127.0.0.1:4040) confirms the OID4VP exchange:
- `GET /oid4vp/api/request-object/{ID}`: The wallet fetches the Request Object.
- `POST /oid4vp/api/request-object/{ID}/response-data`: The wallet sends the Verifiable Presentation, as an `x-www-form-urlencoded` body containing a `vp_token`.
The vp_token structure is a VC+SD-JWT (Verifiable Credential plus Selective Disclosure JWT), a core part of the OID4VP specification.
presentation_submission=...&vp_token=eyJ2ZXIiO...Looking into it we can see that it is a `vptoken`. We can read more about it in the OID4VP specification: https://openid.net/specs/openid-4-verifiable-presentations-1_0.html
(Security) Notes
The use of an empty accepted_issuer_dids array ([]) in the verification request seems problematic. While one might assume this rejects all issuers, the observed behavior is the opposite: it appears to skip issuer validation entirely.
This design choice means the Verifier Service defaults to accepting any Verifiable Credential (VC) as long as the presentation is valid and the vct filter (betaid-sdjwt) is met. It permits verification to succeed with any self-issued VC that merely asserts the correct type, regardless of the issuing DID. We will try to exploit this weakness in the next post where we try to issue a credential.
For production hardening, endpoint exposure must be locked down: only the public-facing /oid4vp/** endpoints should be exposed, while all management APIs like /management/** should be firewalled.
For the front-end, always use the verification_deeplink URI (swiyu-verify://?client_id=...&request_uri=...) instead of the raw verification_url. The deeplink is the correct mechanism to instruct the mobile OS to immediately launch the wallet app upon scanning, which is the expected user flow.

Gian-Luca Frei
Gian-Luca Frei is security engineer and specialist for login and authentication. He has a proven track record of securing systems with the highest security standards, including e-banking portals and health applications.
He previously spent 6 years at Zühlke as a security consultant, working in a highly international setting across Switzerland, Singapore, and Hong Kong.
Gian-Luca is also the founder and co-leader of the OWASP Application Gateway Project.
He has a keen interest in modern cryptographic protocols, and his contributions were recognized with the ISSS Excellence Award in 2019.
Our latest article
Offer Your User Secure and Easy Login Experinces?
Ready to elevate your very first user touchpoin? Contact us today and transform your business with better user experiences.
