148 lines
5.6 KiB
Python
148 lines
5.6 KiB
Python
#!/usr/bin/env python3
|
|
import base64
|
|
import subprocess
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
from domain_manifest_signing import (
|
|
SignerConfigurationError,
|
|
canonical_manifest_payload,
|
|
load_signer_config_from_environment,
|
|
sign_domain_manifest,
|
|
)
|
|
|
|
|
|
class DomainManifestSigningTests(unittest.TestCase):
|
|
def setUp(self):
|
|
self.temp = tempfile.TemporaryDirectory()
|
|
self.root = Path(self.temp.name)
|
|
self.private_key = self.root / "signer.pem"
|
|
self.public_key = self.root / "signer-public.pem"
|
|
subprocess.run(
|
|
["openssl", "genpkey", "-algorithm", "ED25519", "-out", self.private_key],
|
|
check=True,
|
|
capture_output=True,
|
|
)
|
|
self.private_key.chmod(0o600)
|
|
subprocess.run(
|
|
["openssl", "pkey", "-in", self.private_key, "-pubout", "-out", self.public_key],
|
|
check=True,
|
|
capture_output=True,
|
|
)
|
|
|
|
def tearDown(self):
|
|
self.temp.cleanup()
|
|
|
|
def test_canonical_payload_matches_the_hololake_client_contract(self):
|
|
encoded = canonical_manifest_payload(
|
|
domain_id="DOM-FIFTH-0001",
|
|
repository_id="REPO-012",
|
|
signer_id="GH-LIGHTHOUSE-001",
|
|
source_commit="b" * 40,
|
|
)
|
|
self.assertEqual(
|
|
encoded,
|
|
b'{"domainId":"DOM-FIFTH-0001","repositoryId":"REPO-012",'
|
|
b'"schema":"gh-aios.domain-manifest/v1","signerId":"GH-LIGHTHOUSE-001",'
|
|
b'"sourceCommit":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"}',
|
|
)
|
|
|
|
def test_signs_a_digest_bound_ed25519_envelope(self):
|
|
envelope = sign_domain_manifest(
|
|
private_key_path=self.private_key,
|
|
domain_id="DOM-FIFTH-0001",
|
|
repository_id="REPO-012",
|
|
signer_id="GH-LIGHTHOUSE-001",
|
|
source_commit="b" * 40,
|
|
)
|
|
payload = canonical_manifest_payload(
|
|
domain_id=envelope["domainId"],
|
|
repository_id=envelope["repositoryId"],
|
|
signer_id=envelope["signerId"],
|
|
source_commit=envelope["sourceCommit"],
|
|
)
|
|
signature_path = self.root / "signature.bin"
|
|
payload_path = self.root / "payload.json"
|
|
payload_path.write_bytes(payload)
|
|
signature_path.write_bytes(base64.b64decode(envelope["signature"], validate=True))
|
|
verified = subprocess.run(
|
|
[
|
|
"openssl", "pkeyutl", "-verify", "-rawin", "-pubin",
|
|
"-inkey", self.public_key, "-sigfile", signature_path, "-in", payload_path,
|
|
],
|
|
capture_output=True,
|
|
)
|
|
self.assertEqual(verified.returncode, 0, verified.stderr.decode())
|
|
self.assertEqual(len(envelope["digest"]), 64)
|
|
self.assertEqual(set(envelope), {
|
|
"digest", "domainId", "repositoryId", "schema", "signature", "signerId", "sourceCommit",
|
|
})
|
|
|
|
def test_payload_tampering_does_not_verify(self):
|
|
envelope = sign_domain_manifest(
|
|
private_key_path=self.private_key,
|
|
domain_id="DOM-FIFTH-0001",
|
|
repository_id="REPO-012",
|
|
signer_id="GH-LIGHTHOUSE-001",
|
|
source_commit="b" * 40,
|
|
)
|
|
signature_path = self.root / "signature.bin"
|
|
tampered_path = self.root / "tampered.json"
|
|
signature_path.write_bytes(base64.b64decode(envelope["signature"], validate=True))
|
|
tampered = canonical_manifest_payload(
|
|
domain_id="DOM-FIFTH-0001",
|
|
repository_id="REPO-012",
|
|
signer_id="GH-LIGHTHOUSE-001",
|
|
source_commit="c" * 40,
|
|
)
|
|
tampered_path.write_bytes(tampered)
|
|
verified = subprocess.run(
|
|
[
|
|
"openssl", "pkeyutl", "-verify", "-rawin", "-pubin",
|
|
"-inkey", self.public_key, "-sigfile", signature_path, "-in", tampered_path,
|
|
],
|
|
capture_output=True,
|
|
)
|
|
self.assertNotEqual(verified.returncode, 0)
|
|
|
|
def test_signing_is_unavailable_without_complete_environment_configuration(self):
|
|
self.assertIsNone(load_signer_config_from_environment({}))
|
|
with self.assertRaises(SignerConfigurationError):
|
|
load_signer_config_from_environment({"LIGHTHOUSE_DOMAIN_SIGNING_KEY_PATH": str(self.private_key)})
|
|
|
|
def test_rejects_relative_or_permissive_private_key_files(self):
|
|
with self.assertRaises(SignerConfigurationError):
|
|
sign_domain_manifest(
|
|
private_key_path=Path("relative.pem"),
|
|
domain_id="DOM-FIFTH-0001",
|
|
repository_id="REPO-012",
|
|
signer_id="GH-LIGHTHOUSE-001",
|
|
source_commit="b" * 40,
|
|
)
|
|
self.private_key.chmod(0o644)
|
|
with self.assertRaises(SignerConfigurationError):
|
|
sign_domain_manifest(
|
|
private_key_path=self.private_key,
|
|
domain_id="DOM-FIFTH-0001",
|
|
repository_id="REPO-012",
|
|
signer_id="GH-LIGHTHOUSE-001",
|
|
source_commit="b" * 40,
|
|
)
|
|
|
|
def test_rejects_unbounded_identifiers_and_invalid_source_commits(self):
|
|
for field, value in (("domain_id", "../../escape"), ("source_commit", "not-a-commit")):
|
|
arguments = {
|
|
"private_key_path": self.private_key,
|
|
"domain_id": "DOM-FIFTH-0001",
|
|
"repository_id": "REPO-012",
|
|
"signer_id": "GH-LIGHTHOUSE-001",
|
|
"source_commit": "b" * 40,
|
|
}
|
|
arguments[field] = value
|
|
with self.assertRaises(SignerConfigurationError):
|
|
sign_domain_manifest(**arguments)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|