For boring and totally not nefarious reasons, I want to read all the data contained in my passport's NFC chip using Linux. After a long and annoying search, I settled on roeften's pypassport. I can…
For boring and totally not nefarious reasons, I want to read all the data contained in my passport's NFC chip using Linux. After a long and annoying search, I settled on roeften's pypassport.
I can now read all the passport information, including biometrics.
The NFC chip in a passport is protected by a password. The password is printed on the inside of the physical passport. As well as needing to be physically close to the passport for NFC to work0, you also need to be able to see the password. The password is printed in the "Machine Readable Zone" (MRZ) - which is why some border guards will swipe your passport through a reader before scanning the chip; they need the password and don't want to type it in.
I had a small problem though. I'm using my old passport1 which has been cancelled. Cancelling isn't just about revoking the document. It is also physically altered:
Cut off the bottom left hand corner of the personal details page, making sure you cut the MRZ on the corner opposite the photo.
So a chunk of the MRZ is missing! Oh no! Whatever can we do!?
The password is made up of three pieces of data:
Each piece also has a checksum. This calculation is defined in Appendix A to Part 3 of Document 9303.
Oh, and there's a checksum for the entire string. It's this final checksum which is cut off when the passport cover is snipped.
The final password is: Number Number-checksum DOB DOB-checksum Expiry Expiry-checkum checksum-of-previous-digits
If you know the passport number, date of birth, and expiry date, you can generate your own Machine Readable Zone - this acts as the password for the NFC chip.
Python 3
def calculateChecksum( value ): weighting = [7,3,1] characterWeight = { '0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9, '<': 0, 'A':10, 'B':11, 'C':12, 'D':13, 'E':14, 'F':15, 'G':16, 'H':17, 'I':18, 'J':19, 'K':20, 'L':21, 'M':22, 'N':23, 'O':24, 'P':25, 'Q':26, 'R':27, 'S':28, 'T':29, 'U':30, 'V':31, 'W':32, 'X':33, 'Y':34, 'Z':35 } counter = 0 result = 0 for x in value: result += characterWeight[str(x)] * weighting[counter%3] counter += 1 return str(result%10) def calculateMRZ( passportNumber, DOB, expiry ): """ DOB and expiry are formatted as YYMMDD """ passportCheck = calculateChecksum( passportNumber ) DOBCheck = calculateChecksum( DOB ) expiryCheck = calculateChecksum( expiry ) mrzNumber = passportNumber + passportCheck + DOB + DOBCheck + expiry + expiryCheck mrzCheck = calculateChecksum( mrzNumber ).zfill(2) mrz = passportNumber + passportCheck + "XXX" + DOB + DOBCheck + "X" + expiry + expiryCheck + "<<<<<<<<<<<<<<" + mrzCheck return mrz print( calculateMRZ("123456789", "841213", "220229") )
I would have thought that cutting the cover of the passport would destroy the antenna inside it. But, going back to the UK guidance:
You must not cut the back cover on the ePassport
Ah! That's where the NFC chip is. I presume this is so that cancelled passports can still be verified for authenticity.
The security is, thankfully, all fairly standard Public Key Cryptography - 9303 part 11 explains it in excruciating levels of detail.
One thing I found curious - because the chip has no timer, it cannot know how often it is being read. You could bombard it with thousands of password attempts and not get locked out. Indeed, the specification says:
the success probability of the attacker is given by the time the attacker has access to the IC, the duration of a single attempt to guess the password, and the entropy of the passport.
Wellllll… maybeeeee…?
Passports are generally valid for only 10 years. So that's 36,525 possible expiry dates.
Passport holders are generally under 100 years old. So that's 3,652,500 possible dates of birth.
That's already 133,407,562,500 attempts - and we haven't even got on to the 1E24 possible passport numbers!
In my experiments, sending an incorrect but valid MRZ results in the chip returning "Security status not satisfied (0x6982)" in a very short space of time. Usually less than a second.
But sending that incorrect attempt seemed to introduce a delay in the next response - by a few seconds. Sending the correct MRZ seemed to reset this and let the chip be read instantly.
So, if you knew the target's passport number and birthday, brute forcing the expiry date would take a couple of days. Not instant, but not impossible.
Most commercial NFC chips support 100,000 writes with no limit for the number of reads. Some also have a 24 bit read counter which increments after every read attempt. After 16 million reads, the counter doesn't increment. It could be possible for a chip to self-destruct after a specific number of reads - but I've no evidence that passport chips do that.
If you were to brute-force the MRZ, you would discover the passport-holder's date of birth. You would also get:
All of that is something which you can see from looking at the passport. So there's little value in attempting to read it electronically.
As mentioned, I'm using https://github.com/roeften/pypassport
The only library I needed to install was pyasn1 using pip3 install pyasn1
- your setup may vary.
Download PyPassport. In the same directory, you can create a test Python file to see if the passport can be read. Here's what it needs to contain:
Python 3
from pypassport import epassport, reader # Replace this MRZ with the one from your passport MRZ = "1234567897XXX8412139X2202299<<<<<<<<<<<<<<04" def trace(name, msg): if name == "EPassport": print(name + ": " + msg) r = reader.ReaderManager().waitForCard() ep = epassport.EPassport(r, MRZ) ep.register(trace) ep.readPassport()
Plug in your NFC reader, place your passport on it, run the above code. If it works, it will spit out a lot of debug information, including all the data it can find on the passport.
The structure of the passport data is a little convoluted. The specification puts data into different "Data Groups" - each with its own ID.
By running:
Python 3
ep.keys()
You can see which Data Groups are available. In my case, ['60', '61', '75', '77']
60
is the common area which contains some metadata. Nothing interesting there. 61
is DG1 - the full MRZ. This contains the holder's name, sex, nationality, etc. 77
is the Document Security Object - this was empty for me. 75
is DG2 to DG4 Biometric Templates - this contains the image and other metadata. Dumping the biometrics - print( ep["75"] )
- gives these interesting pieces of metadata:
'83': '20190311201345',
'meta': { 'Expression': 'Unspecified',
'EyeColour' : 'Unspecified',
'FaceImageBlockLength': 19286,
'FaceImageType': 'Basic',
'FeatureMask': '000000',
'FeaturePoint': {0: {'FeaturePointCode': 'C1',
'FeatureType': '01',
'HorizontalPosition': 249,
'Reserved': '0000',
'VerticalPosition': 216},
1: {'FeaturePointCode': 'C2',
'FeatureType': '01',
'HorizontalPosition': 141,
'Reserved': '0000',
'VerticalPosition': 214}},
'Features': {},
'Gender': 'Unspecified',
'HairColour': 'Unspecified',
'ImageColourSpace': 'RGB24',
'ImageDataType': 'JPEG',
'ImageDeviceType': 0,
'ImageHeight': 481,
'ImageQuality': 'Unspecified',
'ImageSourceType': 'Static Scan',
'ImageWidth': 385,
'LengthOfRecord': 19300,
'NumberOfFacialImages': 1,
'NumberOfFeaturePoint': 2,
'PoseAngle': '0600B5',
'PoseAngleUncertainty': '000000',
'VersionNumber': b'010'
}
If I understand the testing document - the "Feature Points" are the middle of the eyes. Interesting to see that gender (not sex!) and hair colour are also able to be recorded. The "PoseAngle" represents the pitch, yaw, and roll of the face.
Passport images are saved either with JPEG or with JPEG2000 encoding. Given the extremely limited memory available photos are small and highly compressed. Mine was a mere 19KB.
To save the image, grab the bytes and plonk them onto disk:
Python 3
photo = ep["75"]["A1"]["5F2E"] with open( "photo.jpg", "wb" ) as f: f.write( photo )
As expected, the "FeaturePoints" co-ordinates corresponded roughly to the centre of my eyes. Nifty!
I tried a few different tools. Listed here so you don't make the same mistakes as me!
The venerable mrtdreader. My NFC device beeped, then mrtdreader said "No NFC device found."
I think this is because NFC Tools haven't been updated in ages.
I looked at pyPassport but it is only available for Python 2.
This pypassport only checks if a passport is resistant to specific security vulnerabilities.
Digital Logic's ePassport software only works with their hardware readers.
tananaev's passport-reader - works perfectly on Android. So I knew my passport chip was readable - but the app won't run on Linux.
Yeah, I reckon so! Realistically, you aren't going to be able to crack the MRZ to read someone's passport. But if you need to gather personal information3, it's perfectly possible to do so quickly from a passport.
The MRZ is a Machine Readable Zone - so it is fairly simple to OCR the text and then pass that to your NFC reader.
And even if the MRZ is gone, you can reconstruct it from the data printed on the passport.
Of course, this won't be able to detect fraudulent passports. It doesn't check against a database to see if it has been revoked4. I don't think it will detect any cryptographic anomalies.
But if you just want to see what's on your travel documents, it works perfectly.
The spec for machine readable travel documents is sadly not the most concise but if you're interested in the nitty-gritty details of how to validate documents, how to read data from them, etc then jump into ICAO 9303:
https://www.icao.int/publications/documents/9303_p10_cons_en...
https://www.icao.int/publications/documents/9303_p11_cons_en...
But please keep in mind that this is just the spec for how it's supposed to be implemented. Real world implementations of it have lots of creative interpretations of the spec in addition to straight bugs in their implementations, so if you're going to write software that has to work with various different documents issued by various governments, you'll have many fun debugging sessions :)
It seems every country that moves to electronic travel authorization has an app that requires me to verify my passport with this method. I have a fairly new passport, issued in the last few years, and a recent phone… and this process is a huge pain. I need to massage my passport with my phone for a minute, maybe I get a bite, hold it still… oops, start over… try again… okay, use our partner’s face ID recognition service instead… ugh it’s horrible.
I don’t know if the issue is the very low power chip in the passport, or some damage or what… but I dread the process any time I need to do it.
It's just a future fantasy that isn't fit for our dystopian world. I'm still waiting for the fantasy of fixed potholes.
Honestly, it’s better than “take a photo of your passport and upload it to our unsecured S3 bucket.”
Or id.me, as used by the IRS. "Scan your license, front and back"...
Front, 200dpi, "Unable to find a face in the image". 300dpi, "Unable to find a face in the image". Let's try lower, 72dpi, "Thank you".
Back, let's start at 72dpi, since that worked for the front. "Unable to read a barcode in the image". Higher, 200dpi, "Unable to read a barcode in the image". 300dpi? "Thank you".
Here's a tidied up version of the Python code to generate the MRZ from the passport data. It also corrects a padding error.
https://pastebin.com/k0Tty22a
My Dutch driver's licence has a single MRZ-like line across the bottom. It seems to encode the country and licence number but I can't make any sense of the rest of the line. Anyone have any leads?I haven't found the docs for the Dutch version but this article shows the content of the MRZ of a French drivers license. They seem to match the Dutch ones as well.
https://trustdochub.com/en/mrz-strip-french-driving-licence/...
Only partially. At least for my Dutch licence. It contains neither holder's last name nor end date.
It does start with D1NLD. Then a single digit which is not the checksum of the foregoing (using the passport checksum algorithm). Then the document number. Then some letters and numbers I can't make any sense of. It ends with a correct global checksum for all of the foregoing.
See the spec I mentioned. But here's an excerpt. https://upload.disroot.org/r/Y_vdT16A#cW4coNxPvTJN6b8InO5cHW...
Drivers licenses aren't ICAO 9303 compliant. For EU documents a separate spec is being used (NEN-ISO-IEC 18013-3). Small pointer: https://eur-lex.europa.eu/legal-content/EN/TXT/HTML/?uri=CEL...
Context: Made an implementation for reading these when Dutch drivers license model with NFC first came available (model 2014 if I remember correctly)
I've written some Rust code to do the same thing. Mainly to get a copy of the photo stored on my passport, and because I was curious about how eMRTDs worked. I enjoyed reading through the ICAO 9303 specs, they were very detailed.
Example: https://github.com/alexrsagen/rs-nfc1/blob/main/examples/rea...
Library with eMRTD specific code: https://github.com/alexrsagen/rs-mrtd1