feat(lighthouse): define domain manifest signing envelope
This commit is contained in:
parent
6e26d1f354
commit
ca25dab2a3
3 changed files with 297 additions and 0 deletions
|
|
@ -0,0 +1,148 @@
|
|||
#!/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()
|
||||
Loading…
Reference in a new issue