diff --git a/CHANGELOG.md b/CHANGELOG.md index c00dc72..caaf5e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- Sealing to a low-order X25519 peer key raises `SealedChannelError`, consistent + with other invalid channel keys, instead of leaking a backend `ValueError`. + These keys were already refused; no ciphertext format or acceptance rule changes. + ### Removed - The `opaque` attestation provider value. It had no collector and was never diff --git a/src/ca2a_runtime/channel/sealed.py b/src/ca2a_runtime/channel/sealed.py index bddae18..70c9b16 100644 --- a/src/ca2a_runtime/channel/sealed.py +++ b/src/ca2a_runtime/channel/sealed.py @@ -69,7 +69,10 @@ def seal(self, payload: bytes, *, aad: bytes = b"") -> bytes: eph_priv = X25519PrivateKey.generate() eph_pub = eph_priv.public_key().public_bytes_raw() peer_pub_raw = self._peer_pub.public_bytes_raw() - shared = eph_priv.exchange(self._peer_pub) + try: + shared = eph_priv.exchange(self._peer_pub) + except ValueError as exc: + raise SealedChannelError("invalid peer public key", detail=str(exc)) from exc key = _derive_key(shared, eph_pub, peer_pub_raw) nonce = os.urandom(_NONCE_LEN) ct = ChaCha20Poly1305(key).encrypt(nonce, payload, aad) diff --git a/tests/unit/test_sealed_channel.py b/tests/unit/test_sealed_channel.py index d7d18f3..401b019 100644 --- a/tests/unit/test_sealed_channel.py +++ b/tests/unit/test_sealed_channel.py @@ -58,3 +58,12 @@ def test_malformed_blob_rejected() -> None: def test_invalid_peer_key_rejected() -> None: with pytest.raises(SealedChannelError): SealedChannel("not-hex-zz") + + +@pytest.mark.parametrize("peer_key", ["00" * 32, "01" + "00" * 31, "ec" + "ff" * 30 + "7f"]) +def test_low_order_peer_key_has_channel_error(peer_key: str) -> None: + # These keys parse as X25519 keys but cannot produce a nonzero shared secret. + channel = SealedChannel(peer_key) + with pytest.raises(SealedChannelError, match="invalid peer public key") as caught: + channel.seal(b"secret", aad=b"session-A") + assert isinstance(caught.value.__cause__, ValueError)