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
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)
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
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
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:")
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"}}}
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
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)
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
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)
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
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, )