diff --git a/CHANGELOG.md b/CHANGELOG.md index 4dc0ba6..b707fdf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### Fixed +- Verification now reports malformed embedded Ed25519 public keys as a failed + signature check instead of raising a decoding exception. - `verify` and `inspect` now report unreadable or invalid receipt files as one-line CLI errors instead of raising tracebacks. diff --git a/src/answerproof/verifier.py b/src/answerproof/verifier.py index 9b85d52..4e4cd27 100644 --- a/src/answerproof/verifier.py +++ b/src/answerproof/verifier.py @@ -57,7 +57,10 @@ def failures(self) -> list[CheckResult]: def verify_signature(receipt: Receipt) -> CheckResult: - vk = VerifyKey.from_base64(receipt.signature.public_key) + try: + vk = VerifyKey.from_base64(receipt.signature.public_key) + except ValueError: + return CheckResult("signature", False, "invalid Ed25519 public key") ok = vk.verify(receipt.payload.canonical_bytes(), receipt.signature.signature) return CheckResult("signature", ok, "" if ok else "Ed25519 signature does not match payload") diff --git a/tests/test_verifier.py b/tests/test_verifier.py index ab44fb8..d63858e 100644 --- a/tests/test_verifier.py +++ b/tests/test_verifier.py @@ -32,6 +32,27 @@ def test_signer_pinning_failure(receipt, sources): assert any(c.name == "signer_pin" and not c.passed for c in verdict.checks) +@pytest.mark.parametrize( + "malformed_key", + [ + "!!!not-base64!!!", + "c2hvcnQ", + "not-ascii-🔑", + ], +) +def test_malformed_public_key_returns_failed_signature_check(receipt, malformed_key): + signature = receipt.signature.model_copy(update={"public_key": malformed_key}) + corrupted = receipt.model_copy(update={"signature": signature}) + + verdict = verify_receipt(corrupted) + + assert not verdict.valid + assert len(verdict.failures()) == 1 + failure = verdict.failures()[0] + assert failure.name == "signature" + assert failure.detail == "invalid Ed25519 public key" + + def test_wrong_source_content_fails(receipt): bad = {"s1": "This is not the original content."} verdict = verify_receipt(receipt, source_contents=bad)