diff --git a/CHANGELOG.md b/CHANGELOG.md index 0dde66bee..26edf36b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ All notable changes to this project will be documented in this file. This project uses the changelog in accordance with [keepchangelog](http://keepachangelog.com/). Please use this to write notable changes, which is not the same as git commit log... ## [unreleased][unreleased] +- Fixed `tools/ePassport` - a document number longer than nine characters left its overflow in the MRZ optional-data field, where it was reported as the holder's personal number (@pkilar) - Added `tools/ePassport` - EF_DG13 is decoded, and a Polish document's PESEL is shown as the personal number when it carries no EF_DG11 (@pkilar) - Fixed `tools/ePassport` - a disabled button drew Kivy's banded disabled texture, which left its label unreadable (@pkilar) - Fixed `hf 14b` - a card asking for a waiting time extension of 4 or more left the timeout at zero, so any ISO14443-B card needing time to answer looked like it had stopped responding (@pkilar) diff --git a/tools/ePassport/epassport/emrtd/mrz.py b/tools/ePassport/epassport/emrtd/mrz.py index 85a5cbf05..d84420d70 100644 --- a/tools/ePassport/epassport/emrtd/mrz.py +++ b/tools/ePassport/epassport/emrtd/mrz.py @@ -483,8 +483,12 @@ def _parse_td3(lines: list[str], today: _dt.date | None) -> Mrz: date_of_birth=Checked(l2[13:19], l2[19], verify(l2[13:19], l2[19])), sex=l2[20], date_of_expiry=Checked(l2[21:27], l2[27], verify(l2[21:27], l2[27])), + # The value comes from what _extended_document_number left behind: a + # long document number spills into this field, and that spill belongs + # to the number, not to the State's optional data. The check digit + # still answers for the field as transmitted, overflow included. optional_data=Checked( - l2[28:42].rstrip(FILLER), l2[42], verify(l2[28:42], l2[42]) + optional.rstrip(FILLER), l2[42], verify(l2[28:42], l2[42]) ), composite=Checked( "", diff --git a/tools/ePassport/tests/test_mrz.py b/tools/ePassport/tests/test_mrz.py index 29d26f1b1..4725812c3 100644 --- a/tools/ePassport/tests/test_mrz.py +++ b/tools/ePassport/tests/test_mrz.py @@ -193,3 +193,67 @@ def test_split_names_handles_single_and_missing_given_names() -> None: assert mrz.split_names("ERIKSSON< list[str]: + """ICAO puts the overflow in the optional-data field. + + Positions 1-9 hold the first nine characters, position 10 a filler in + place of the check digit, and the optional-data field opens with the rest + of the number followed by the check digit for the whole of it. + """ + head, rest = number[:9], number[9:] + dob, expiry = "740812", "120415" + optional = (rest + mrz.check_digit(number)).ljust(14, "<") + body = ( + head + + "<" + + "UTO" + + dob + + mrz.check_digit(dob) + + "F" + + expiry + + mrz.check_digit(expiry) + + optional + + mrz.check_digit(optional) + ) + line2 = body + mrz.check_digit(body[0:10] + body[13:20] + body[21:43]) + return ["P None: + parsed = mrz.parse(_td3_with_long_number()) + assert parsed.document_number.value == "AB1234567890" + assert parsed.document_number.ok is True + + +def test_the_overflow_does_not_linger_in_the_optional_field() -> None: + """It is part of the document number, not data the State chose to add. + + Left there it was reported as the holder's personal number, since that + falls back to the MRZ optional-data field when DG11 carries none. + """ + parsed = mrz.parse(_td3_with_long_number()) + assert parsed.optional_data.value == "" + + +def test_a_nine_character_number_leaves_the_optional_field_alone() -> None: + dob, expiry = "740812", "120415" + optional = "ZE184226B".ljust(14, "<") + body = ( + "L898902C" + + "<" + + mrz.check_digit("L898902C<") + + "UTO" + + dob + + mrz.check_digit(dob) + + "F" + + expiry + + mrz.check_digit(expiry) + + optional + + mrz.check_digit(optional) + ) + line2 = body + mrz.check_digit(body[0:10] + body[13:20] + body[21:43]) + parsed = mrz.parse(["P