mirror of
https://github.com/freedomofpress/dangerzone.git
synced 2025-04-28 18:02:38 +02:00
Add signatures tests
This commit is contained in:
parent
6359e488e3
commit
238ea527e6
20 changed files with 539 additions and 60 deletions
|
@ -261,7 +261,12 @@ def get_local_image_digest(image: str) -> str:
|
||||||
raise errors.MultipleImagesFoundException(
|
raise errors.MultipleImagesFoundException(
|
||||||
f"Expected a single line of output, got {len(lines)} lines"
|
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:
|
except subprocess.CalledProcessError as e:
|
||||||
raise errors.ImageNotPresentException(
|
raise errors.ImageNotPresentException(
|
||||||
f"The image {image} does not exist locally"
|
f"The image {image} does not exist locally"
|
||||||
|
|
|
@ -96,8 +96,8 @@ class Container(IsolationProvider):
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def install() -> bool:
|
def install() -> bool:
|
||||||
"""Check if an update is available and install it if necessary."""
|
"""Check if an update is available and install it if necessary."""
|
||||||
# XXX Do this only if users have optted in to auto-updates
|
# 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.
|
# # Load the image tarball into the container runtime.
|
||||||
update_available, image_digest = updater.is_update_available(
|
update_available, image_digest = updater.is_update_available(
|
||||||
container_utils.CONTAINER_NAME
|
container_utils.CONTAINER_NAME
|
||||||
|
@ -111,8 +111,6 @@ class Container(IsolationProvider):
|
||||||
for tag in old_tags:
|
for tag in old_tags:
|
||||||
tag = container_utils.CONTAINER_NAME + ":" + tag
|
tag = container_utils.CONTAINER_NAME + ":" + tag
|
||||||
container_utils.delete_image_tag(tag)
|
container_utils.delete_image_tag(tag)
|
||||||
else:
|
|
||||||
return True
|
|
||||||
|
|
||||||
updater.verify_local_image(
|
updater.verify_local_image(
|
||||||
container_utils.CONTAINER_NAME, updater.DEFAULT_PUBKEY_LOCATION
|
container_utils.CONTAINER_NAME, updater.DEFAULT_PUBKEY_LOCATION
|
||||||
|
@ -207,7 +205,6 @@ class Container(IsolationProvider):
|
||||||
updater.verify_local_image(
|
updater.verify_local_image(
|
||||||
container_utils.CONTAINER_NAME,
|
container_utils.CONTAINER_NAME,
|
||||||
updater.DEFAULT_PUBKEY_LOCATION,
|
updater.DEFAULT_PUBKEY_LOCATION,
|
||||||
image_digest,
|
|
||||||
)
|
)
|
||||||
security_args = self.get_runtime_security_args()
|
security_args = self.get_runtime_security_args()
|
||||||
debug_args = []
|
debug_args = []
|
||||||
|
|
|
@ -37,7 +37,7 @@ LAST_LOG_INDEX = SIGNATURES_PATH / "last_log_index"
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"verify_signature",
|
"verify_signature",
|
||||||
"load_signatures",
|
"load_and_verify_signatures",
|
||||||
"store_signatures",
|
"store_signatures",
|
||||||
"verify_offline_image_signature",
|
"verify_offline_image_signature",
|
||||||
]
|
]
|
||||||
|
@ -77,11 +77,15 @@ def verify_signature(signature: dict, image_digest: str, pubkey: str | Path) ->
|
||||||
|
|
||||||
cosign.ensure_installed()
|
cosign.ensure_installed()
|
||||||
signature_bundle = signature_to_bundle(signature)
|
signature_bundle = signature_to_bundle(signature)
|
||||||
|
try:
|
||||||
payload_bytes = b64decode(signature_bundle["Payload"])
|
payload_bytes = b64decode(signature_bundle["Payload"])
|
||||||
payload_digest = json.loads(payload_bytes)["critical"]["image"][
|
payload_digest = json.loads(payload_bytes)["critical"]["image"][
|
||||||
"docker-manifest-digest"
|
"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}":
|
if payload_digest != f"sha256:{image_digest}":
|
||||||
raise errors.SignatureMismatch(
|
raise errors.SignatureMismatch(
|
||||||
"The given signature does not match the expected image digest "
|
"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.write(payload_bytes)
|
||||||
payload_file.flush()
|
payload_file.flush()
|
||||||
|
|
||||||
|
if isinstance(pubkey, str):
|
||||||
|
pubkey = Path(pubkey)
|
||||||
|
|
||||||
cmd = [
|
cmd = [
|
||||||
"cosign",
|
"cosign",
|
||||||
"verify-blob",
|
"verify-blob",
|
||||||
"--key",
|
"--key",
|
||||||
pubkey,
|
str(pubkey.absolute()),
|
||||||
"--bundle",
|
"--bundle",
|
||||||
signature_file.name,
|
signature_file.name,
|
||||||
payload_file.name,
|
payload_file.name,
|
||||||
|
@ -149,8 +156,12 @@ def verify_signatures(
|
||||||
image_digest: str,
|
image_digest: str,
|
||||||
pubkey: str,
|
pubkey: str,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
|
if len(signatures) < 1:
|
||||||
|
raise errors.SignatureVerificationError("No signatures found")
|
||||||
|
|
||||||
for signature in signatures:
|
for signature in signatures:
|
||||||
verify_signature(signature, image_digest, pubkey)
|
verify_signature(signature, image_digest, pubkey)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@ -164,9 +175,14 @@ def get_last_log_index() -> int:
|
||||||
|
|
||||||
|
|
||||||
def get_log_index_from_signatures(signatures: List[Dict]) -> int:
|
def get_log_index_from_signatures(signatures: List[Dict]) -> int:
|
||||||
return reduce(
|
def _reducer(accumulator: int, signature: Dict) -> int:
|
||||||
lambda acc, sig: max(acc, sig["Bundle"]["Payload"]["logIndex"]), signatures, 0
|
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:
|
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 ""
|
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
|
Load signatures from the local filesystem
|
||||||
|
|
||||||
See store_signatures() for the expected format.
|
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():
|
if not pubkey_signatures.exists():
|
||||||
msg = (
|
msg = (
|
||||||
f"Cannot find a '{pubkey_signatures}' folder."
|
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:
|
with open(pubkey_signatures / f"{image_digest}.json") as f:
|
||||||
log.debug("Loading signatures from %s", f.name)
|
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:
|
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")
|
raise errors.ImageNotFound(f"The image {image} does not exist locally")
|
||||||
|
|
||||||
log.debug(f"Image digest: {image_digest}")
|
log.debug(f"Image digest: {image_digest}")
|
||||||
signatures = load_signatures(image_digest, pubkey)
|
load_and_verify_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)
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def get_remote_signatures(image: str, digest: str) -> List[Dict]:
|
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()
|
cosign.ensure_installed()
|
||||||
|
|
||||||
# XXX: try/catch here
|
try:
|
||||||
process = subprocess.run(
|
process = subprocess.run(
|
||||||
["cosign", "download", "signature", f"{image}@sha256:{digest}"],
|
["cosign", "download", "signature", f"{image}@sha256:{digest}"],
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
check=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
|
# Remove the last return, split on newlines, convert from JSON
|
||||||
signatures_raw = process.stdout.decode("utf-8").strip().split("\n")
|
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:
|
if len(signatures) < 1:
|
||||||
raise errors.NoRemoteSignatures("No signatures found for the image")
|
raise errors.NoRemoteSignatures("No signatures found for the image")
|
||||||
return signatures
|
return signatures
|
||||||
|
@ -418,8 +441,8 @@ def prepare_airgapped_archive(image_name: str, destination: str) -> None:
|
||||||
)
|
)
|
||||||
|
|
||||||
cosign.ensure_installed()
|
cosign.ensure_installed()
|
||||||
# Get the image from the registry
|
|
||||||
|
|
||||||
|
# Get the image from the registry
|
||||||
with TemporaryDirectory() as tmpdir:
|
with TemporaryDirectory() as tmpdir:
|
||||||
msg = f"Downloading image {image_name}. \nIt might take a while."
|
msg = f"Downloading image {image_name}. \nIt might take a while."
|
||||||
log.info(msg)
|
log.info(msg)
|
||||||
|
|
7
tests/assets/signatures/README.md
Normal file
7
tests/assets/signatures/README.md
Normal file
|
@ -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.
|
|
@ -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
|
||||||
|
}
|
||||||
|
]
|
|
@ -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
|
||||||
|
}
|
||||||
|
]
|
|
@ -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
|
||||||
|
}
|
||||||
|
]
|
|
@ -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
|
||||||
|
}
|
||||||
|
]
|
|
@ -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
|
||||||
|
}
|
||||||
|
]
|
|
@ -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
|
||||||
|
}
|
||||||
|
]
|
|
@ -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.
|
|
@ -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}]
|
|
@ -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}]
|
|
@ -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}]
|
|
@ -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}]
|
|
@ -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}]
|
4
tests/assets/test.pub.key
Normal file
4
tests/assets/test.pub.key
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
-----BEGIN PUBLIC KEY-----
|
||||||
|
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEoE0CXLGff79fR8KyPnSvOY74UBkt
|
||||||
|
2sLi+aVFUzS1Qwt4wosxHhcDN2B6QSsLlvgsH82q6qcA6PL2SdS/p4jWGA==
|
||||||
|
-----END PUBLIC KEY-----
|
|
@ -13,6 +13,13 @@ from dangerzone.gui import Application
|
||||||
sys.dangerzone_dev = True # type: ignore[attr-defined]
|
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.
|
# Use this fixture to make `pytest-qt` invoke our custom QApplication.
|
||||||
# See https://pytest-qt.readthedocs.io/en/latest/qapplication.html#testing-custom-qapplications
|
# See https://pytest-qt.readthedocs.io/en/latest/qapplication.html#testing-custom-qapplications
|
||||||
@pytest.fixture(scope="session")
|
@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]
|
"doc", test_docs, ids=[str(doc.name) for doc in test_docs]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# External Docs - base64 docs encoded for externally sourced documents
|
# External Docs - base64 docs encoded for externally sourced documents
|
||||||
# XXX to reduce the chance of accidentally opening them
|
# XXX to reduce the chance of accidentally opening them
|
||||||
test_docs_external_dir = Path(__file__).parent.joinpath(SAMPLE_EXTERNAL_DIRECTORY)
|
test_docs_external_dir = Path(__file__).parent.joinpath(SAMPLE_EXTERNAL_DIRECTORY)
|
||||||
|
|
238
tests/test_registry.py
Normal file
238
tests/test_registry.py
Normal file
|
@ -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
|
|
@ -108,7 +108,8 @@ def test_get_log_index_from_missing_log_index():
|
||||||
|
|
||||||
def test_upgrade_container_image_if_already_up_to_date(mocker):
|
def test_upgrade_container_image_if_already_up_to_date(mocker):
|
||||||
mocker.patch(
|
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):
|
with pytest.raises(errors.ImageAlreadyUpToDate):
|
||||||
upgrade_container_image(
|
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):
|
def test_upgrade_container_without_signatures(mocker):
|
||||||
mocker.patch(
|
mocker.patch(
|
||||||
"dangerzone.updater.signatures.is_update_available",
|
"dangerzone.updater.registry.is_new_remote_image_available",
|
||||||
return_value=(True, "sha256:123456"),
|
return_value=(True, "sha256:123456"),
|
||||||
)
|
)
|
||||||
mocker.patch("dangerzone.updater.signatures.get_remote_signatures", return_value=[])
|
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,
|
signatures_path=VALID_SIGNATURES_PATH,
|
||||||
)
|
)
|
||||||
mocker.patch(
|
mocker.patch(
|
||||||
"dangerzone.updater.signatures.is_update_available",
|
"dangerzone.updater.registry.is_new_remote_image_available",
|
||||||
return_value=(
|
return_value=(
|
||||||
True,
|
True,
|
||||||
image_digest,
|
image_digest,
|
||||||
|
@ -208,6 +209,19 @@ def test_get_remote_signatures_cosign_error(mocker, fp: FakeProcess):
|
||||||
get_remote_signatures(image, digest)
|
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(
|
def test_store_signatures_with_different_digests(
|
||||||
valid_signature, signature_other_digest, mocker, tmp_path
|
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
|
# Verify that the signatures file was not created
|
||||||
assert not (signatures_path / f"{image_digest}.json").exists()
|
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()
|
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():
|
def test_stores_signatures_updates_last_log_index():
|
||||||
pass
|
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():
|
def test_get_file_digest():
|
||||||
# Mock the signatures path
|
# Mock the signatures path
|
||||||
|
@ -335,31 +366,79 @@ def test_get_file_digest():
|
||||||
assert f.read() == "100"
|
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
|
Test that is_update_available returns True when a new image is available
|
||||||
currently present.
|
and all checks pass
|
||||||
"""
|
"""
|
||||||
# Mock container_image_exists to return False
|
# Mock is_new_remote_image_available to return True and digest
|
||||||
mocker.patch(
|
mocker.patch(
|
||||||
"dangerzone.container_utils.get_local_image_digest",
|
"dangerzone.updater.registry.is_new_remote_image_available",
|
||||||
side_effect=dzerrors.ImageNotPresentException,
|
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(
|
mocker.patch(
|
||||||
"dangerzone.updater.registry.get_manifest_digest",
|
"dangerzone.updater.signatures.check_signatures_and_logindex",
|
||||||
return_value=RANDOM_DIGEST,
|
return_value=[{"some": "signature"}],
|
||||||
)
|
)
|
||||||
|
|
||||||
# Call is_update_available
|
# 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
|
# Verify the result
|
||||||
assert update_available is True
|
assert update_available is True
|
||||||
assert digest == RANDOM_DIGEST
|
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):
|
def test_verify_signature(valid_signature):
|
||||||
"""Test that verify_signature raises an error when the payload digest doesn't match."""
|
"""Test that verify_signature raises an error when the payload digest doesn't match."""
|
||||||
verify_signature(
|
verify_signature(
|
||||||
|
@ -383,3 +462,7 @@ def test_verify_signature_tempered(tempered_signature):
|
||||||
def test_verify_signatures_empty_list():
|
def test_verify_signatures_empty_list():
|
||||||
with pytest.raises(errors.SignatureVerificationError):
|
with pytest.raises(errors.SignatureVerificationError):
|
||||||
verify_signatures([], "1234", TEST_PUBKEY_PATH)
|
verify_signatures([], "1234", TEST_PUBKEY_PATH)
|
||||||
|
|
||||||
|
|
||||||
|
def test_verify_signatures_not_0():
|
||||||
|
pass
|
||||||
|
|
Loading…
Reference in a new issue