From 238ea527e6fc9cf3f2966716507c20a3bb1cfb6d 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] Add signatures tests --- dangerzone/container_utils.py | 7 +- dangerzone/isolation_provider/container.py | 25 +- dangerzone/updater/signatures.py | 85 ++++--- 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/conftest.py | 8 + tests/test_registry.py | 238 ++++++++++++++++++ tests/test_signatures.py | 111 ++++++-- 20 files changed, 539 insertions(+), 60 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_registry.py diff --git a/dangerzone/container_utils.py b/dangerzone/container_utils.py index 162901e..7c8f06d 100644 --- a/dangerzone/container_utils.py +++ b/dangerzone/container_utils.py @@ -261,7 +261,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 46f382a..d452967 100644 --- a/dangerzone/updater/signatures.py +++ b/dangerzone/updater/signatures.py @@ -37,7 +37,7 @@ LAST_LOG_INDEX = SIGNATURES_PATH / "last_log_index" __all__ = [ "verify_signature", - "load_signatures", + "load_and_verify_signatures", "store_signatures", "verify_offline_image_signature", ] @@ -77,11 +77,15 @@ def verify_signature(signature: dict, image_digest: str, pubkey: str | Path) -> 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( "The given signature does not match the expected image digest " @@ -98,11 +102,14 @@ def verify_signature(signature: dict, image_digest: str, pubkey: str | Path) -> 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, @@ -149,8 +156,12 @@ def verify_signatures( image_digest: str, pubkey: str, ) -> bool: + if len(signatures) < 1: + raise errors.SignatureVerificationError("No signatures found") + for signature in signatures: verify_signature(signature, image_digest, pubkey) + return True @@ -164,9 +175,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: @@ -302,13 +318,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." @@ -318,7 +342,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: @@ -380,32 +409,26 @@ 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)) + signatures = list(filter(bool, map(json.loads, signatures_raw))) if len(signatures) < 1: raise errors.NoRemoteSignatures("No signatures found for the image") return signatures @@ -418,8 +441,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/conftest.py b/tests/conftest.py index b55b5ca..64f1a44 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -13,6 +13,13 @@ from dangerzone.gui import Application sys.dangerzone_dev = True # type: ignore[attr-defined] +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" + + # Use this fixture to make `pytest-qt` invoke our custom QApplication. # See https://pytest-qt.readthedocs.io/en/latest/qapplication.html#testing-custom-qapplications @pytest.fixture(scope="session") @@ -132,6 +139,7 @@ for_each_doc = pytest.mark.parametrize( "doc", test_docs, ids=[str(doc.name) for doc in test_docs] ) + # External Docs - base64 docs encoded for externally sourced documents # XXX to reduce the chance of accidentally opening them test_docs_external_dir = Path(__file__).parent.joinpath(SAMPLE_EXTERNAL_DIRECTORY) diff --git a/tests/test_registry.py b/tests/test_registry.py new file mode 100644 index 0000000..efbf576 --- /dev/null +++ b/tests/test_registry.py @@ -0,0 +1,238 @@ +import hashlib + +import pytest +import requests +from pytest_mock import MockerFixture + +from dangerzone.updater.registry import ( + Image, + _get_auth_header, + _url, + get_manifest, + get_manifest_digest, + list_tags, + parse_image_location, +) + + +def test_parse_image_location_no_tag(): + """Test that parse_image_location correctly handles an image location without a tag.""" + image_str = "ghcr.io/freedomofpress/dangerzone" + image = parse_image_location(image_str) + + assert isinstance(image, Image) + assert image.registry == "ghcr.io" + assert image.namespace == "freedomofpress" + assert image.image_name == "dangerzone" + assert image.tag == "latest" # Default tag should be "latest" + assert image.digest is None + + +def test_parse_image_location_with_tag(): + """Test that parse_image_location correctly handles an image location with a tag.""" + image_str = "ghcr.io/freedomofpress/dangerzone:v0.4.2" + image = parse_image_location(image_str) + + assert isinstance(image, Image) + assert image.registry == "ghcr.io" + assert image.namespace == "freedomofpress" + assert image.image_name == "dangerzone" + assert image.tag == "v0.4.2" + + +def test_parse_image_location_tag_plus_digest(): + """Test that parse_image_location handles an image location with a tag that includes a digest.""" + image_str = ( + "ghcr.io/freedomofpress/dangerzone" + ":20250205-0.8.0-148-ge67fbc1" + "@sha256:19e8eacd75879d05f6621c2ea8dd955e68ee3e07b41b9d53f4c8cc9929a68a67" + ) + + image = parse_image_location(image_str) + + assert isinstance(image, Image) + assert image.registry == "ghcr.io" + assert image.namespace == "freedomofpress" + assert image.image_name == "dangerzone" + assert image.tag == "20250205-0.8.0-148-ge67fbc1" + assert ( + image.digest + == "sha256:19e8eacd75879d05f6621c2ea8dd955e68ee3e07b41b9d53f4c8cc9929a68a67" + ) + + +def test_parse_invalid_image_location(): + """Test that parse_image_location raises an error for invalid image locations.""" + invalid_image_locations = [ + "ghcr.io/dangerzone", # Missing namespace + "ghcr.io/freedomofpress/dangerzone:", # Empty tag + "freedomofpress/dangerzone", # Missing registry + "ghcr.io:freedomofpress/dangerzone", # Invalid format + "", # Empty string + ] + + for invalid_image in invalid_image_locations: + with pytest.raises(ValueError, match="Malformed image location"): + parse_image_location(invalid_image) + + +def test_list_tags(mocker: MockerFixture): + """Test that list_tags correctly retrieves tags from the registry.""" + # Mock the authentication response + image_str = "ghcr.io/freedomofpress/dangerzone" + + # Mock requests.get to return appropriate values for both calls + mock_response_auth = mocker.Mock() + mock_response_auth.json.return_value = {"token": "dummy_token"} + mock_response_auth.raise_for_status.return_value = None + + mock_response_tags = mocker.Mock() + mock_response_tags.json.return_value = { + "tags": ["v0.4.0", "v0.4.1", "v0.4.2", "latest"] + } + mock_response_tags.raise_for_status.return_value = None + + # Setup the mock to return different responses for each URL + def mock_get(url, **kwargs): + if "token" in url: + return mock_response_auth + else: + return mock_response_tags + + mocker.patch("requests.get", side_effect=mock_get) + + # Call the function + tags = list_tags(image_str) + + # Verify the result + assert tags == ["v0.4.0", "v0.4.1", "v0.4.2", "latest"] + + +def test_list_tags_auth_error(mocker: MockerFixture): + """Test that list_tags handles authentication errors correctly.""" + image_str = "ghcr.io/freedomofpress/dangerzone" + + # Mock requests.get to raise an HTTPError + mock_response = mocker.Mock() + mock_response.raise_for_status.side_effect = requests.exceptions.HTTPError( + "401 Client Error: Unauthorized" + ) + + mocker.patch("requests.get", return_value=mock_response) + + # Call the function and expect an error + with pytest.raises(requests.exceptions.HTTPError): + list_tags(image_str) + + +def test_list_tags_registry_error(mocker: MockerFixture): + """Test that list_tags handles registry errors correctly.""" + image_str = "ghcr.io/freedomofpress/dangerzone" + + # Mock requests.get to return success for auth but error for tags + mock_response_auth = mocker.Mock() + mock_response_auth.json.return_value = {"token": "dummy_token"} + mock_response_auth.raise_for_status.return_value = None + + mock_response_tags = mocker.Mock() + mock_response_tags.raise_for_status.side_effect = requests.exceptions.HTTPError( + "404 Client Error: Not Found" + ) + + # Setup the mock to return different responses for each URL + def mock_get(url, **kwargs): + if "token" in url: + return mock_response_auth + else: + return mock_response_tags + + mocker.patch("requests.get", side_effect=mock_get) + + # Call the function and expect an error + with pytest.raises(requests.exceptions.HTTPError): + list_tags(image_str) + + +def test_get_manifest(mocker: MockerFixture): + """Test that get_manifest correctly retrieves manifests from the registry.""" + image_str = "ghcr.io/freedomofpress/dangerzone:v0.4.2" + + # Mock the responses + manifest_content = { + "schemaVersion": 2, + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "config": { + "mediaType": "application/vnd.docker.container.image.v1+json", + "size": 1234, + "digest": "sha256:abc123def456", + }, + "layers": [ + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "size": 12345, + "digest": "sha256:layer1", + } + ], + } + + mock_response_auth = mocker.Mock() + mock_response_auth.json.return_value = {"token": "dummy_token"} + mock_response_auth.raise_for_status.return_value = None + + mock_response_manifest = mocker.Mock() + mock_response_manifest.json.return_value = manifest_content + mock_response_manifest.status_code = 200 + mock_response_manifest.raise_for_status.return_value = None + + # Setup the mock to return different responses for each URL + def mock_get(url, **kwargs): + if "token" in url: + return mock_response_auth + else: + return mock_response_manifest + + mocker.patch("requests.get", side_effect=mock_get) + + # Call the function + response = get_manifest(image_str) + + # Verify the result + assert response.status_code == 200 + assert response.json() == manifest_content + + +def test_get_manifest_digest(): + """Test that get_manifest_digest correctly calculates the manifest digest.""" + # Create a sample manifest content + manifest_content = b'{"schemaVersion":2,"mediaType":"application/vnd.docker.distribution.manifest.v2+json"}' + + # Calculate the expected digest manually + import hashlib + + expected_digest = hashlib.sha256(manifest_content).hexdigest() + + # Call the function with the content directly + digest = get_manifest_digest("unused_image_str", manifest_content) + + # Verify the result + assert digest == expected_digest + + +def test_get_manifest_digest_from_registry(mocker: MockerFixture): + """Test that get_manifest_digest correctly retrieves and calculates digests from the registry.""" + image_str = "ghcr.io/freedomofpress/dangerzone:v0.4.2" + + # Sample manifest content + manifest_content = b'{"schemaVersion":2,"mediaType":"application/vnd.docker.distribution.manifest.v2+json"}' + expected_digest = hashlib.sha256(manifest_content).hexdigest() + + # Mock get_manifest + mock_response = mocker.Mock() + mock_response.content = manifest_content + mocker.patch("dangerzone.updater.registry.get_manifest", return_value=mock_response) + + # Call the function + digest = get_manifest_digest(image_str) + + # Verify the result + assert digest == expected_digest diff --git a/tests/test_signatures.py b/tests/test_signatures.py index 5f7a846..b744db8 100644 --- a/tests/test_signatures.py +++ b/tests/test_signatures.py @@ -108,7 +108,8 @@ def test_get_log_index_from_missing_log_index(): def test_upgrade_container_image_if_already_up_to_date(mocker): mocker.patch( - "dangerzone.updater.signatures.is_update_available", return_value=(False, None) + "dangerzone.updater.registry.is_new_remote_image_available", + return_value=(False, None), ) with pytest.raises(errors.ImageAlreadyUpToDate): upgrade_container_image( @@ -118,7 +119,7 @@ def test_upgrade_container_image_if_already_up_to_date(mocker): def test_upgrade_container_without_signatures(mocker): mocker.patch( - "dangerzone.updater.signatures.is_update_available", + "dangerzone.updater.registry.is_new_remote_image_available", return_value=(True, "sha256:123456"), ) mocker.patch("dangerzone.updater.signatures.get_remote_signatures", return_value=[]) @@ -139,7 +140,7 @@ def test_upgrade_container_lower_log_index(mocker): signatures_path=VALID_SIGNATURES_PATH, ) mocker.patch( - "dangerzone.updater.signatures.is_update_available", + "dangerzone.updater.registry.is_new_remote_image_available", return_value=( True, image_digest, @@ -208,6 +209,19 @@ def test_get_remote_signatures_cosign_error(mocker, fp: FakeProcess): get_remote_signatures(image, digest) +def test_get_remote_signatures_cosign_error(mocker, fp: FakeProcess): + 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, + stderr="Error: no signatures associated", + ) + with pytest.raises(errors.NoRemoteSignatures): + get_remote_signatures(image, digest) + + def test_store_signatures_with_different_digests( valid_signature, signature_other_digest, mocker, tmp_path ): @@ -239,7 +253,7 @@ def test_store_signatures_with_different_digests( # 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 + # Verify that the log index file was not created assert not (signatures_path / "last_log_index").exists() @@ -309,6 +323,23 @@ def test_stores_signatures_updates_last_log_index(valid_signature, mocker, tmp_p def test_stores_signatures_updates_last_log_index(): pass + # 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 + mocker.patch( + "dangerzone.updater.signatures.get_last_log_index", + return_value=50, + ) + def test_get_file_digest(): # Mock the signatures path @@ -335,31 +366,79 @@ def test_get_file_digest(): assert f.read() == "100" -def test_is_update_available_when_no_local_image(mocker): +def test_is_update_available_when_remote_image_available(mocker): """ - Test that is_update_available returns True when no local image is - currently present. + Test that is_update_available returns True when a new image is available + and all checks pass """ - # Mock container_image_exists to return False + # Mock is_new_remote_image_available to return True and digest mocker.patch( - "dangerzone.container_utils.get_local_image_digest", - side_effect=dzerrors.ImageNotPresentException, + "dangerzone.updater.registry.is_new_remote_image_available", + return_value=(True, RANDOM_DIGEST), ) - # Mock get_manifest_digest to return a digest + # Mock check_signatures_and_logindex to not raise any exceptions mocker.patch( - "dangerzone.updater.registry.get_manifest_digest", - return_value=RANDOM_DIGEST, + "dangerzone.updater.signatures.check_signatures_and_logindex", + return_value=[{"some": "signature"}], ) # Call is_update_available - update_available, digest = is_update_available("ghcr.io/freedomofpress/dangerzone") + update_available, digest = is_update_available( + "ghcr.io/freedomofpress/dangerzone", "test.pub" + ) # Verify the result assert update_available is True assert digest == RANDOM_DIGEST +def test_is_update_available_when_no_remote_image(mocker): + """ + Test that is_update_available returns False when no remote image is available + """ + # Mock is_new_remote_image_available to return False + mocker.patch( + "dangerzone.updater.registry.is_new_remote_image_available", + return_value=(False, None), + ) + + # Call is_update_available + update_available, digest = is_update_available( + "ghcr.io/freedomofpress/dangerzone", "test.pub" + ) + + # Verify the result + assert update_available is False + assert digest is None + + +def test_is_update_available_with_invalid_log_index(mocker): + """ + Test that is_update_available returns False when the log index is invalid + """ + # Mock is_new_remote_image_available to return True + mocker.patch( + "dangerzone.updater.registry.is_new_remote_image_available", + return_value=(True, RANDOM_DIGEST), + ) + + # Mock check_signatures_and_logindex to raise InvalidLogIndex + mocker.patch( + "dangerzone.updater.signatures.check_signatures_and_logindex", + side_effect=errors.InvalidLogIndex("Invalid log index"), + ) + + # Call is_update_available + update_available, digest = is_update_available( + "ghcr.io/freedomofpress/dangerzone", "test.pub" + ) + + # Verify the result + assert update_available is False + assert digest is None + + def test_verify_signature(valid_signature): """Test that verify_signature raises an error when the payload digest doesn't match.""" verify_signature( @@ -383,3 +462,7 @@ def test_verify_signature_tempered(tempered_signature): def test_verify_signatures_empty_list(): with pytest.raises(errors.SignatureVerificationError): verify_signatures([], "1234", TEST_PUBKEY_PATH) + + +def test_verify_signatures_not_0(): + pass