From b6fa4fa35b886b48855f0927409ca1da185b09b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexis=20M=C3=A9taireau?= Date: Wed, 12 Feb 2025 18:23:12 +0100 Subject: [PATCH] =?UTF-8?q?parent=20a19e34137825e95d1f432893b5a7002efdbbe8?= =?UTF-8?q?66=20author=20Alexis=20M=C3=A9taireau=20?= =?UTF-8?q?=201739380992=20+0100=20committer=20Alexis=20M=C3=A9taireau=20=201744646096=20+0200=20gpgsig=20-----BEGI?= =?UTF-8?q?N=20PGP=20SIGNATURE-----?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit iHUEABYKAB0WIQRFQpTG/4uXFqX2QanGXHqJqP/FbgUCZ/0v0AAKCRDGXHqJqP/F bmA8AP9QZjs6bSmxvmzvYvwJa8wYIo6OsdUyQdoZ4kAMd5X7XwEA+WIbuxU/o2bG KisPBI+N8LSwwIke3eNS+ne6Vil7qwg= =XCYF -----END PGP SIGNATURE----- (WIP) Add tests --- dangerzone/container_utils.py | 7 +- dangerzone/isolation_provider/container.py | 25 +- dangerzone/updater/signatures.py | 89 +++-- tests/assets/signatures/README.md | 7 + ...d955e68ee3e07b41b9d53f4c8cc9929a68a67.json | 18 + ...aa9338681e64dd3e34a34873866cb051d694e.json | 18 + ...5745d532d7a4079886e1647924bee7ef1c14d.json | 18 + ...2230dc6566997f852ef5d62b0338b46796e01.json | 18 + ...d955e68ee3e07b41b9d53f4c8cc9929a68a67.json | 18 + ...aa9338681e64dd3e34a34873866cb051d694e.json | 18 + .../README.md | 1 + ...d955e68ee3e07b41b9d53f4c8cc9929a68a67.json | 1 + ...aa9338681e64dd3e34a34873866cb051d694e.json | 1 + ...5745d532d7a4079886e1647924bee7ef1c14d.json | 1 + ...2230dc6566997f852ef5d62b0338b46796e01.json | 1 + ...bac18522b35b2491fdf716236a0b3502a2ca7.json | 1 + tests/assets/test.pub.key | 4 + tests/test_signatures.py | 314 ++++++++++++++++++ 18 files changed, 513 insertions(+), 47 deletions(-) create mode 100644 tests/assets/signatures/README.md create mode 100644 tests/assets/signatures/invalid/95b432860b272938246b10e1cfc89a24e1db352b3aebaa799c4284c42c46bd95/19e8eacd75879d05f6621c2ea8dd955e68ee3e07b41b9d53f4c8cc9929a68a67.json create mode 100644 tests/assets/signatures/invalid/95b432860b272938246b10e1cfc89a24e1db352b3aebaa799c4284c42c46bd95/220b52200e3e47b1b42010667fcaa9338681e64dd3e34a34873866cb051d694e.json create mode 100644 tests/assets/signatures/invalid/95b432860b272938246b10e1cfc89a24e1db352b3aebaa799c4284c42c46bd95/4da441235e84e93518778827a5c5745d532d7a4079886e1647924bee7ef1c14d.json create mode 100644 tests/assets/signatures/invalid/95b432860b272938246b10e1cfc89a24e1db352b3aebaa799c4284c42c46bd95/7b21dbdebffed855621dfcdeaa52230dc6566997f852ef5d62b0338b46796e01.json create mode 100644 tests/assets/signatures/tempered/95b432860b272938246b10e1cfc89a24e1db352b3aebaa799c4284c42c46bd95/19e8eacd75879d05f6621c2ea8dd955e68ee3e07b41b9d53f4c8cc9929a68a67.json create mode 100644 tests/assets/signatures/tempered/95b432860b272938246b10e1cfc89a24e1db352b3aebaa799c4284c42c46bd95/220b52200e3e47b1b42010667fcaa9338681e64dd3e34a34873866cb051d694e.json create mode 100644 tests/assets/signatures/tempered/95b432860b272938246b10e1cfc89a24e1db352b3aebaa799c4284c42c46bd95/README.md create mode 100644 tests/assets/signatures/valid/95b432860b272938246b10e1cfc89a24e1db352b3aebaa799c4284c42c46bd95/19e8eacd75879d05f6621c2ea8dd955e68ee3e07b41b9d53f4c8cc9929a68a67.json create mode 100644 tests/assets/signatures/valid/95b432860b272938246b10e1cfc89a24e1db352b3aebaa799c4284c42c46bd95/220b52200e3e47b1b42010667fcaa9338681e64dd3e34a34873866cb051d694e.json create mode 100644 tests/assets/signatures/valid/95b432860b272938246b10e1cfc89a24e1db352b3aebaa799c4284c42c46bd95/4da441235e84e93518778827a5c5745d532d7a4079886e1647924bee7ef1c14d.json create mode 100644 tests/assets/signatures/valid/95b432860b272938246b10e1cfc89a24e1db352b3aebaa799c4284c42c46bd95/7b21dbdebffed855621dfcdeaa52230dc6566997f852ef5d62b0338b46796e01.json create mode 100644 tests/assets/signatures/valid/95b432860b272938246b10e1cfc89a24e1db352b3aebaa799c4284c42c46bd95/fa948726aac29a6ac49f01ec8fbbac18522b35b2491fdf716236a0b3502a2ca7.json create mode 100644 tests/assets/test.pub.key create mode 100644 tests/test_signatures.py diff --git a/dangerzone/container_utils.py b/dangerzone/container_utils.py index 4af885e..546f273 100644 --- a/dangerzone/container_utils.py +++ b/dangerzone/container_utils.py @@ -254,7 +254,12 @@ def get_local_image_digest(image: str) -> str: raise errors.MultipleImagesFoundException( f"Expected a single line of output, got {len(lines)} lines" ) - return lines[0].replace("sha256:", "") + image_digest = lines[0].replace("sha256:", "") + if not image_digest: + raise errors.ImageNotPresentException( + f"The image {image} does not exist locally" + ) + return image_digest except subprocess.CalledProcessError as e: raise errors.ImageNotPresentException( f"The image {image} does not exist locally" diff --git a/dangerzone/isolation_provider/container.py b/dangerzone/isolation_provider/container.py index 3151680..a5bb6b7 100644 --- a/dangerzone/isolation_provider/container.py +++ b/dangerzone/isolation_provider/container.py @@ -96,23 +96,21 @@ class Container(IsolationProvider): @staticmethod def install() -> bool: """Check if an update is available and install it if necessary.""" - # XXX Do this only if users have optted in to auto-updates - - # # Load the image tarball into the container runtime. - update_available, image_digest = updater.is_update_available( - container_utils.CONTAINER_NAME - ) - if update_available and image_digest: - updater.upgrade_container_image( - container_utils.CONTAINER_NAME, - image_digest, - updater.DEFAULT_PUBKEY_LOCATION, + # XXX Do this only if users have opted in to auto-updates + if False: # Comment this for now, just as an exemple of this can be implemented + # # Load the image tarball into the container runtime. + update_available, image_digest = updater.is_update_available( + container_utils.CONTAINER_NAME ) + if update_available and image_digest: + updater.upgrade_container_image( + container_utils.CONTAINER_NAME, + image_digest, + updater.DEFAULT_PUBKEY_LOCATION, + ) for tag in old_tags: tag = container_utils.CONTAINER_NAME + ":" + tag container_utils.delete_image_tag(tag) - else: - return True updater.verify_local_image( container_utils.CONTAINER_NAME, updater.DEFAULT_PUBKEY_LOCATION @@ -207,7 +205,6 @@ class Container(IsolationProvider): updater.verify_local_image( container_utils.CONTAINER_NAME, updater.DEFAULT_PUBKEY_LOCATION, - image_digest, ) security_args = self.get_runtime_security_args() debug_args = [] diff --git a/dangerzone/updater/signatures.py b/dangerzone/updater/signatures.py index cad05f0..cc006ad 100644 --- a/dangerzone/updater/signatures.py +++ b/dangerzone/updater/signatures.py @@ -33,7 +33,7 @@ LAST_LOG_INDEX = SIGNATURES_PATH / "last_log_index" __all__ = [ "verify_signature", - "load_signatures", + "load_and_verify_signatures", "store_signatures", "verify_offline_image_signature", ] @@ -61,18 +61,22 @@ def signature_to_bundle(sig: Dict) -> Dict: } -def verify_signature(signature: dict, image_digest: str, pubkey: str) -> bool: +def verify_signature(signature: dict, image_digest: str, pubkey: str | Path) -> bool: """Verify a signature against a given public key""" # XXX - Also verfy the identity/docker-reference field against the expected value # e.g. ghcr.io/freedomofpress/dangerzone/dangerzone cosign.ensure_installed() signature_bundle = signature_to_bundle(signature) - - payload_bytes = b64decode(signature_bundle["Payload"]) - payload_digest = json.loads(payload_bytes)["critical"]["image"][ - "docker-manifest-digest" - ] + try: + payload_bytes = b64decode(signature_bundle["Payload"]) + payload_digest = json.loads(payload_bytes)["critical"]["image"][ + "docker-manifest-digest" + ] + except Exception as e: + raise errors.SignatureVerificationError( + f"Unable to extract the payload digest from the signature: {e}" + ) if payload_digest != f"sha256:{image_digest}": raise errors.SignatureMismatch( f"The signature does not match the image digest ({payload_digest}, {image_digest})" @@ -88,11 +92,14 @@ def verify_signature(signature: dict, image_digest: str, pubkey: str) -> bool: payload_file.write(payload_bytes) payload_file.flush() + if isinstance(pubkey, str): + pubkey = Path(pubkey) + cmd = [ "cosign", "verify-blob", "--key", - pubkey, + str(pubkey.absolute()), "--bundle", signature_file.name, payload_file.name, @@ -143,9 +150,14 @@ def verify_signatures( image_digest: str, pubkey: str, ) -> bool: + if len(signatures) < 1: + raise errors.SignatureVerificationError("No signatures found") + for signature in signatures: if not verify_signature(signature, image_digest, pubkey): - raise errors.SignatureVerificationError() + msg = f"Unable to verify signature for {image_digest} with pubkey {pubkey}" + raise errors.SignatureVerificationError(msg) + return True @@ -159,9 +171,14 @@ def get_last_log_index() -> int: def get_log_index_from_signatures(signatures: List[Dict]) -> int: - return reduce( - lambda acc, sig: max(acc, sig["Bundle"]["Payload"]["logIndex"]), signatures, 0 - ) + def _reducer(accumulator: int, signature: Dict) -> int: + try: + logIndex = int(signature["Bundle"]["Payload"]["logIndex"]) + except (KeyError, ValueError): + return accumulator + return max(accumulator, logIndex) + + return reduce(_reducer, signatures, 0) def write_log_index(log_index: int) -> None: @@ -297,13 +314,21 @@ def get_file_digest(file: Optional[str] = None, content: Optional[bytes] = None) return "" -def load_signatures(image_digest: str, pubkey: str) -> List[Dict]: +def load_and_verify_signatures( + image_digest: str, + pubkey: str, + bypass_verification: bool = False, + signatures_path: Optional[Path] = None, +) -> List[Dict]: """ Load signatures from the local filesystem See store_signatures() for the expected format. """ - pubkey_signatures = SIGNATURES_PATH / get_file_digest(pubkey) + if not signatures_path: + signatures_path = SIGNATURES_PATH + + pubkey_signatures = signatures_path / get_file_digest(pubkey) if not pubkey_signatures.exists(): msg = ( f"Cannot find a '{pubkey_signatures}' folder." @@ -313,7 +338,12 @@ def load_signatures(image_digest: str, pubkey: str) -> List[Dict]: with open(pubkey_signatures / f"{image_digest}.json") as f: log.debug("Loading signatures from %s", f.name) - return json.load(f) + signatures = json.load(f) + + if not bypass_verification: + verify_signatures(signatures, image_digest, pubkey) + + return signatures def store_signatures(signatures: list[Dict], image_digest: str, pubkey: str) -> None: @@ -375,32 +405,27 @@ def verify_local_image(image: str, pubkey: str) -> bool: raise errors.ImageNotFound(f"The image {image} does not exist locally") log.debug(f"Image digest: {image_digest}") - signatures = load_signatures(image_digest, pubkey) - if len(signatures) < 1: - raise errors.LocalSignatureNotFound("No signatures found") - - for signature in signatures: - if not verify_signature(signature, image_digest, pubkey): - msg = f"Unable to verify signature for {image} with pubkey {pubkey}" - raise errors.SignatureVerificationError(msg) + load_and_verify_signatures(image_digest, pubkey) return True def get_remote_signatures(image: str, digest: str) -> List[Dict]: - """Retrieve the signatures from the registry, via `cosign download`.""" + """Retrieve the signatures from the registry, via `cosign download signatures`.""" cosign.ensure_installed() - # XXX: try/catch here - process = subprocess.run( - ["cosign", "download", "signature", f"{image}@sha256:{digest}"], - capture_output=True, - check=True, - ) + try: + process = subprocess.run( + ["cosign", "download", "signature", f"{image}@sha256:{digest}"], + capture_output=True, + check=True, + ) + except subprocess.CalledProcessError as e: + raise errors.NoRemoteSignatures(e) - # XXX: Check the output first. # Remove the last return, split on newlines, convert from JSON signatures_raw = process.stdout.decode("utf-8").strip().split("\n") signatures = list(map(json.loads, signatures_raw)) + breakpoint() if len(signatures) < 1: raise errors.NoRemoteSignatures("No signatures found for the image") return signatures @@ -413,8 +438,8 @@ def prepare_airgapped_archive(image_name: str, destination: str) -> None: ) cosign.ensure_installed() - # Get the image from the registry + # Get the image from the registry with TemporaryDirectory() as tmpdir: msg = f"Downloading image {image_name}. \nIt might take a while." log.info(msg) diff --git a/tests/assets/signatures/README.md b/tests/assets/signatures/README.md new file mode 100644 index 0000000..e79adbc --- /dev/null +++ b/tests/assets/signatures/README.md @@ -0,0 +1,7 @@ +This folder contains signature-folders used for the testing the signatures implementation. + +The following folders are used: + +- `valid`: this folder contains signatures which should be considered valid and generated with the key available at `tests/assets/test.pub.key` +- `invalid`: this folder contains signatures which should be considered invalid, because their format doesn't match the expected one. e.g. it uses plain text instead of base64-encoded text. +- `tempered`: This folder contain signatures which have been tempered-with. The goal is to have signatures that looks valid, but actually aren't. diff --git a/tests/assets/signatures/invalid/95b432860b272938246b10e1cfc89a24e1db352b3aebaa799c4284c42c46bd95/19e8eacd75879d05f6621c2ea8dd955e68ee3e07b41b9d53f4c8cc9929a68a67.json b/tests/assets/signatures/invalid/95b432860b272938246b10e1cfc89a24e1db352b3aebaa799c4284c42c46bd95/19e8eacd75879d05f6621c2ea8dd955e68ee3e07b41b9d53f4c8cc9929a68a67.json new file mode 100644 index 0000000..8ff0ba9 --- /dev/null +++ b/tests/assets/signatures/invalid/95b432860b272938246b10e1cfc89a24e1db352b3aebaa799c4284c42c46bd95/19e8eacd75879d05f6621c2ea8dd955e68ee3e07b41b9d53f4c8cc9929a68a67.json @@ -0,0 +1,18 @@ +[ + { + "Base64Signature": "Invalid base64 signature", + "Payload": "eyJjcml0aWNhbCI6eyJpZGVudGl0eSI6eyJkb2NrZXItcmVmZXJlbmNlIjoiZ2hjci5pby9hbG1ldC9kYW5nZXJ6b25lL2RhbmdlcnpvbmUifSwiaW1hZ2UiOnsiZG9ja2VyLW1hbmlmZXN0LWRpZ2VzdCI6InNoYTI1NjoxOWU4ZWFjZDc1ODc5ZDA1ZjY2MjFjMmVhOGRkOTU1ZTY4ZWUzZTA3YjQxYjlkNTNmNGM4Y2M5OTI5YTY4YTY3In0sInR5cGUiOiJjb3NpZ24gY29udGFpbmVyIGltYWdlIHNpZ25hdHVyZSJ9LCJvcHRpb25hbCI6bnVsbH0=", + "Cert": null, + "Chain": null, + "Bundle": { + "SignedEntryTimestamp": "MEUCIC9oXH9VVP96frVOmDw704FBqMN/Bpm2RMdTm6BtSwL/AiEA6mCIjhV65fYuy4CwjsIzQHi/oW6IBwtd6oCvN2dI6HQ=", + "Payload": { + "body": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiJmMjEwNDJjY2RjOGU0ZjA1ZGEzNmE5ZjU4ODg5MmFlZGRlMzYzZTQ2ZWNjZGZjM2MyNzAyMTkwZDU0YTdmZmVlIn19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FWUNJUUNWaTJGUFI3Mjl1aHAvY3JFdUNTOW9yQzRhMnV0OHN3dDdTUnZXYUVSTGp3SWhBSlM1dzU3MHhsQnJsM2Nhd1Y1akQ1dk85RGh1dkNrdCtzOXJLdGc2NzVKQSIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCUVZVSk1TVU1nUzBWWkxTMHRMUzBLVFVacmQwVjNXVWhMYjFwSmVtb3dRMEZSV1VsTGIxcEplbW93UkVGUlkwUlJaMEZGYjBVd1ExaE1SMlptTnpsbVVqaExlVkJ1VTNaUFdUYzBWVUpyZEFveWMweHBLMkZXUmxWNlV6RlJkM1EwZDI5emVFaG9ZMFJPTWtJMlVWTnpUR3gyWjNOSU9ESnhObkZqUVRaUVRESlRaRk12Y0RScVYwZEJQVDBLTFMwdExTMUZUa1FnVUZWQ1RFbERJRXRGV1MwdExTMHRDZz09In19fX0=", + "integratedTime": 1738752154, + "logIndex": 168898587, + "logID": "c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d" + } + }, + "RFC3161Timestamp": null + } +] \ No newline at end of file diff --git a/tests/assets/signatures/invalid/95b432860b272938246b10e1cfc89a24e1db352b3aebaa799c4284c42c46bd95/220b52200e3e47b1b42010667fcaa9338681e64dd3e34a34873866cb051d694e.json b/tests/assets/signatures/invalid/95b432860b272938246b10e1cfc89a24e1db352b3aebaa799c4284c42c46bd95/220b52200e3e47b1b42010667fcaa9338681e64dd3e34a34873866cb051d694e.json new file mode 100644 index 0000000..34ff6e4 --- /dev/null +++ b/tests/assets/signatures/invalid/95b432860b272938246b10e1cfc89a24e1db352b3aebaa799c4284c42c46bd95/220b52200e3e47b1b42010667fcaa9338681e64dd3e34a34873866cb051d694e.json @@ -0,0 +1,18 @@ +[ + { + "Base64Signature": "MEQCICi2AOAJbS1k3334VMSo+qxaI4f5VoNnuVExZ4tfIu7rAiAiwuKdo8rGfFMGMLSFSQvoLF3JuwFy4JtNW6kQlwH7vg==", + "Payload": "Invalid base64 payload", + "Cert": null, + "Chain": null, + "Bundle": { + "SignedEntryTimestamp": "MEUCIEvx6NtFeAag9TplqMLjVczT/tC6lpKe9SnrxbehBlxfAiEA07BE3f5JsMLsUsmHD58D6GaZr2yz+yQ66Os2ps8oKz8=", + "Payload": { + "body": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiI4YmJmNGRiNjBmMmExM2IyNjI2NTI3MzljNWM5ZTYwNjNiMDYyNjVlODU1Zjc3MTdjMTdlYWY4YzViZTQyYWUyIn19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FUUNJQ2kyQU9BSmJTMWszMzM0Vk1TbytxeGFJNGY1Vm9ObnVWRXhaNHRmSXU3ckFpQWl3dUtkbzhyR2ZGTUdNTFNGU1F2b0xGM0p1d0Z5NEp0Tlc2a1Fsd0g3dmc9PSIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCUVZVSk1TVU1nUzBWWkxTMHRMUzBLVFVacmQwVjNXVWhMYjFwSmVtb3dRMEZSV1VsTGIxcEplbW93UkVGUlkwUlJaMEZGYjBVd1ExaE1SMlptTnpsbVVqaExlVkJ1VTNaUFdUYzBWVUpyZEFveWMweHBLMkZXUmxWNlV6RlJkM1EwZDI5emVFaG9ZMFJPTWtJMlVWTnpUR3gyWjNOSU9ESnhObkZqUVRaUVRESlRaRk12Y0RScVYwZEJQVDBLTFMwdExTMUZUa1FnVUZWQ1RFbERJRXRGV1MwdExTMHRDZz09In19fX0=", + "integratedTime": 1738859497, + "logIndex": 169356501, + "logID": "c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d" + } + }, + "RFC3161Timestamp": null + } +] \ No newline at end of file diff --git a/tests/assets/signatures/invalid/95b432860b272938246b10e1cfc89a24e1db352b3aebaa799c4284c42c46bd95/4da441235e84e93518778827a5c5745d532d7a4079886e1647924bee7ef1c14d.json b/tests/assets/signatures/invalid/95b432860b272938246b10e1cfc89a24e1db352b3aebaa799c4284c42c46bd95/4da441235e84e93518778827a5c5745d532d7a4079886e1647924bee7ef1c14d.json new file mode 100644 index 0000000..15e9fae --- /dev/null +++ b/tests/assets/signatures/invalid/95b432860b272938246b10e1cfc89a24e1db352b3aebaa799c4284c42c46bd95/4da441235e84e93518778827a5c5745d532d7a4079886e1647924bee7ef1c14d.json @@ -0,0 +1,18 @@ +[ + { + "Base64Signature": "MEQCIDJxvB7lBU+VNYBD0xw/3Bi8wY7GPJ2fBP7mUFbguApoAiAIpuQT+sgatOY6yXkkA8K/sM40d5/gt7jQywWPbq5+iw==", + "Payload": "eyJjcml0aWNhbCI6eyJpZGVudGl0eSI6eyJkb2NrZXItcmVmZXJlbmNlIjoiZ2hjci5pby9hcHlyZ2lvL2RhbmdlcnpvbmUvZGFuZ2Vyem9uZSJ9LCJpbWFnZSI6eyJkb2NrZXItbWFuaWZlc3QtZGlnZXN0Ijoic2hhMjU2OjRkYTQ0MTIzNWU4NGU5MzUxODc3ODgyN2E1YzU3NDVkNTMyZDdhNDA3OTg4NmUxNjQ3OTI0YmVlN2VmMWMxNGQifSwidHlwZSI6ImNvc2lnbiBjb250YWluZXIgaW1hZ2Ugc2lnbmF0dXJlIn0sIm9wdGlvbmFsIjpudWxsfQ==", + "Cert": null, + "Chain": null, + "Bundle": { + "SignedEntryTimestamp": "Invalid signed entry timestamp", + "Payload": { + "body": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiIyMGE2ZDU1NTk4Y2U0NjU3NWZkZjViZGU3YzhhYWE2YTU2ZjZlMGRmOWNiYTY1MTJhMDAxODhjMTU1NGIzYjE3In19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FUUNJREp4dkI3bEJVK1ZOWUJEMHh3LzNCaTh3WTdHUEoyZkJQN21VRmJndUFwb0FpQUlwdVFUK3NnYXRPWTZ5WGtrQThLL3NNNDBkNS9ndDdqUXl3V1BicTUraXc9PSIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCUVZVSk1TVU1nUzBWWkxTMHRMUzBLVFVacmQwVjNXVWhMYjFwSmVtb3dRMEZSV1VsTGIxcEplbW93UkVGUlkwUlJaMEZGYjBVd1ExaE1SMlptTnpsbVVqaExlVkJ1VTNaUFdUYzBWVUpyZEFveWMweHBLMkZXUmxWNlV6RlJkM1EwZDI5emVFaG9ZMFJPTWtJMlVWTnpUR3gyWjNOSU9ESnhObkZqUVRaUVRESlRaRk12Y0RScVYwZEJQVDBLTFMwdExTMUZUa1FnVUZWQ1RFbERJRXRGV1MwdExTMHRDZz09In19fX0=", + "integratedTime": 1738688492, + "logIndex": 168652066, + "logID": "c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d" + } + }, + "RFC3161Timestamp": null + } +] \ No newline at end of file diff --git a/tests/assets/signatures/invalid/95b432860b272938246b10e1cfc89a24e1db352b3aebaa799c4284c42c46bd95/7b21dbdebffed855621dfcdeaa52230dc6566997f852ef5d62b0338b46796e01.json b/tests/assets/signatures/invalid/95b432860b272938246b10e1cfc89a24e1db352b3aebaa799c4284c42c46bd95/7b21dbdebffed855621dfcdeaa52230dc6566997f852ef5d62b0338b46796e01.json new file mode 100644 index 0000000..9594f7f --- /dev/null +++ b/tests/assets/signatures/invalid/95b432860b272938246b10e1cfc89a24e1db352b3aebaa799c4284c42c46bd95/7b21dbdebffed855621dfcdeaa52230dc6566997f852ef5d62b0338b46796e01.json @@ -0,0 +1,18 @@ +[ + { + "Base64Signature": "MEUCIQC2WlJH+B8VuX1c6i4sDwEGEZc53hXUD6/ds9TMJ3HrfwIgCxSnrNYRD2c8XENqfqc+Ik1gx0DK9kPNsn/Lt8V/dCo=", + "Payload": "eyJjcml0aWNhbCI6eyJpZGVudGl0eSI6eyJkb2NrZXItcmVmZXJlbmNlIjoiZ2hjci5pby9hbG1ldC9kYW5nZXJ6b25lL2RhbmdlcnpvbmUifSwiaW1hZ2UiOnsiZG9ja2VyLW1hbmlmZXN0LWRpZ2VzdCI6InNoYTI1Njo3YjIxZGJkZWJmZmVkODU1NjIxZGZjZGVhYTUyMjMwZGM2NTY2OTk3Zjg1MmVmNWQ2MmIwMzM4YjQ2Nzk2ZTAxIn0sInR5cGUiOiJjb3NpZ24gY29udGFpbmVyIGltYWdlIHNpZ25hdHVyZSJ9LCJvcHRpb25hbCI6bnVsbH0=", + "Cert": null, + "Chain": null, + "Bundle": { + "SignedEntryTimestamp": "MEYCIQDn04gOHqiZcwUO+NVV9+29+abu6O/k1ve9zatJ3gVu9QIhAJL3E+mqVPdMPfMSdhHt2XDQsYzfRDDJNJEABQlbV3Jg", + "Payload": { + "body": "Invalid bundle payload body", + "integratedTime": 1738862352, + "logIndex": 169369149, + "logID": "c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d" + } + }, + "RFC3161Timestamp": null + } +] \ No newline at end of file diff --git a/tests/assets/signatures/tempered/95b432860b272938246b10e1cfc89a24e1db352b3aebaa799c4284c42c46bd95/19e8eacd75879d05f6621c2ea8dd955e68ee3e07b41b9d53f4c8cc9929a68a67.json b/tests/assets/signatures/tempered/95b432860b272938246b10e1cfc89a24e1db352b3aebaa799c4284c42c46bd95/19e8eacd75879d05f6621c2ea8dd955e68ee3e07b41b9d53f4c8cc9929a68a67.json new file mode 100644 index 0000000..54a49bf --- /dev/null +++ b/tests/assets/signatures/tempered/95b432860b272938246b10e1cfc89a24e1db352b3aebaa799c4284c42c46bd95/19e8eacd75879d05f6621c2ea8dd955e68ee3e07b41b9d53f4c8cc9929a68a67.json @@ -0,0 +1,18 @@ +[ + { + "Base64Signature": "MAIhAJWLYU9Hvb26Gn9ysS4JL2isLhra63yzC3tJG9ZoREuPAiEAlLnDnvTGUGuXdxrBXmMPm870OG68KS36z2sq2DrvkkAK", + "Payload": "eyJjcml0aWNhbCI6eyJpZGVudGl0eSI6eyJkb2NrZXItcmVmZXJlbmNlIjoiZ2hjci5pby9hbG1ldC9kYW5nZXJ6b25lL2RhbmdlcnpvbmUifSwiaW1hZ2UiOnsiZG9ja2VyLW1hbmlmZXN0LWRpZ2VzdCI6InNoYTI1NjoxOWU4ZWFjZDc1ODc5ZDA1ZjY2MjFjMmVhOGRkOTU1ZTY4ZWUzZTA3YjQxYjlkNTNmNGM4Y2M5OTI5YTY4YTY3In0sInR5cGUiOiJjb3NpZ24gY29udGFpbmVyIGltYWdlIHNpZ25hdHVyZSJ9LCJvcHRpb25hbCI6bnVsbH0=", + "Cert": null, + "Chain": null, + "Bundle": { + "SignedEntryTimestamp": "MEUCIC9oXH9VVP96frVOmDw704FBqMN/Bpm2RMdTm6BtSwL/AiEA6mCIjhV65fYuy4CwjsIzQHi/oW6IBwtd6oCvN2dI6HQ=", + "Payload": { + "body": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiJmMjEwNDJjY2RjOGU0ZjA1ZGEzNmE5ZjU4ODg5MmFlZGRlMzYzZTQ2ZWNjZGZjM2MyNzAyMTkwZDU0YTdmZmVlIn19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FWUNJUUNWaTJGUFI3Mjl1aHAvY3JFdUNTOW9yQzRhMnV0OHN3dDdTUnZXYUVSTGp3SWhBSlM1dzU3MHhsQnJsM2Nhd1Y1akQ1dk85RGh1dkNrdCtzOXJLdGc2NzVKQSIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCUVZVSk1TVU1nUzBWWkxTMHRMUzBLVFVacmQwVjNXVWhMYjFwSmVtb3dRMEZSV1VsTGIxcEplbW93UkVGUlkwUlJaMEZGYjBVd1ExaE1SMlptTnpsbVVqaExlVkJ1VTNaUFdUYzBWVUpyZEFveWMweHBLMkZXUmxWNlV6RlJkM1EwZDI5emVFaG9ZMFJPTWtJMlVWTnpUR3gyWjNOSU9ESnhObkZqUVRaUVRESlRaRk12Y0RScVYwZEJQVDBLTFMwdExTMUZUa1FnVUZWQ1RFbERJRXRGV1MwdExTMHRDZz09In19fX0=", + "integratedTime": 1738752154, + "logIndex": 168898587, + "logID": "c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d" + } + }, + "RFC3161Timestamp": null + } +] \ No newline at end of file diff --git a/tests/assets/signatures/tempered/95b432860b272938246b10e1cfc89a24e1db352b3aebaa799c4284c42c46bd95/220b52200e3e47b1b42010667fcaa9338681e64dd3e34a34873866cb051d694e.json b/tests/assets/signatures/tempered/95b432860b272938246b10e1cfc89a24e1db352b3aebaa799c4284c42c46bd95/220b52200e3e47b1b42010667fcaa9338681e64dd3e34a34873866cb051d694e.json new file mode 100644 index 0000000..8bb1af4 --- /dev/null +++ b/tests/assets/signatures/tempered/95b432860b272938246b10e1cfc89a24e1db352b3aebaa799c4284c42c46bd95/220b52200e3e47b1b42010667fcaa9338681e64dd3e34a34873866cb051d694e.json @@ -0,0 +1,18 @@ +[ + { + "Base64Signature": "MEQCICi2AOAJbS1k3334VMSo+qxaI4f5VoNnuVExZ4tfIu7rAiAiwuKdo8rGfFMGMLSFSQvoLF3JuwFy4JtNW6kQlwH7vg==", + "Payload": "eyJjcml0aWNhbCI6eyJpZGVudGl0eSI6eyJkb2NrZXItcmVmZXJlbmNlIjoiZ2hjci5pby9oNHh4MHIvZGFuZ2Vyem9uZS9kYW5nZXJ6b25lIn0sImltYWdlIjp7ImRvY2tlci1tYW5pZmVzdC1kaWdlc3QiOiJzaGEyNTY6MjIwYjUyMjAwZTNlNDdiMWI0MjAxMDY2N2ZjYWE5MzM4NjgxZTY0ZGQzZTM0YTM0ODczODY2Y2IwNTFkNjk0ZSJ9LCJ0eXBlIjoiY29zaWduIGNvbnRhaW5lciBpbWFnZSBzaWduYXR1cmUifSwib3B0aW9uYWwiOm51bGx9Cg==", + "Cert": null, + "Chain": null, + "Bundle": { + "SignedEntryTimestamp": "MEUCIEvx6NtFeAag9TplqMLjVczT/tC6lpKe9SnrxbehBlxfAiEA07BE3f5JsMLsUsmHD58D6GaZr2yz+yQ66Os2ps8oKz8=", + "Payload": { + "body": "eyJhcGlWZXJzaW9uIjoiNi42LjYiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiI4YmJmNGRiNjBmMmExM2IyNjI2NTI3MzljNWM5ZTYwNjNiMDYyNjVlODU1Zjc3MTdjMTdlYWY4YzViZTQyYWUyIn19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FUUNJQ2kyQU9BSmJTMWszMzM0Vk1TbytxeGFJNGY1Vm9ObnVWRXhaNHRmSXU3ckFpQWl3dUtkbzhyR2ZGTUdNTFNGU1F2b0xGM0p1d0Z5NEp0Tlc2a1Fsd0g3dmc9PSIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCUVZVSk1TVU1nUzBWWkxTMHRMUzBLVFVacmQwVjNXVWhMYjFwSmVtb3dRMEZSV1VsTGIxcEplbW93UkVGUlkwUlJaMEZGYjBVd1ExaE1SMlptTnpsbVVqaExlVkJ1VTNaUFdUYzBWVUpyZEFveWMweHBLMkZXUmxWNlV6RlJkM1EwZDI5emVFaG9ZMFJPTWtJMlVWTnpUR3gyWjNOSU9ESnhObkZqUVRaUVRESlRaRk12Y0RScVYwZEJQVDBLTFMwdExTMUZUa1FnVUZWQ1RFbERJRXRGV1MwdExTMHRDZz09In19fX0K", + "integratedTime": 1738859497, + "logIndex": 169356501, + "logID": "c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d" + } + }, + "RFC3161Timestamp": null + } +] \ No newline at end of file diff --git a/tests/assets/signatures/tempered/95b432860b272938246b10e1cfc89a24e1db352b3aebaa799c4284c42c46bd95/README.md b/tests/assets/signatures/tempered/95b432860b272938246b10e1cfc89a24e1db352b3aebaa799c4284c42c46bd95/README.md new file mode 100644 index 0000000..16819a4 --- /dev/null +++ b/tests/assets/signatures/tempered/95b432860b272938246b10e1cfc89a24e1db352b3aebaa799c4284c42c46bd95/README.md @@ -0,0 +1 @@ +This folder contain signatures which have been tempered-with. The goal is to have signatures that looks valid, but actually aren't. diff --git a/tests/assets/signatures/valid/95b432860b272938246b10e1cfc89a24e1db352b3aebaa799c4284c42c46bd95/19e8eacd75879d05f6621c2ea8dd955e68ee3e07b41b9d53f4c8cc9929a68a67.json b/tests/assets/signatures/valid/95b432860b272938246b10e1cfc89a24e1db352b3aebaa799c4284c42c46bd95/19e8eacd75879d05f6621c2ea8dd955e68ee3e07b41b9d53f4c8cc9929a68a67.json new file mode 100644 index 0000000..01db986 --- /dev/null +++ b/tests/assets/signatures/valid/95b432860b272938246b10e1cfc89a24e1db352b3aebaa799c4284c42c46bd95/19e8eacd75879d05f6621c2ea8dd955e68ee3e07b41b9d53f4c8cc9929a68a67.json @@ -0,0 +1 @@ +[{"Base64Signature": "MEYCIQCVi2FPR729uhp/crEuCS9orC4a2ut8swt7SRvWaERLjwIhAJS5w570xlBrl3cawV5jD5vO9DhuvCkt+s9rKtg675JA", "Payload": "eyJjcml0aWNhbCI6eyJpZGVudGl0eSI6eyJkb2NrZXItcmVmZXJlbmNlIjoiZ2hjci5pby9hbG1ldC9kYW5nZXJ6b25lL2RhbmdlcnpvbmUifSwiaW1hZ2UiOnsiZG9ja2VyLW1hbmlmZXN0LWRpZ2VzdCI6InNoYTI1NjoxOWU4ZWFjZDc1ODc5ZDA1ZjY2MjFjMmVhOGRkOTU1ZTY4ZWUzZTA3YjQxYjlkNTNmNGM4Y2M5OTI5YTY4YTY3In0sInR5cGUiOiJjb3NpZ24gY29udGFpbmVyIGltYWdlIHNpZ25hdHVyZSJ9LCJvcHRpb25hbCI6bnVsbH0=", "Cert": null, "Chain": null, "Bundle": {"SignedEntryTimestamp": "MEUCIC9oXH9VVP96frVOmDw704FBqMN/Bpm2RMdTm6BtSwL/AiEA6mCIjhV65fYuy4CwjsIzQHi/oW6IBwtd6oCvN2dI6HQ=", "Payload": {"body": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiJmMjEwNDJjY2RjOGU0ZjA1ZGEzNmE5ZjU4ODg5MmFlZGRlMzYzZTQ2ZWNjZGZjM2MyNzAyMTkwZDU0YTdmZmVlIn19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FWUNJUUNWaTJGUFI3Mjl1aHAvY3JFdUNTOW9yQzRhMnV0OHN3dDdTUnZXYUVSTGp3SWhBSlM1dzU3MHhsQnJsM2Nhd1Y1akQ1dk85RGh1dkNrdCtzOXJLdGc2NzVKQSIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCUVZVSk1TVU1nUzBWWkxTMHRMUzBLVFVacmQwVjNXVWhMYjFwSmVtb3dRMEZSV1VsTGIxcEplbW93UkVGUlkwUlJaMEZGYjBVd1ExaE1SMlptTnpsbVVqaExlVkJ1VTNaUFdUYzBWVUpyZEFveWMweHBLMkZXUmxWNlV6RlJkM1EwZDI5emVFaG9ZMFJPTWtJMlVWTnpUR3gyWjNOSU9ESnhObkZqUVRaUVRESlRaRk12Y0RScVYwZEJQVDBLTFMwdExTMUZUa1FnVUZWQ1RFbERJRXRGV1MwdExTMHRDZz09In19fX0=", "integratedTime": 1738752154, "logIndex": 168898587, "logID": "c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d"}}, "RFC3161Timestamp": null}] \ No newline at end of file diff --git a/tests/assets/signatures/valid/95b432860b272938246b10e1cfc89a24e1db352b3aebaa799c4284c42c46bd95/220b52200e3e47b1b42010667fcaa9338681e64dd3e34a34873866cb051d694e.json b/tests/assets/signatures/valid/95b432860b272938246b10e1cfc89a24e1db352b3aebaa799c4284c42c46bd95/220b52200e3e47b1b42010667fcaa9338681e64dd3e34a34873866cb051d694e.json new file mode 100644 index 0000000..8827c9c --- /dev/null +++ b/tests/assets/signatures/valid/95b432860b272938246b10e1cfc89a24e1db352b3aebaa799c4284c42c46bd95/220b52200e3e47b1b42010667fcaa9338681e64dd3e34a34873866cb051d694e.json @@ -0,0 +1 @@ +[{"Base64Signature": "MEQCICi2AOAJbS1k3334VMSo+qxaI4f5VoNnuVExZ4tfIu7rAiAiwuKdo8rGfFMGMLSFSQvoLF3JuwFy4JtNW6kQlwH7vg==", "Payload": "eyJjcml0aWNhbCI6eyJpZGVudGl0eSI6eyJkb2NrZXItcmVmZXJlbmNlIjoiZ2hjci5pby9hbG1ldC9kYW5nZXJ6b25lL2RhbmdlcnpvbmUifSwiaW1hZ2UiOnsiZG9ja2VyLW1hbmlmZXN0LWRpZ2VzdCI6InNoYTI1NjoyMjBiNTIyMDBlM2U0N2IxYjQyMDEwNjY3ZmNhYTkzMzg2ODFlNjRkZDNlMzRhMzQ4NzM4NjZjYjA1MWQ2OTRlIn0sInR5cGUiOiJjb3NpZ24gY29udGFpbmVyIGltYWdlIHNpZ25hdHVyZSJ9LCJvcHRpb25hbCI6bnVsbH0=", "Cert": null, "Chain": null, "Bundle": {"SignedEntryTimestamp": "MEUCIEvx6NtFeAag9TplqMLjVczT/tC6lpKe9SnrxbehBlxfAiEA07BE3f5JsMLsUsmHD58D6GaZr2yz+yQ66Os2ps8oKz8=", "Payload": {"body": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiI4YmJmNGRiNjBmMmExM2IyNjI2NTI3MzljNWM5ZTYwNjNiMDYyNjVlODU1Zjc3MTdjMTdlYWY4YzViZTQyYWUyIn19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FUUNJQ2kyQU9BSmJTMWszMzM0Vk1TbytxeGFJNGY1Vm9ObnVWRXhaNHRmSXU3ckFpQWl3dUtkbzhyR2ZGTUdNTFNGU1F2b0xGM0p1d0Z5NEp0Tlc2a1Fsd0g3dmc9PSIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCUVZVSk1TVU1nUzBWWkxTMHRMUzBLVFVacmQwVjNXVWhMYjFwSmVtb3dRMEZSV1VsTGIxcEplbW93UkVGUlkwUlJaMEZGYjBVd1ExaE1SMlptTnpsbVVqaExlVkJ1VTNaUFdUYzBWVUpyZEFveWMweHBLMkZXUmxWNlV6RlJkM1EwZDI5emVFaG9ZMFJPTWtJMlVWTnpUR3gyWjNOSU9ESnhObkZqUVRaUVRESlRaRk12Y0RScVYwZEJQVDBLTFMwdExTMUZUa1FnVUZWQ1RFbERJRXRGV1MwdExTMHRDZz09In19fX0=", "integratedTime": 1738859497, "logIndex": 169356501, "logID": "c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d"}}, "RFC3161Timestamp": null}] \ No newline at end of file diff --git a/tests/assets/signatures/valid/95b432860b272938246b10e1cfc89a24e1db352b3aebaa799c4284c42c46bd95/4da441235e84e93518778827a5c5745d532d7a4079886e1647924bee7ef1c14d.json b/tests/assets/signatures/valid/95b432860b272938246b10e1cfc89a24e1db352b3aebaa799c4284c42c46bd95/4da441235e84e93518778827a5c5745d532d7a4079886e1647924bee7ef1c14d.json new file mode 100644 index 0000000..fd13e9c --- /dev/null +++ b/tests/assets/signatures/valid/95b432860b272938246b10e1cfc89a24e1db352b3aebaa799c4284c42c46bd95/4da441235e84e93518778827a5c5745d532d7a4079886e1647924bee7ef1c14d.json @@ -0,0 +1 @@ +[{"Base64Signature": "MEQCIDJxvB7lBU+VNYBD0xw/3Bi8wY7GPJ2fBP7mUFbguApoAiAIpuQT+sgatOY6yXkkA8K/sM40d5/gt7jQywWPbq5+iw==", "Payload": "eyJjcml0aWNhbCI6eyJpZGVudGl0eSI6eyJkb2NrZXItcmVmZXJlbmNlIjoiZ2hjci5pby9hcHlyZ2lvL2RhbmdlcnpvbmUvZGFuZ2Vyem9uZSJ9LCJpbWFnZSI6eyJkb2NrZXItbWFuaWZlc3QtZGlnZXN0Ijoic2hhMjU2OjRkYTQ0MTIzNWU4NGU5MzUxODc3ODgyN2E1YzU3NDVkNTMyZDdhNDA3OTg4NmUxNjQ3OTI0YmVlN2VmMWMxNGQifSwidHlwZSI6ImNvc2lnbiBjb250YWluZXIgaW1hZ2Ugc2lnbmF0dXJlIn0sIm9wdGlvbmFsIjpudWxsfQ==", "Cert": null, "Chain": null, "Bundle": {"SignedEntryTimestamp": "MEYCIQDuuuHoyZ2i4HKxik4Ju/MWkELwc1w5SfzcpCV7G+vZHAIhAO25R/+lIfQ/kMfC4PfeoWDwLpvnH9cq6dVSzl12i1su", "Payload": {"body": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiIyMGE2ZDU1NTk4Y2U0NjU3NWZkZjViZGU3YzhhYWE2YTU2ZjZlMGRmOWNiYTY1MTJhMDAxODhjMTU1NGIzYjE3In19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FUUNJREp4dkI3bEJVK1ZOWUJEMHh3LzNCaTh3WTdHUEoyZkJQN21VRmJndUFwb0FpQUlwdVFUK3NnYXRPWTZ5WGtrQThLL3NNNDBkNS9ndDdqUXl3V1BicTUraXc9PSIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCUVZVSk1TVU1nUzBWWkxTMHRMUzBLVFVacmQwVjNXVWhMYjFwSmVtb3dRMEZSV1VsTGIxcEplbW93UkVGUlkwUlJaMEZGYjBVd1ExaE1SMlptTnpsbVVqaExlVkJ1VTNaUFdUYzBWVUpyZEFveWMweHBLMkZXUmxWNlV6RlJkM1EwZDI5emVFaG9ZMFJPTWtJMlVWTnpUR3gyWjNOSU9ESnhObkZqUVRaUVRESlRaRk12Y0RScVYwZEJQVDBLTFMwdExTMUZUa1FnVUZWQ1RFbERJRXRGV1MwdExTMHRDZz09In19fX0=", "integratedTime": 1738688492, "logIndex": 168652066, "logID": "c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d"}}, "RFC3161Timestamp": null}] \ No newline at end of file diff --git a/tests/assets/signatures/valid/95b432860b272938246b10e1cfc89a24e1db352b3aebaa799c4284c42c46bd95/7b21dbdebffed855621dfcdeaa52230dc6566997f852ef5d62b0338b46796e01.json b/tests/assets/signatures/valid/95b432860b272938246b10e1cfc89a24e1db352b3aebaa799c4284c42c46bd95/7b21dbdebffed855621dfcdeaa52230dc6566997f852ef5d62b0338b46796e01.json new file mode 100644 index 0000000..e857c4b --- /dev/null +++ b/tests/assets/signatures/valid/95b432860b272938246b10e1cfc89a24e1db352b3aebaa799c4284c42c46bd95/7b21dbdebffed855621dfcdeaa52230dc6566997f852ef5d62b0338b46796e01.json @@ -0,0 +1 @@ +[{"Base64Signature": "MEUCIQC2WlJH+B8VuX1c6i4sDwEGEZc53hXUD6/ds9TMJ3HrfwIgCxSnrNYRD2c8XENqfqc+Ik1gx0DK9kPNsn/Lt8V/dCo=", "Payload": "eyJjcml0aWNhbCI6eyJpZGVudGl0eSI6eyJkb2NrZXItcmVmZXJlbmNlIjoiZ2hjci5pby9hbG1ldC9kYW5nZXJ6b25lL2RhbmdlcnpvbmUifSwiaW1hZ2UiOnsiZG9ja2VyLW1hbmlmZXN0LWRpZ2VzdCI6InNoYTI1Njo3YjIxZGJkZWJmZmVkODU1NjIxZGZjZGVhYTUyMjMwZGM2NTY2OTk3Zjg1MmVmNWQ2MmIwMzM4YjQ2Nzk2ZTAxIn0sInR5cGUiOiJjb3NpZ24gY29udGFpbmVyIGltYWdlIHNpZ25hdHVyZSJ9LCJvcHRpb25hbCI6bnVsbH0=", "Cert": null, "Chain": null, "Bundle": {"SignedEntryTimestamp": "MEYCIQDn04gOHqiZcwUO+NVV9+29+abu6O/k1ve9zatJ3gVu9QIhAJL3E+mqVPdMPfMSdhHt2XDQsYzfRDDJNJEABQlbV3Jg", "Payload": {"body": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiIzZWQwNWJlYTc2ZWFmMzBmYWM1NzBlNzhlODBlZmQxNDNiZWQxNzFjM2VjMDY5MWI2MDU3YjdhMDAzNGEyMzhlIn19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FVUNJUUMyV2xKSCtCOFZ1WDFjNmk0c0R3RUdFWmM1M2hYVUQ2L2RzOVRNSjNIcmZ3SWdDeFNuck5ZUkQyYzhYRU5xZnFjK0lrMWd4MERLOWtQTnNuL0x0OFYvZENvPSIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCUVZVSk1TVU1nUzBWWkxTMHRMUzBLVFVacmQwVjNXVWhMYjFwSmVtb3dRMEZSV1VsTGIxcEplbW93UkVGUlkwUlJaMEZGYjBVd1ExaE1SMlptTnpsbVVqaExlVkJ1VTNaUFdUYzBWVUpyZEFveWMweHBLMkZXUmxWNlV6RlJkM1EwZDI5emVFaG9ZMFJPTWtJMlVWTnpUR3gyWjNOSU9ESnhObkZqUVRaUVRESlRaRk12Y0RScVYwZEJQVDBLTFMwdExTMUZUa1FnVUZWQ1RFbERJRXRGV1MwdExTMHRDZz09In19fX0=", "integratedTime": 1738862352, "logIndex": 169369149, "logID": "c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d"}}, "RFC3161Timestamp": null}] \ No newline at end of file diff --git a/tests/assets/signatures/valid/95b432860b272938246b10e1cfc89a24e1db352b3aebaa799c4284c42c46bd95/fa948726aac29a6ac49f01ec8fbbac18522b35b2491fdf716236a0b3502a2ca7.json b/tests/assets/signatures/valid/95b432860b272938246b10e1cfc89a24e1db352b3aebaa799c4284c42c46bd95/fa948726aac29a6ac49f01ec8fbbac18522b35b2491fdf716236a0b3502a2ca7.json new file mode 100644 index 0000000..660dbbf --- /dev/null +++ b/tests/assets/signatures/valid/95b432860b272938246b10e1cfc89a24e1db352b3aebaa799c4284c42c46bd95/fa948726aac29a6ac49f01ec8fbbac18522b35b2491fdf716236a0b3502a2ca7.json @@ -0,0 +1 @@ +[{"Base64Signature": "MEQCIHqXEMuAmt1pFCsHC71+ejlG5kjKrf1+AQW202OY3vhsAiA0BoDAVgAk9K7SgIRBpIV6u0veyB1iypzV0DteNh3IoQ==", "Payload": "eyJjcml0aWNhbCI6eyJpZGVudGl0eSI6eyJkb2NrZXItcmVmZXJlbmNlIjoiZ2hjci5pby9hbG1ldC9kYW5nZXJ6b25lL2RhbmdlcnpvbmUifSwiaW1hZ2UiOnsiZG9ja2VyLW1hbmlmZXN0LWRpZ2VzdCI6InNoYTI1NjpmYTk0ODcyNmFhYzI5YTZhYzQ5ZjAxZWM4ZmJiYWMxODUyMmIzNWIyNDkxZmRmNzE2MjM2YTBiMzUwMmEyY2E3In0sInR5cGUiOiJjb3NpZ24gY29udGFpbmVyIGltYWdlIHNpZ25hdHVyZSJ9LCJvcHRpb25hbCI6bnVsbH0=", "Cert": null, "Chain": null, "Bundle": {"SignedEntryTimestamp": "MEUCIQCrZ+2SSYdpIOEbyUXXaBxeqT8RTujpqdXipls9hmNvDgIgdWV84PiCY2cI49QjHjun7lj25/znGMDiwjCuPjIPA6Q=", "Payload": {"body": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiI5ZjcwM2I4NTM4MjM4N2U2OTgwNzYxNDg1YzU0NGIzNmJmMThmNTA5ODQwMTMxYzRmOTJhMjE4OTI3MTJmNDJmIn19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FUUNJSHFYRU11QW10MXBGQ3NIQzcxK2VqbEc1a2pLcmYxK0FRVzIwMk9ZM3Zoc0FpQTBCb0RBVmdBazlLN1NnSVJCcElWNnUwdmV5QjFpeXB6VjBEdGVOaDNJb1E9PSIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCUVZVSk1TVU1nUzBWWkxTMHRMUzBLVFVacmQwVjNXVWhMYjFwSmVtb3dRMEZSV1VsTGIxcEplbW93UkVGUlkwUlJaMEZGYjBVd1ExaE1SMlptTnpsbVVqaExlVkJ1VTNaUFdUYzBWVUpyZEFveWMweHBLMkZXUmxWNlV6RlJkM1EwZDI5emVFaG9ZMFJPTWtJMlVWTnpUR3gyWjNOSU9ESnhObkZqUVRaUVRESlRaRk12Y0RScVYwZEJQVDBLTFMwdExTMUZUa1FnVUZWQ1RFbERJRXRGV1MwdExTMHRDZz09In19fX0=", "integratedTime": 1737478056, "logIndex": 164177381, "logID": "c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d"}}, "RFC3161Timestamp": null}, {"Base64Signature": "MEYCIQDg8MeymBLOn+Khue0yK1yQy4Fu/+GXmyC/xezXO/p1JgIhAN6QLojKzkZGxyYirbqRbZCVcIM4YN3Y18FXwpW4RuUy", "Payload": "eyJjcml0aWNhbCI6eyJpZGVudGl0eSI6eyJkb2NrZXItcmVmZXJlbmNlIjoiZ2hjci5pby9hbG1ldC9kYW5nZXJ6b25lL2RhbmdlcnpvbmUifSwiaW1hZ2UiOnsiZG9ja2VyLW1hbmlmZXN0LWRpZ2VzdCI6InNoYTI1NjpmYTk0ODcyNmFhYzI5YTZhYzQ5ZjAxZWM4ZmJiYWMxODUyMmIzNWIyNDkxZmRmNzE2MjM2YTBiMzUwMmEyY2E3In0sInR5cGUiOiJjb3NpZ24gY29udGFpbmVyIGltYWdlIHNpZ25hdHVyZSJ9LCJvcHRpb25hbCI6bnVsbH0=", "Cert": null, "Chain": null, "Bundle": {"SignedEntryTimestamp": "MEUCIQCQLlrH2xo/bA6r386vOwA0OjUe0TqcxROT/Wo220jvGgIgPgRlKnQxWoXlD/Owf1Ogk5XlfXAt2f416LDbk4AoEvk=", "Payload": {"body": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiI5ZjcwM2I4NTM4MjM4N2U2OTgwNzYxNDg1YzU0NGIzNmJmMThmNTA5ODQwMTMxYzRmOTJhMjE4OTI3MTJmNDJmIn19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FWUNJUURnOE1leW1CTE9uK0todWUweUsxeVF5NEZ1LytHWG15Qy94ZXpYTy9wMUpnSWhBTjZRTG9qS3prWkd4eVlpcmJxUmJaQ1ZjSU00WU4zWTE4Rlh3cFc0UnVVeSIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCUVZVSk1TVU1nUzBWWkxTMHRMUzBLVFVacmQwVjNXVWhMYjFwSmVtb3dRMEZSV1VsTGIxcEplbW93UkVGUlkwUlJaMEZGYjBVd1ExaE1SMlptTnpsbVVqaExlVkJ1VTNaUFdUYzBWVUpyZEFveWMweHBLMkZXUmxWNlV6RlJkM1EwZDI5emVFaG9ZMFJPTWtJMlVWTnpUR3gyWjNOSU9ESnhObkZqUVRaUVRESlRaRk12Y0RScVYwZEJQVDBLTFMwdExTMUZUa1FnVUZWQ1RFbERJRXRGV1MwdExTMHRDZz09In19fX0=", "integratedTime": 1737557525, "logIndex": 164445483, "logID": "c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d"}}, "RFC3161Timestamp": null}, {"Base64Signature": "MEQCIEhUVYVW6EdovGDSSZt1Ffc86OfzEKAas94M4eFK7hoFAiA4+6219LktmgJSKuc2ObsnL5QjHyNLk58BwY0s8gBHbQ==", "Payload": "eyJjcml0aWNhbCI6eyJpZGVudGl0eSI6eyJkb2NrZXItcmVmZXJlbmNlIjoiZ2hjci5pby9hbG1ldC9kYW5nZXJ6b25lL2RhbmdlcnpvbmUifSwiaW1hZ2UiOnsiZG9ja2VyLW1hbmlmZXN0LWRpZ2VzdCI6InNoYTI1NjpmYTk0ODcyNmFhYzI5YTZhYzQ5ZjAxZWM4ZmJiYWMxODUyMmIzNWIyNDkxZmRmNzE2MjM2YTBiMzUwMmEyY2E3In0sInR5cGUiOiJjb3NpZ24gY29udGFpbmVyIGltYWdlIHNpZ25hdHVyZSJ9LCJvcHRpb25hbCI6bnVsbH0=", "Cert": null, "Chain": null, "Bundle": {"SignedEntryTimestamp": "MEQCIDRUTMwL+/eW79ARRLE8h/ByCrvo0rOn3vUYQg1E6KIBAiBi/bzoqcL2Ik27KpwfFosww4l7yI+9IqwCvUlkQgEB7g==", "Payload": {"body": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaGFzaGVkcmVrb3JkIiwic3BlYyI6eyJkYXRhIjp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiI5ZjcwM2I4NTM4MjM4N2U2OTgwNzYxNDg1YzU0NGIzNmJmMThmNTA5ODQwMTMxYzRmOTJhMjE4OTI3MTJmNDJmIn19LCJzaWduYXR1cmUiOnsiY29udGVudCI6Ik1FUUNJRWhVVllWVzZFZG92R0RTU1p0MUZmYzg2T2Z6RUtBYXM5NE00ZUZLN2hvRkFpQTQrNjIxOUxrdG1nSlNLdWMyT2Jzbkw1UWpIeU5MazU4QndZMHM4Z0JIYlE9PSIsInB1YmxpY0tleSI6eyJjb250ZW50IjoiTFMwdExTMUNSVWRKVGlCUVZVSk1TVU1nUzBWWkxTMHRMUzBLVFVacmQwVjNXVWhMYjFwSmVtb3dRMEZSV1VsTGIxcEplbW93UkVGUlkwUlJaMEZGYjBVd1ExaE1SMlptTnpsbVVqaExlVkJ1VTNaUFdUYzBWVUpyZEFveWMweHBLMkZXUmxWNlV6RlJkM1EwZDI5emVFaG9ZMFJPTWtJMlVWTnpUR3gyWjNOSU9ESnhObkZqUVRaUVRESlRaRk12Y0RScVYwZEJQVDBLTFMwdExTMUZUa1FnVUZWQ1RFbERJRXRGV1MwdExTMHRDZz09In19fX0=", "integratedTime": 1737567664, "logIndex": 164484602, "logID": "c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d"}}, "RFC3161Timestamp": null}] \ No newline at end of file diff --git a/tests/assets/test.pub.key b/tests/assets/test.pub.key new file mode 100644 index 0000000..a36dd82 --- /dev/null +++ b/tests/assets/test.pub.key @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEoE0CXLGff79fR8KyPnSvOY74UBkt +2sLi+aVFUzS1Qwt4wosxHhcDN2B6QSsLlvgsH82q6qcA6PL2SdS/p4jWGA== +-----END PUBLIC KEY----- diff --git a/tests/test_signatures.py b/tests/test_signatures.py new file mode 100644 index 0000000..2681896 --- /dev/null +++ b/tests/test_signatures.py @@ -0,0 +1,314 @@ +import json +import unittest +from pathlib import Path + +import pytest +from pytest_subprocess import FakeProcess + +from dangerzone import errors as dzerrors +from dangerzone.updater import errors +from dangerzone.updater.signatures import ( + Signature, + get_config_dir, + get_last_log_index, + get_log_index_from_signatures, + get_remote_signatures, + is_update_available, + load_and_verify_signatures, + prepare_airgapped_archive, + store_signatures, + upgrade_container_image, + verify_local_image, + verify_signature, + verify_signatures, +) + +ASSETS_PATH = Path(__file__).parent / "assets" +TEST_PUBKEY_PATH = ASSETS_PATH / "test.pub.key" +INVALID_SIGNATURES_PATH = ASSETS_PATH / "signatures" / "invalid" +VALID_SIGNATURES_PATH = ASSETS_PATH / "signatures" / "valid" +TEMPERED_SIGNATURES_PATH = ASSETS_PATH / "signatures" / "tempered" + +RANDOM_DIGEST = "aacc9b586648bbe3040f2822153b1d5ead2779af45ff750fd6f04daf4a9f64b4" + + +def test_load_valid_signatures(mocker): + mocker.patch("dangerzone.updater.signatures.SIGNATURES_PATH", VALID_SIGNATURES_PATH) + valid_signatures = list(VALID_SIGNATURES_PATH.glob("**/*.json")) + assert len(valid_signatures) > 0 + for file in valid_signatures: + signatures = load_and_verify_signatures(file.stem, TEST_PUBKEY_PATH) + assert isinstance(signatures, list) + assert len(signatures) > 0 + + +def test_load_invalid_signatures(mocker): + mocker.patch( + "dangerzone.updater.signatures.SIGNATURES_PATH", INVALID_SIGNATURES_PATH + ) + invalid_signatures = list(INVALID_SIGNATURES_PATH.glob("**/*.json")) + assert len(invalid_signatures) > 0 + for file in invalid_signatures: + with pytest.raises(errors.SignatureError): + load_and_verify_signatures(file.stem, TEST_PUBKEY_PATH) + + +def test_load_tempered_signatures(mocker): + mocker.patch( + "dangerzone.updater.signatures.SIGNATURES_PATH", TEMPERED_SIGNATURES_PATH + ) + tempered_signatures = list(TEMPERED_SIGNATURES_PATH.glob("**/*.json")) + assert len(tempered_signatures) > 0 + for file in tempered_signatures: + with pytest.raises(errors.SignatureError): + load_and_verify_signatures(file.stem, TEST_PUBKEY_PATH) + + +def test_get_log_index_from_signatures(): + signatures = [{"Bundle": {"Payload": {"logIndex": 1}}}] + assert get_log_index_from_signatures(signatures) == 1 + + +def test_get_log_index_from_signatures_empty(): + signatures = [] + assert get_log_index_from_signatures(signatures) == 0 + + +def test_get_log_index_from_malformed_signatures(): + signatures = [{"Bundle": {"Payload": {"logIndex": "foo"}}}] + assert get_log_index_from_signatures(signatures) == 0 + + +def test_get_log_index_from_missing_log_index(): + signatures = [{"Bundle": {"Payload": {}}}] + assert get_log_index_from_signatures(signatures) == 0 + + +def test_upgrade_container_image_if_already_up_to_date(mocker): + mocker.patch( + "dangerzone.updater.signatures.is_update_available", return_value=(False, None) + ) + with pytest.raises(errors.ImageAlreadyUpToDate): + upgrade_container_image( + "ghcr.io/freedomofpress/dangerzone/dangerzone", "sha256:123456", "test.pub" + ) + + +def test_upgrade_container_without_signatures(mocker): + mocker.patch( + "dangerzone.updater.signatures.is_update_available", + return_value=(True, "sha256:123456"), + ) + mocker.patch("dangerzone.updater.signatures.get_remote_signatures", return_value=[]) + with pytest.raises(errors.SignatureVerificationError): + upgrade_container_image( + "ghcr.io/freedomofpress/dangerzone/dangerzone", + "sha256:123456", + "test.pub", + ) + + +def test_upgrade_container_lower_log_index(mocker): + image_digest = "4da441235e84e93518778827a5c5745d532d7a4079886e1647924bee7ef1c14d" + signatures = load_and_verify_signatures( + image_digest, + TEST_PUBKEY_PATH, + bypass_verification=True, + signatures_path=VALID_SIGNATURES_PATH, + ) + mocker.patch( + "dangerzone.updater.signatures.is_update_available", + return_value=( + True, + image_digest, + ), + ) + mocker.patch( + "dangerzone.updater.signatures.get_remote_signatures", + return_value=signatures, + ) + # Mock to avoid loosing time on test failures + mocker.patch("dangerzone.container_utils.container_pull") + # The log index of the incoming signatures is 168652066 + mocker.patch( + "dangerzone.updater.signatures.get_last_log_index", + return_value=168652067, + ) + + with pytest.raises(errors.InvalidLogIndex): + upgrade_container_image( + "ghcr.io/freedomofpress/dangerzone/dangerzone", + image_digest, + TEST_PUBKEY_PATH, + ) + + +def test_prepare_airgapped_archive_requires_digest(): + with pytest.raises(errors.AirgappedImageDownloadError): + prepare_airgapped_archive( + "ghcr.io/freedomofpress/dangerzone/dangerzone", "test.tar" + ) + + +def test_get_remote_signatures_error(fp: FakeProcess, mocker): + image = "ghcr.io/freedomofpress/dangerzone/dangerzone" + digest = "123456" + mocker.patch("dangerzone.updater.cosign.ensure_installed", return_value=True) + fp.register_subprocess( + ["cosign", "download", "signature", f"{image}@sha256:{digest}"], returncode=1 + ) + with pytest.raises(errors.NoRemoteSignatures): + get_remote_signatures(image, digest) + + +def test_get_remote_signatures_empty(fp: FakeProcess, mocker): + image = "ghcr.io/freedomofpress/dangerzone/dangerzone" + digest = "123456" + mocker.patch("dangerzone.updater.cosign.ensure_installed", return_value=True) + fp.register_subprocess( + ["cosign", "download", "signature", f"{image}@sha256:{digest}"], + stdout=json.dumps([]), + ) + with pytest.raises(errors.NoRemoteSignatures): + get_remote_signatures(image, digest) + + +def test_get_remote_signatures_cosign_error(): + pass + + +def test_store_signatures_with_different_digests( + valid_signature, signature_other_digest, mocker, tmp_path +): + """Test that store_signatures raises an error when a signature's digest doesn't match.""" + + image_digest = "sha256:123456" + + # Mock the signatures path + signatures_path = tmp_path / "signatures" + signatures_path.mkdir() + mocker.patch("dangerzone.updater.signatures.SIGNATURES_PATH", signatures_path) + + # Mock get_log_index_from_signatures + mocker.patch( + "dangerzone.updater.signatures.get_log_index_from_signatures", + return_value=100, + ) + + # Mock get_last_log_index + # Verify that the signatures file was not created + assert not (signatures_path / f"{image_digest}.json").exists() + assert not (signatures_path / "last_log_index").exists() + + +def test_stores_signatures_updates_last_log_index(valid_signature, mocker, tmp_path): + """Test that store_signatures updates the last log index file.""" + signatures = [valid_signature] + # Extract the digest from the signature + image_digest = Signature(valid_signature).manifest_digest + + # Mock the signatures path + signatures_path = tmp_path / "signatures" + signatures_path.mkdir() + mocker.patch("dangerzone.updater.signatures.SIGNATURES_PATH", signatures_path) + + # Create an existing last_log_index file with a lower value + with open(signatures_path / "last_log_index", "w") as f: + f.write("50") + + # Mock get_log_index_from_signatures to return a higher value + mocker.patch( + "dangerzone.updater.signatures.get_log_index_from_signatures", + return_value=100, + ) + + # Call store_signatures + with pytest.raises(errors.SignatureMismatch): + store_signatures(signatures, image_digest, TEST_PUBKEY_PATH) + ("dangerzone.updater.signatures.get_last_log_index",) + # Verify that the signatures file was not created + assert not (signatures_path / f"{image_digest}.json").exists() + + # Verify that the log index file was not updated + assert not (signatures_path / "last_log_index").exists() + + +def test_stores_signatures_updates_last_log_index(valid_signature, mocker, tmp_path): + """Test that store_signatures updates the last log index file.""" + signatures = [valid_signature] + # Extract the digest from the signature + image_digest = Signature(valid_signature).manifest_digest + + +def test_get_file_digest(): + # Mock the signatures path + signatures_path = tmp_path / "signatures" + signatures_path.mkdir() + mocker.patch("dangerzone.updater.signatures.SIGNATURES_PATH", signatures_path) + + # Create an existing last_log_index file with a lower value + with open(signatures_path / "last_log_index", "w") as f: + f.write("50") + + # Mock get_log_index_from_signatures to return a higher value + mocker.patch( + "dangerzone.updater.signatures.get_log_index_from_signatures", + return_value=100, + ) + + # Call store_signatures + store_signatures(signatures, image_digest, TEST_PUBKEY_PATH) + + # Verify that the log index file was updated + assert (signatures_path / "last_log_index").exists() + with open(signatures_path / "last_log_index", "r") as f: + assert f.read() == "100" + + +def test_is_update_available_when_no_local_image(mocker): + """ + Test that is_update_available returns True when no local image is + currently present. + """ + # Mock container_image_exists to return False + mocker.patch( + "dangerzone.container_utils.get_local_image_digest", + side_effect=dzerrors.ImageNotPresentException, + ) + + # Mock get_manifest_digest to return a digest + mocker.patch( + "dangerzone.updater.registry.get_manifest_digest", + return_value=RANDOM_DIGEST, + ) + + # Call is_update_available + update_available, digest = is_update_available("ghcr.io/freedomofpress/dangerzone") + + # Verify the result + assert update_available is True + assert digest == RANDOM_DIGEST + + +def test_verify_signature(valid_signature): + """Test that verify_signature raises an error when the payload digest doesn't match.""" + verify_signature( + valid_signature, + Signature(valid_signature).manifest_digest, + TEST_PUBKEY_PATH, + ) + + +def test_verify_signature_tempered(tempered_signature): + """Test that verify_signature raises an error when the payload digest doesn't match.""" + # Call verify_signature and expect an error + with pytest.raises(errors.SignatureError): + verify_signature( + tempered_signature, + Signature(tempered_signature).manifest_digest, + TEST_PUBKEY_PATH, + ) + + +def test_verify_signatures_not_0(): + pass