def test_get_config_canonical(
    image_config: ImageConfig,
    image_config_signed: ImageConfig,
    json_bytes_canonical: bytes,
    json_bytes_signed_canonical: bytes,
):
    """Test the canonical form for signed and unsigned configurations."""
    assert image_config.get_config_canonical() == json_bytes_canonical
    assert image_config_signed.get_config_canonical(
    ) == json_bytes_signed_canonical
Beispiel #2
0
def test_get_config_digest_canonical(
    image_config: ImageConfig,
    image_config_signed: ImageConfig,
    config_digest_canonical: str,
):
    """Test canonical digest calculation for signed and unsigned configurations."""
    assert image_config.get_config_digest_canonical(
    ) == config_digest_canonical
    assert image_config_signed.get_config_digest_canonical(
    ) == config_digest_canonical
 def append_new_image_config(config: ImageConfig,
                             endorse: bool = False,
                             iteration=i):
     signer = FakeSigner("<<< {0} {1}: {2} >>>".format(
         iteration,
         "Endorsing" if endorse else "Signing",
         config.get_config_digest_canonical(),
     ))
     config.sign(signer, endorse)
     stack.append(config)
 def append_new_image_config(
     config: ImageConfig,
     signature_type: SignatureTypes = SignatureTypes.SIGN,
     iteration=i,
 ):
     signer = FakeSigner("<<< {0} {1}: {2} >>>".format(
         iteration,
         "Endorsing"
         if signature_type == SignatureTypes.ENDORSE else "Signing",
         config.get_config_digest_canonical(),
     ))
     config.sign(signer, signature_type)
     stack.append(config)
Beispiel #5
0
def test_sign(image_config: ImageConfig, image_config_signed: ImageConfig):
    """Test configuration signing for signed and unsigned configurations."""

    signer = FakeSigner()
    assert image_config.sign(signer) == signer.signature_value
    assert image_config_signed.sign(signer) == signer.signature_value

    # Previously unsigned configurations should now contain the new signature.
    assert b"BEGIN FAKE SIGNATURE" in image_config.get_config()

    # Previously signed configurations should now contain the original signature(s) and the new signature.
    assert b"BEGIN FAKE SIGNATURE" in image_config_signed.get_config()
    assert b"BEGIN PGP SIGNATURE" in image_config_signed.get_config()
def test__get_labels():
    """Test that labels are able to be retrieved."""
    # Uppercase 'C'
    assert ImageConfig._get_labels(json.loads('{"Config":{"Labels":{"x":"5"}}}')) == {
        "x": "5"
    }

    # Lowercase 'C'
    assert ImageConfig._get_labels(json.loads('{"config":{"Labels":{"x":"5"}}}')) == {
        "x": "5"
    }

    # Missing 'Labels'
    assert ImageConfig._get_labels(json.loads('{"Config":{}}')) == {}
def test_get_signature_list(
    image_config: ImageConfig,
    image_config_signed: ImageConfig,
    config_digest_canonical: str,
    signature: str,
):
    """Test signature data parsing for signed and unsigned configurations."""
    signatures_signed = image_config_signed.get_signature_list()
    assert len(signatures_signed) == 1
    assert signatures_signed[0]["digest"] == config_digest_canonical
    assert signatures_signed[0]["signature"] == signature

    signatures_unsigned = image_config.get_signature_list()
    assert not signatures_unsigned
async def test_minimal():
    """Test minimal image configuration (for non-conformant labels)k."""

    # Note: At a minimum, [Cc]onfig key must exist with non-null value
    image_config = ImageConfig(b'{"Config":{}}')
    config_digest_canonical = image_config.get_digest_canonical()
    signer = FakeSigner()
    assert await image_config.sign(signer) == signer.signature_value

    # A signature should always be able to be added ...
    assert b"BEGIN FAKE SIGNATURE" in image_config.get_bytes()
    signatures = image_config.get_signature_list()
    assert len(signatures) == 1
    assert signatures[0]["digest"] == config_digest_canonical
    assert signatures[0]["signature"] == signer.signature_value
Beispiel #9
0
def test_get_signature_data(
    image_config: ImageConfig,
    image_config_signed: ImageConfig,
    config_digest: str,
    signature: str,
):
    """Test signature data parsing for signed and unsigned configurations."""
    signature_data_signed = image_config_signed.get_signature_data()
    assert signature_data_signed["original_config"] == config_digest
    assert signature_data_signed["signatures"] == signature
    assert signature_data_signed["signature_list"] == [signature]

    signature_data_unsigned = image_config.get_signature_data()
    assert signature_data_unsigned["original_config"] is None
    assert signature_data_unsigned["signatures"] == ""
    assert signature_data_unsigned["signature_list"] == []
def test_sign_resign(
    image_config: ImageConfig,
    image_config_signed: ImageConfig,
    config_digest_canonical: str,
    config_digest_signed_canonical: str,
    signature: str,
):
    """Test configuration resigning for signed and unsigned configurations."""

    signer = FakeSigner()
    assert image_config.sign(signer,
                             SignatureTypes.RESIGN) == signer.signature_value
    assert (image_config_signed.sign(
        signer, SignatureTypes.RESIGN) == signer.signature_value)

    # Previously unsigned configurations should now contain the new signature.
    assert b"BEGIN FAKE SIGNATURE" in image_config.get_config()
    signatures = image_config.get_signature_list()
    assert len(signatures) == 1
    assert signatures[0]["digest"] == config_digest_canonical
    assert signatures[0]["signature"] == signer.signature_value

    # Previously signed configurations should now contain (only) the new signature.
    assert b"BEGIN FAKE SIGNATURE" in image_config_signed.get_config()
    assert b"BEGIN PGP SIGNATURE" not in image_config_signed.get_config()
    signatures_signed = image_config_signed.get_signature_list()
    assert len(signatures_signed) == 1
    assert signatures[0]["digest"] == config_digest_canonical
    assert signatures[0]["signature"] == signer.signature_value
def test_sign_endorse(
    image_config: ImageConfig,
    image_config_signed: ImageConfig,
    config_digest_canonical: str,
    config_digest_signed_canonical: str,
    signature: str,
):
    """Test configuration endorsement for signed and unsigned configurations."""

    signer = FakeSigner()
    assert image_config.sign(signer,
                             SignatureTypes.ENDORSE) == signer.signature_value
    assert (image_config_signed.sign(
        signer, SignatureTypes.ENDORSE) == signer.signature_value)

    # Previously unsigned configurations should now contain the new signature.
    assert b"BEGIN FAKE SIGNATURE" in image_config.get_config()
    signatures = image_config.get_signature_list()
    assert len(signatures) == 1
    assert signatures[0]["digest"] == config_digest_canonical
    assert signatures[0]["signature"] == signer.signature_value

    # Previously signed configurations should now contain the original signature(s) and the new signature.
    assert b"BEGIN FAKE SIGNATURE" in image_config_signed.get_config()
    assert b"BEGIN PGP SIGNATURE" in image_config_signed.get_config()
    signatures_signed = image_config_signed.get_signature_list()
    assert len(signatures_signed) == 2
    assert signatures_signed[0]["digest"] == config_digest_canonical
    assert signatures_signed[0]["signature"] == signature
    assert signatures_signed[1]["digest"] == config_digest_signed_canonical
    assert signatures_signed[1]["signature"] == signer.signature_value
Beispiel #12
0
async def test_verify_signatures_manipulated_signatures(
    gnupg_keypair: GnuPGKeypair, image_config: ImageConfig
):
    """Test that signature verification detects manipulated signatures."""

    signer = GPGSigner(
        keyid=gnupg_keypair.keyid,
        passphrase=gnupg_keypair.passphrase,
        homedir=gnupg_keypair.gnupg_home,
    )

    # Add a single signature ...
    await image_config.sign(signer)
    response = await image_config.verify_signatures(
        signer_kwargs={GPGSigner.__name__: {"homedir": gnupg_keypair.gnupg_home}}
    )
    assert response.results[0].valid

    # Modify the digest value of the (first) signature ...
    signatures = image_config.get_signature_list()
    temp = deepcopy(signatures)
    temp[0] = ImageConfigSignatureEntry(
        digest=FormattedSHA256.calculate(b"tampertampertamper"),
        signature=temp[0].signature,
    )
    image_config.set_signature_list(temp)

    # An exception should be raised if digest value from the signature does not match the canonical digest of the image
    # configuration (without any signatures).
    with pytest.raises(DigestMismatchError) as exception:
        await image_config.verify_signatures()
    assert str(exception.value).startswith("Image config canonical digest mismatch:")

    # Restore the unmodified signature and endorse ...
    image_config.set_signature_list(signatures)
    await image_config.sign(signer, SignatureTypes.ENDORSE)
    response = await image_config.verify_signatures(
        signer_kwargs={GPGSigner.__name__: {"homedir": gnupg_keypair.gnupg_home}}
    )
    assert response.results[0].valid

    # Modify the digest value of the second signature ...
    signatures = image_config.get_signature_list()
    temp = deepcopy(signatures)
    temp[1] = ImageConfigSignatureEntry(
        digest=FormattedSHA256.calculate(b"tampertampertamper"),
        signature=temp[1].signature,
    )
    image_config.set_signature_list(temp)

    # An exception should be raised if digest value from the signature does not match the canonical digest of the image
    # configuration (including the first signature).
    with pytest.raises(DigestMismatchError) as exception:
        await image_config.verify_signatures()
    assert str(exception.value).startswith("Image config canonical digest mismatch:")
Beispiel #13
0
def test_verify_signatures(image_config: ImageConfig):
    """Test signature verification for signed and unsigned configurations."""

    # Unsigned configurations should explicitly raise an exception.
    with pytest.raises(Exception) as e:
        image_config.verify_signatures()
    assert str(e.value) == "Image does not contain any signatures!"

    # Sign a previously unsigned configuration, so that only the new signature type is present.
    # Note: It is not trivial to embed "known" GPG / PKI signature types, as assumptions about the
    #       test environment are difficult to make.
    image_config.sign(FakeSigner())

    # An exception should be raised if the provider for a signature type is not known
    with pytest.raises(Exception) as e:
        image_config.verify_signatures()
    assert str(e.value) == "Unsupported signature type!"

    # Replace the class method for resolving signature providers ...
    original_method = Signer.for_signature
    Signer.for_signature = _signer_for_signature

    # The Signer's verify() method should be invoked.
    assert image_config.verify_signatures()["results"] == [{
        "type": "fake",
        "valid": True
    }]

    # Restore the original class method
    Signer.for_signature = original_method
async def test_verify_signatures_manipulated_signatures(image_config: ImageConfig):
    """Test that signature verification detects manipulated signatures."""

    # Add a single signature ...
    signer = FakeSigner()
    assert await image_config.sign(signer) == signer.signature_value

    # Replace the class method for resolving signature providers ...
    original_method = Signer.for_signature
    Signer.for_signature = _signer_for_signature

    # Sanity check
    response = await image_config.verify_signatures()
    assert response["results"][0]["valid"] is True

    # Modify the digest value of the (first) signature ...
    signatures = image_config.get_signature_list()
    temp = deepcopy(signatures)
    temp[0]["digest"] = "tampertampertamper"
    image_config.set_signature_list(temp)

    # An exception should be raised if digest value from the signature does not match the canonical digest of the image
    # configuration (without any signatures).
    with pytest.raises(DigestMismatchError) as exception:
        await image_config.verify_signatures()
    assert str(exception.value).startswith("Image config canonical digest mismatch:")

    # Restore the unmodified signature and endorse ...
    image_config.set_signature_list(signatures)
    assert (
        await image_config.sign(signer, SignatureTypes.ENDORSE)
        == signer.signature_value
    )

    # Sanity check
    response = await image_config.verify_signatures()
    assert response["results"][0]["valid"] is True

    # Modify the digest value of the second signature ...
    signatures = image_config.get_signature_list()
    temp = deepcopy(signatures)
    temp[1]["digest"] = "tampertampertamper"
    image_config.set_signature_list(temp)

    # An exception should be raised if digest value from the signature does not match the canonical digest of the image
    # configuration (including the first signature).
    with pytest.raises(DigestMismatchError) as exception:
        await image_config.verify_signatures()
    assert str(exception.value).startswith("Image config canonical digest mismatch:")

    # Restore the original class method
    Signer.for_signature = original_method
def test__normalize():
    """Test that signed and unsigned configuration can be normalized."""
    # Missing 'Labels'
    assert ImageConfig._normalize(json.loads('{"Config":{}}')) == {
        "Config": {"Labels": {"signatures": "[]"}}
    }

    # Missing 'signatures'
    assert ImageConfig._normalize(json.loads('{"Config":{"Labels":{"x":"5"}}}')) == {
        "Config": {"Labels": {"signatures": "[]", "x": "5"}}
    }

    # Empty 'signatures'
    assert ImageConfig._normalize(
        json.loads('{"Config":{"Labels":{"signatures":"[]","x":"5"}}}')
    ) == {"Config": {"Labels": {"signatures": "[]", "x": "5"}}}

    # Existing 'signatures'
    assert ImageConfig._normalize(
        json.loads('{"Config":{"Labels":{"signatures":"[{\\"y\\":\\"4\\"}]","x":"5"}}}')
    ) == {"Config": {"Labels": {"signatures": '[{"y":"4"}]', "x": "5"}}}
Beispiel #16
0
async def test_minimal(gnupg_keypair: GnuPGKeypair):
    """Test minimal image configuration (for non-conformant labels)k."""

    signer = GPGSigner(
        keyid=gnupg_keypair.keyid,
        passphrase=gnupg_keypair.passphrase,
        homedir=gnupg_keypair.gnupg_home,
    )

    # Note: At a minimum, [Cc]onfig key must exist with non-null value
    image_config = ImageConfig(b'{"Config":{}}')
    config_digest_canonical = image_config.get_digest_canonical()
    signature = await image_config.sign(signer)
    assert "PGP SIGNATURE" in signature

    # A signature should always be able to be added ...
    assert b"BEGIN PGP SIGNATURE" in image_config.get_bytes()
    signatures = image_config.get_signature_list()
    assert len(signatures) == 1
    assert signatures[0].digest == config_digest_canonical
    assert signatures[0].signature == signature
Beispiel #17
0
def test_unsign(image_config: ImageConfig, image_config_signed: ImageConfig):
    """Test configuration unsigning for signed and unsigned configurations."""

    image_config.unsign()
    image_config_signed.unsign()

    # Previously unsigned configurations should still contain no signature.
    assert b"BEGIN FAKE SIGNATURE" not in image_config.get_config()

    # Previously signed configurations should now contain no signature(s).
    assert b"BEGIN FAKE SIGNATURE" not in image_config_signed.get_config()
    assert b"BEGIN PGP SIGNATURE" not in image_config_signed.get_config()
def test_clear_signature_list(
    image_config: ImageConfig, image_config_signed: ImageConfig
):
    """Test signature data parsing for signed and unsigned configurations."""
    image_config_signed.clear_signature_list()
    assert not image_config_signed.get_signature_list()

    image_config.clear_signature_list()
    signatures_unsigned = image_config.get_signature_list()
    assert not signatures_unsigned
def test_sign_endorse_recursive(image_config: ImageConfig):
    """Test interlaced signatures and endorsements."""

    # Stack representation of a binary tree
    stack = [copy.deepcopy(image_config)]
    iterations = 6
    # Breadth first traversal ...
    for i in range(iterations):
        for _ in range(len(stack)):
            # Validate the signature / endorsement permutations of the first entry on the stack ...
            signatures = stack[0].get_signature_list()
            assert len(signatures) == i
            for sig, signature in enumerate(signatures):
                if "Signing" in signature["signature"] or sig == 0:
                    # Signature digests should be independent of the number of signatures
                    assert (signature["digest"] ==
                            image_config.get_config_digest_canonical())
                else:
                    # Endorsement digests should include all entities of a lower order
                    temp = copy.deepcopy(stack[0])
                    temp.set_signature_list(temp.get_signature_list()[:sig])
                    assert signature[
                        "digest"] == temp.get_config_digest_canonical()

            def append_new_image_config(
                config: ImageConfig,
                signature_type: SignatureTypes = SignatureTypes.SIGN,
                iteration=i,
            ):
                signer = FakeSigner("<<< {0} {1}: {2} >>>".format(
                    iteration,
                    "Endorsing"
                    if signature_type == SignatureTypes.ENDORSE else "Signing",
                    config.get_config_digest_canonical(),
                ))
                config.sign(signer, signature_type)
                stack.append(config)

            # TODO: Add optimization to stop appending to the stack if they will never be validated

            # Push two more image configurations on to the stack: one signed, one endorsed ...
            append_new_image_config(copy.deepcopy(stack[0]))
            append_new_image_config(stack.pop(0), SignatureTypes.ENDORSE)
Beispiel #20
0
def test_get_image_layers(image_config: ImageConfig,
                          image_config_signed: ImageConfig,
                          image_layers: list):
    """Test image layer preservation for signed and unsigned configurations."""
    assert image_config.get_image_layers() == image_layers
    assert image_config_signed.get_image_layers() == image_layers
Beispiel #21
0
 def get_image_config(self, image_name: ImageName) -> ImageConfig:
     if not self.config:
         config = get_test_data(self.request, __name__, "stub_config.json")
         self.config = ImageConfig(config)
     return self.config
def image_config(json_bytes: bytes) -> ImageConfig:
    """Provides an ImageConfig instance for the sample image configuration."""
    # Do not use caching; get a new instance for each test
    return ImageConfig(json_bytes)
Beispiel #23
0
def test_acceptance_sign_unsign_symmetry(image_config: ImageConfig,
                                         image_config_signed: ImageConfig):
    """Tests that sign and unsign are (mostly) symmetric operations."""

    config_digest = image_config.get_config_digest()

    # 1. Sign
    signer = FakeSigner()
    assert image_config.sign(signer) == signer.signature_value
    assert image_config_signed.sign(signer) == signer.signature_value

    # Previously unsigned configurations should now contain the new signature.
    assert b"BEGIN FAKE SIGNATURE" in image_config.get_config()

    # Previously signed configurations should now contain the original signature(s) and the new signature.
    assert b"BEGIN FAKE SIGNATURE" in image_config_signed.get_config()
    assert b"BEGIN PGP SIGNATURE" in image_config_signed.get_config()

    # 2. Unsign
    image_config.unsign()
    image_config_signed.unsign()

    # Configurations where we added the first signature should be reverted.
    assert b"BEGIN FAKE SIGNATURE" not in image_config.get_config()

    # Configurations where we appended a signature should now contain no signature(s).
    assert b"BEGIN FAKE SIGNATURE" not in image_config_signed.get_config()
    assert b"BEGIN PGP SIGNATURE" not in image_config_signed.get_config()

    assert image_config.get_config_digest() == config_digest
Beispiel #24
0
def image_config_signed(json_bytes_signed):
    # Do not use caching; get a new instance for each test
    return ImageConfig(json_bytes_signed)
async def test_sign_endorse_recursive(image_config: ImageConfig):
    """Test interlaced signatures and endorsements."""

    # Stack representation of a binary tree
    stack = [{"name": "?-Unsigned", "image_config": deepcopy(image_config)}]
    LOGGER.debug("Unsigned Canonical Digest: %s", image_config.get_digest_canonical())

    async def append_new_image_config(
        *,
        config: ImageConfig,
        signature_type: SignatureTypes = SignatureTypes.SIGN,
        iteration,
    ):
        action = f"X{signature_type.name}"
        signer = FakeSigner(f"[{iteration}-{action: <8}: {{0}}]")
        await config.sign(signer, signature_type)
        stack.append({"name": f"{iteration}-{action}", "image_config": config})

    iterations = 6
    # Breadth first traversal ...
    for i in range(iterations):
        LOGGER.debug("Iteration %d", i)
        for _ in range(len(stack)):
            frame = stack[0]
            LOGGER.debug("  Checking %s", frame["name"])
            # Validate the signature / endorsement permutations of the first entry on the stack ...
            signatures = frame["image_config"].get_signature_list()

            flat_list = "".join([signature["signature"] for signature in signatures])
            if f"X{SignatureTypes.RESIGN.name}" in flat_list:
                # Too lazy to calculate how many signatures were removed ...
                assert len(signatures) <= i
            else:
                assert len(signatures) == i

            for sig, signature in enumerate(signatures):
                LOGGER.debug("    %s", signature["signature"])
                if f"X{SignatureTypes.ENDORSE.name}" in signature["signature"]:
                    # Endorsement digests should include all entities of a lower order.
                    temp = deepcopy(frame["image_config"])
                    temp.set_signature_list(temp.get_signature_list()[:sig])
                    assert signature["digest"] == temp.get_digest_canonical()
                    assert temp.get_digest_canonical() in signature["signature"]
                else:
                    # Signature digests should be independent of the number of signatures.
                    # Re-signed images should always contain 1 signature.
                    assert signature["digest"] == image_config.get_digest_canonical()
                    assert image_config.get_digest_canonical() in signature["signature"]

            # Unshift the first image configuration, append three more image configurations on to the stack: ...
            # ... one signed ...
            await append_new_image_config(
                config=deepcopy(frame["image_config"]), iteration=i
            )
            # ... one endorsed ...
            await append_new_image_config(
                config=deepcopy(frame["image_config"]),
                signature_type=SignatureTypes.ENDORSE,
                iteration=i,
            )
            # ... one resigned ...
            await append_new_image_config(
                config=stack.pop(0).get("image_config"),
                signature_type=SignatureTypes.RESIGN,
                iteration=i,
            )