From 361da8e734f63c1ce9391085399e258388ebd45e Mon Sep 17 00:00:00 2001 From: Yuval Adam <_@yuv.al> Date: Fri, 31 Dec 2021 11:05:24 +0200 Subject: Initial structure refactoring --- Pipfile | 5 +- Pipfile.lock | 122 ++++++++++++++++++++++++++- README.md | 6 +- certs/IL-NB-DSC-01.der | Bin 1052 -> 0 bytes certs/IL-NB-DSC-01.pem | 4 - certs/RamzorQRPubKey.der | Bin 1313 -> 0 bytes certs/RamzorQRPubKey.pem | 9 -- greenpass/__init__.py | 0 greenpass/__main__.py | 3 + greenpass/certs/IL-NB-DSC-01.der | Bin 0 -> 1052 bytes greenpass/certs/IL-NB-DSC-01.pem | 4 + greenpass/certs/RamzorQRPubKey.der | Bin 0 -> 1313 bytes greenpass/certs/RamzorQRPubKey.pem | 9 ++ greenpass/cli.py | 31 +++++++ greenpass/tests/__init__.py | 0 greenpass/tests/test_verifier.py | 0 greenpass/verifier.py | 134 ++++++++++++++++++++++++++++++ verify.py | 166 ------------------------------------- 18 files changed, 308 insertions(+), 185 deletions(-) delete mode 100644 certs/IL-NB-DSC-01.der delete mode 100644 certs/IL-NB-DSC-01.pem delete mode 100644 certs/RamzorQRPubKey.der delete mode 100644 certs/RamzorQRPubKey.pem create mode 100644 greenpass/__init__.py create mode 100644 greenpass/__main__.py create mode 100644 greenpass/certs/IL-NB-DSC-01.der create mode 100644 greenpass/certs/IL-NB-DSC-01.pem create mode 100644 greenpass/certs/RamzorQRPubKey.der create mode 100644 greenpass/certs/RamzorQRPubKey.pem create mode 100644 greenpass/cli.py create mode 100644 greenpass/tests/__init__.py create mode 100644 greenpass/tests/test_verifier.py create mode 100644 greenpass/verifier.py delete mode 100644 verify.py diff --git a/Pipfile b/Pipfile index 981c2f7..08f6e10 100644 --- a/Pipfile +++ b/Pipfile @@ -12,6 +12,9 @@ click = "*" colorama = "*" [dev-packages] +pytest = "*" +black = "*" [scripts] -verify = "python verify.py" +greenpass = "python -m greenpass" +test = "pytest" diff --git a/Pipfile.lock b/Pipfile.lock index 245bf15..3dc289b 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "ae87784ddff12b0dd87073de786b2bfb79d9660c535ea1b487946b0cfcebecc5" + "sha256": "66090bf6d49afb65adab47769152fd6244d119fa8e97d7aecf5a2bc332a7cfb9" }, "pipfile-spec": 6, "requires": {}, @@ -203,5 +203,123 @@ "version": "==0.1.8" } }, - "develop": {} + "develop": { + "attrs": { + "hashes": [ + "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4", + "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==21.4.0" + }, + "black": { + "hashes": [ + "sha256:77b80f693a569e2e527958459634f18df9b0ba2625ba4e0c2d5da5be42e6f2b3", + "sha256:a615e69ae185e08fdd73e4715e260e2479c861b5740057fde6e8b4e3b7dd589f" + ], + "index": "pypi", + "version": "==21.12b0" + }, + "click": { + "hashes": [ + "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3", + "sha256:410e932b050f5eed773c4cda94de75971c89cdb3155a72a0831139a79e5ecb5b" + ], + "index": "pypi", + "version": "==8.0.3" + }, + "iniconfig": { + "hashes": [ + "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3", + "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32" + ], + "version": "==1.1.1" + }, + "mypy-extensions": { + "hashes": [ + "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d", + "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8" + ], + "version": "==0.4.3" + }, + "packaging": { + "hashes": [ + "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb", + "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522" + ], + "markers": "python_version >= '3.6'", + "version": "==21.3" + }, + "pathspec": { + "hashes": [ + "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a", + "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1" + ], + "version": "==0.9.0" + }, + "platformdirs": { + "hashes": [ + "sha256:1d7385c7db91728b83efd0ca99a5afb296cab9d0ed8313a45ed8ba17967ecfca", + "sha256:440633ddfebcc36264232365d7840a970e75e1018d15b4327d11f91909045fda" + ], + "markers": "python_version >= '3.7'", + "version": "==2.4.1" + }, + "pluggy": { + "hashes": [ + "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159", + "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3" + ], + "markers": "python_version >= '3.6'", + "version": "==1.0.0" + }, + "py": { + "hashes": [ + "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719", + "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==1.11.0" + }, + "pyparsing": { + "hashes": [ + "sha256:04ff808a5b90911829c55c4e26f75fa5ca8a2f5f36aa3a51f68e27033341d3e4", + "sha256:d9bdec0013ef1eb5a84ab39a3b3868911598afa494f5faa038647101504e2b81" + ], + "markers": "python_version >= '3.6'", + "version": "==3.0.6" + }, + "pytest": { + "hashes": [ + "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89", + "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134" + ], + "index": "pypi", + "version": "==6.2.5" + }, + "toml": { + "hashes": [ + "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", + "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f" + ], + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==0.10.2" + }, + "tomli": { + "hashes": [ + "sha256:05b6166bff487dc068d322585c7ea4ef78deed501cc124060e0f238e89a9231f", + "sha256:e3069e4be3ead9668e21cb9b074cd948f7b3113fd9c8bba083f48247aab8b11c" + ], + "markers": "python_version >= '3.6'", + "version": "==1.2.3" + }, + "typing-extensions": { + "hashes": [ + "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e", + "sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b" + ], + "markers": "python_version >= '3.10'", + "version": "==4.0.1" + } + } } diff --git a/README.md b/README.md index fa5270d..22c7c7f 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ Generate a Green Pass or Vaccination Certificate at https://corona.health.gov.il Directly verify your `GreenPass.pdf` or `VaccinationCertificate.pdf`: ```bash -$ pipenv run verify -p GreenPass.pdf +$ pipenv run greenpass -p GreenPass.pdf ``` #### Option 2 - from QR code image @@ -33,7 +33,7 @@ $ pipenv run verify -p GreenPass.pdf Save the QR code as a `.png` file (e.g. from screenshot) and execute: ```bash -$ pipenv run verify -i green_pass_image.png +$ pipenv run greenpass -i green_pass_image.png ``` #### Option 3 - from QR code decoded textual content @@ -41,7 +41,7 @@ $ pipenv run verify -i green_pass_image.png Decode the QR code payload yourself, put it in a txt file and then execute: ```bash -$ pipenv run verify -t green_pass_payload.txt +$ pipenv run greenpass -t green_pass_payload.txt ``` ### Output example diff --git a/certs/IL-NB-DSC-01.der b/certs/IL-NB-DSC-01.der deleted file mode 100644 index c52c364..0000000 Binary files a/certs/IL-NB-DSC-01.der and /dev/null differ diff --git a/certs/IL-NB-DSC-01.pem b/certs/IL-NB-DSC-01.pem deleted file mode 100644 index f73acd5..0000000 --- a/certs/IL-NB-DSC-01.pem +++ /dev/null @@ -1,4 +0,0 @@ ------BEGIN PUBLIC KEY----- -MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEcw+UZUnryP4rqSw+a8xQw4wLHZkc -u4MDjJP7QeBUEpDt8xh4i4RSIBEJrBkAukSSobRDkwMb0dSCsWwK0rfMgQ== ------END PUBLIC KEY----- diff --git a/certs/RamzorQRPubKey.der b/certs/RamzorQRPubKey.der deleted file mode 100644 index ad3df76..0000000 Binary files a/certs/RamzorQRPubKey.der and /dev/null differ diff --git a/certs/RamzorQRPubKey.pem b/certs/RamzorQRPubKey.pem deleted file mode 100644 index db3731d..0000000 --- a/certs/RamzorQRPubKey.pem +++ /dev/null @@ -1,9 +0,0 @@ ------BEGIN PUBLIC KEY----- -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAw4MJrQWgRnYakBsoU/eV -RxpvDnrGvtidQtfU0o0OGKU+p3H16ufPusBzKLHQPGAoZB33lU8wvfP01xUJTvod -qoi6KEKXGXC+XreQ1YJDKhIglYfPxJOOcauWf/tmV+w0xph6O3L5/2JrhxEjIbdu -E8zP8FvZ+KxVFA9LOFQzX7zbbiDUBLCRtIBhwtLCPIiy960O+lVZkMPXg5BrBWjc -NBrDN62PgOxGXvP3iF0bOlz1+m63q9cFzdKqVfOyl8jZRr3GzYD8SVSXO9EbfYId -8DEP+HMmqd4StD2X6OMDc9UrBBHx3nGbRpi2D9QuHA/kq/QAjQqnrd+iuzdSwQi+ -mQIDAQAB ------END PUBLIC KEY----- diff --git a/greenpass/__init__.py b/greenpass/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/greenpass/__main__.py b/greenpass/__main__.py new file mode 100644 index 0000000..a6f669d --- /dev/null +++ b/greenpass/__main__.py @@ -0,0 +1,3 @@ +from .cli import verify + +verify() diff --git a/greenpass/certs/IL-NB-DSC-01.der b/greenpass/certs/IL-NB-DSC-01.der new file mode 100644 index 0000000..c52c364 Binary files /dev/null and b/greenpass/certs/IL-NB-DSC-01.der differ diff --git a/greenpass/certs/IL-NB-DSC-01.pem b/greenpass/certs/IL-NB-DSC-01.pem new file mode 100644 index 0000000..f73acd5 --- /dev/null +++ b/greenpass/certs/IL-NB-DSC-01.pem @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEcw+UZUnryP4rqSw+a8xQw4wLHZkc +u4MDjJP7QeBUEpDt8xh4i4RSIBEJrBkAukSSobRDkwMb0dSCsWwK0rfMgQ== +-----END PUBLIC KEY----- diff --git a/greenpass/certs/RamzorQRPubKey.der b/greenpass/certs/RamzorQRPubKey.der new file mode 100644 index 0000000..ad3df76 Binary files /dev/null and b/greenpass/certs/RamzorQRPubKey.der differ diff --git a/greenpass/certs/RamzorQRPubKey.pem b/greenpass/certs/RamzorQRPubKey.pem new file mode 100644 index 0000000..db3731d --- /dev/null +++ b/greenpass/certs/RamzorQRPubKey.pem @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAw4MJrQWgRnYakBsoU/eV +RxpvDnrGvtidQtfU0o0OGKU+p3H16ufPusBzKLHQPGAoZB33lU8wvfP01xUJTvod +qoi6KEKXGXC+XreQ1YJDKhIglYfPxJOOcauWf/tmV+w0xph6O3L5/2JrhxEjIbdu +E8zP8FvZ+KxVFA9LOFQzX7zbbiDUBLCRtIBhwtLCPIiy960O+lVZkMPXg5BrBWjc +NBrDN62PgOxGXvP3iF0bOlz1+m63q9cFzdKqVfOyl8jZRr3GzYD8SVSXO9EbfYId +8DEP+HMmqd4StD2X6OMDc9UrBBHx3nGbRpi2D9QuHA/kq/QAjQqnrd+iuzdSwQi+ +mQIDAQAB +-----END PUBLIC KEY----- diff --git a/greenpass/cli.py b/greenpass/cli.py new file mode 100644 index 0000000..3546aeb --- /dev/null +++ b/greenpass/cli.py @@ -0,0 +1,31 @@ +import click + +from .verifier import GreenPassVerifier + + +@click.command() +@click.option("-p", "--pdf-path", type=click.Path(exists=True), help="Path to PDF file") +@click.option( + "-i", + "--image-path", + type=click.Path(exists=True), + help="Path to an image with the QR code", +) +@click.option( + "-t", + "--txt-path", + type=click.Path(exists=True), + help="Path to decoded QR code textual content", +) +def verify(pdf_path="", image_path="", txt_path=""): + if image_path: + verifier = GreenPassVerifier.from_qr(image_path) + elif pdf_path: + verifier = GreenPassVerifier.from_pdf(pdf_path) + elif txt_path: + verifier = GreenPassVerifier.from_payload(txt_path) + else: + ctx = click.get_current_context() + click.echo(ctx.get_help()) + ctx.exit() + verifier.verify() diff --git a/greenpass/tests/__init__.py b/greenpass/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/greenpass/tests/test_verifier.py b/greenpass/tests/test_verifier.py new file mode 100644 index 0000000..e69de29 diff --git a/greenpass/verifier.py b/greenpass/verifier.py new file mode 100644 index 0000000..9bfce3b --- /dev/null +++ b/greenpass/verifier.py @@ -0,0 +1,134 @@ +import base64 + +import fitz +import json + +from io import BytesIO +from pathlib import Path + +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import padding, ec +from cryptography.exceptions import InvalidSignature +from PIL import Image +from pyzbar import pyzbar + + +class GreenPassVerifier(object): + def __init__(self, data_bytes): + self.validate_bytes(data_bytes) + + print(data_bytes) + sig, self.payload = data_bytes.split(b"#", maxsplit=1) + self.signature = base64.decodebytes(sig) + self.data = json.loads(self.payload) + + self.validate_data() + self.details = self.get_details() + self.digest = self.get_digest() + + self.ec_cert = self.get_cert_path("IL-NB-DSC-01.pem") + self.rsa_cert = self.get_cert_path("RamzorQRPubKey.pem") + + @classmethod + def from_payload(cls, path): + with open(path, "rb") as f: + return cls(f.read().strip()) + + @classmethod + def from_qr(cls, path): + return cls(pyzbar.decode(Image.open(path))[0].data) + + @classmethod + def from_pdf(cls, path): + doc = fitz.open(path) + for i in range(len(doc)): + for img in doc.get_page_images(i): + xref, width = img[0], img[2] + try: + img = fitz.Pixmap(doc, xref) + data = img.tobytes(output="png") + with open(f"/tmp/greenpass/{xref}.png", "wb") as f: + f.write(data) + return cls.from_qr(BytesIO(data)) + except: + pass + else: + raise Exception("No QR found") + + def validate_bytes(self, bs): + if bs.decode().startswith("GreenPass"): + click.secho( + "⚠️ Green pass QR code contains no signature to verify", + fg="yellow", + bold=True, + ) + click.get_current_context().exit() + + def validate_data(self): + ct = self.data["ct"] + if ct not in (1, 2): + click.secho(f"Unknown certificate type ct={ct}", fg="red", bold=True) + click.get_current_context().exit() + + def get_cert_path(self, name): + return Path(__file__).absolute().parent / "certs" / name + + def get_details(self): + details = [] + data = self.data + if data["ct"] == 1: + for i in range(len(data["p"])): + details.append( + { + "id_num": data["p"][i]["idl"], + "valid_by": data["p"][i]["e"], + "cert_id": data["id"], + } + ) + elif data["ct"] == 2: + details.append( + { + "id_num": data["idl"], + "valid_by": data["e"], + "cert_id": data["id"], + } + ) + return details + + def get_digest(self): + ct = self.data["ct"] + if ct == 1: + digest = self.payload.decode().encode("utf8") + elif ct == 2: + h = hashes.Hash(hashes.SHA256()) + h.update(self.payload) + digest = h.finalize() + return digest + + def verify(self): + for d in self.details: + click.echo(f"\tIsraeli ID Number {d['id_num']}") + click.echo(f"\tID valid by {d['valid_by']}") + click.echo(f"\tCert Unique ID {d['cert_id']}") + + certs = [ + [ + self.rsa_cert, + [ + padding.PKCS1v15(), + hashes.SHA256(), + ], + ], + [self.ec_cert, [ec.ECDSA(hashes.SHA256())]], + ] + for cert, method in certs: + with open(cert, "rb") as f: + k = serialization.load_pem_public_key(f.read()) + try: + k.verify(self.signature, self.digest, *method) + click.secho("✅ Valid signature!", fg="green", bold=True) + break + except InvalidSignature: + pass + else: + click.secho("❌ Invalid signature!", fg="red", bold=True) diff --git a/verify.py b/verify.py deleted file mode 100644 index 23823d2..0000000 --- a/verify.py +++ /dev/null @@ -1,166 +0,0 @@ -import base64 -import click -import fitz -import json - -from io import BytesIO -from pathlib import Path - -from cryptography.hazmat.primitives import hashes, serialization -from cryptography.hazmat.primitives.asymmetric import padding, ec -from cryptography.exceptions import InvalidSignature -from PIL import Image -from pyzbar import pyzbar - - -class GreenPassVerifier(object): - def __init__(self, data_bytes): - self.validate_bytes(data_bytes) - - print(data_bytes) - sig, self.payload = data_bytes.split(b"#", maxsplit=1) - self.signature = base64.decodebytes(sig) - self.data = json.loads(self.payload) - - self.validate_data() - self.details = self.get_details() - self.digest = self.get_digest() - - self.ec_cert = self.get_cert_path("IL-NB-DSC-01.pem") - self.rsa_cert = self.get_cert_path("RamzorQRPubKey.pem") - - @classmethod - def from_payload(cls, path): - with open(path, "rb") as f: - return cls(f.read().strip()) - - @classmethod - def from_qr(cls, path): - return cls(pyzbar.decode(Image.open(path))[0].data) - - @classmethod - def from_pdf(cls, path): - doc = fitz.open(path) - for i in range(len(doc)): - for img in doc.get_page_images(i): - xref, width = img[0], img[2] - try: - img = fitz.Pixmap(doc, xref) - data = img.tobytes(output="png") - with open(f"/tmp/greenpass/{xref}.png", "wb") as f: - f.write(data) - return cls.from_qr(BytesIO(data)) - except: - pass - else: - raise Exception("No QR found") - - def validate_bytes(self, bs): - if bs.decode().startswith("GreenPass"): - click.secho( - "⚠️ Green pass QR code contains no signature to verify", - fg="yellow", - bold=True, - ) - click.get_current_context().exit() - - def validate_data(self): - ct = self.data["ct"] - if ct not in (1, 2): - click.secho(f"Unknown certificate type ct={ct}", fg="red", bold=True) - click.get_current_context().exit() - - def get_cert_path(self, name): - return Path(__file__).absolute().parent / "certs" / name - - def get_details(self): - details = [] - data = self.data - if data["ct"] == 1: - for i in range(len(data["p"])): - details.append( - { - "id_num": data["p"][i]["idl"], - "valid_by": data["p"][i]["e"], - "cert_id": data["id"], - } - ) - elif data["ct"] == 2: - details.append( - { - "id_num": data["idl"], - "valid_by": data["e"], - "cert_id": data["id"], - } - ) - return details - - def get_digest(self): - ct = self.data["ct"] - if ct == 1: - digest = self.payload.decode().encode("utf8") - elif ct == 2: - h = hashes.Hash(hashes.SHA256()) - h.update(self.payload) - digest = h.finalize() - return digest - - def verify(self): - for d in self.details: - click.echo(f"\tIsraeli ID Number {d['id_num']}") - click.echo(f"\tID valid by {d['valid_by']}") - click.echo(f"\tCert Unique ID {d['cert_id']}") - - certs = [ - [ - self.rsa_cert, - [ - padding.PKCS1v15(), - hashes.SHA256(), - ], - ], - [self.ec_cert, [ec.ECDSA(hashes.SHA256())]], - ] - for cert, method in certs: - with open(cert, "rb") as f: - k = serialization.load_pem_public_key(f.read()) - try: - k.verify(self.signature, self.digest, *method) - click.secho("✅ Valid signature!", fg="green", bold=True) - break - except InvalidSignature: - pass - else: - click.secho("❌ Invalid signature!", fg="red", bold=True) - - -@click.command() -@click.option("-p", "--pdf-path", type=click.Path(exists=True), help="Path to PDF file") -@click.option( - "-i", - "--image-path", - type=click.Path(exists=True), - help="Path to an image with the QR code", -) -@click.option( - "-t", - "--txt-path", - type=click.Path(exists=True), - help="Path to decoded QR code textual content", -) -def verify(pdf_path="", image_path="", txt_path=""): - if image_path: - verifier = GreenPassVerifier.from_qr(image_path) - elif pdf_path: - verifier = GreenPassVerifier.from_pdf(pdf_path) - elif txt_path: - verifier = GreenPassVerifier.from_payload(txt_path) - else: - ctx = click.get_current_context() - click.echo(ctx.get_help()) - ctx.exit() - verifier.verify() - - -if __name__ == "__main__": - verify() -- cgit v1.3.1