diff --git a/CHANGELOG.md b/CHANGELOG.md index bfd8058c..b417d6b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,14 @@ All versions prior to 0.9.0 are untracked. ## [Unreleased] +### Added + +* API: The DSSE `Envelope` class now performs automatic validation + ([#1211](https://github.com/sigstore/sigstore-python/pull/1211)) + +* API: Added `signature` property to `Envelope` class for accessing raw + signature bytes ([#1211](https://github.com/sigstore/sigstore-python/pull/1211)) + ### Fixed * Fixed a CLI parsing bug introduced in 3.5.1 where a warning about diff --git a/sigstore/dsse/__init__.py b/sigstore/dsse/__init__.py index d1a0b949..0cc1136e 100644 --- a/sigstore/dsse/__init__.py +++ b/sigstore/dsse/__init__.py @@ -190,6 +190,12 @@ def build(self) -> Statement: return Statement(stmt) +class InvalidEnvelope(Error): + """ + Raised when the associated `Envelope` is invalid in some way. + """ + + class Envelope: """ Represents a DSSE envelope. @@ -207,6 +213,19 @@ def __init__(self, inner: _Envelope) -> None: """ self._inner = inner + self._verify() + + def _verify(self) -> None: + """ + Verify and load the Envelope. + """ + if len(self._inner.signatures) != 1: + raise InvalidEnvelope("envelope must contain exactly one signature") + + if not self._inner.signatures[0].sig: + raise InvalidEnvelope("envelope signature must be non-empty") + + self._signature_bytes = self._inner.signatures[0].sig @classmethod def _from_json(cls, contents: bytes | str) -> Envelope: @@ -228,6 +247,11 @@ def __eq__(self, other: object) -> bool: return self._inner == other._inner + @property + def signature(self) -> bytes: + """Return the decoded bytes of the Envelope signature.""" + return self._signature_bytes + def _pae(type_: str, body: bytes) -> bytes: """ diff --git a/test/unit/test_dsse.py b/test/unit/test_dsse.py index c41fa2c8..0a2ee879 100644 --- a/test/unit/test_dsse.py +++ b/test/unit/test_dsse.py @@ -15,7 +15,10 @@ import base64 import json +import pytest + from sigstore import dsse +from sigstore.dsse import InvalidEnvelope class TestEnvelope: @@ -26,7 +29,6 @@ def test_roundtrip(self): "payloadType": dsse.Envelope._TYPE, "signatures": [ {"sig": base64.b64encode(b"lol").decode()}, - {"sig": base64.b64encode(b"lmao").decode()}, ], } ) @@ -34,8 +36,49 @@ def test_roundtrip(self): assert evp._inner.payload == b"foo" assert evp._inner.payload_type == dsse.Envelope._TYPE - assert [b"lol", b"lmao"] == [s.sig for s in evp._inner.signatures] + assert evp.signature == b"lol" serialized = evp.to_json() assert serialized == raw assert dsse.Envelope._from_json(serialized) == evp + + def test_missing_signature(self): + raw = json.dumps( + { + "payload": base64.b64encode(b"foo").decode(), + "payloadType": dsse.Envelope._TYPE, + "signatures": [], + } + ) + + with pytest.raises(InvalidEnvelope, match="one signature"): + dsse.Envelope._from_json(raw) + + def test_empty_signature(self): + raw = json.dumps( + { + "payload": base64.b64encode(b"foo").decode(), + "payloadType": dsse.Envelope._TYPE, + "signatures": [ + {"sig": ""}, + ], + } + ) + + with pytest.raises(InvalidEnvelope, match="non-empty"): + dsse.Envelope._from_json(raw) + + def test_multiple_signatures(self): + raw = json.dumps( + { + "payload": base64.b64encode(b"foo").decode(), + "payloadType": dsse.Envelope._TYPE, + "signatures": [ + {"sig": base64.b64encode(b"lol").decode()}, + {"sig": base64.b64encode(b"lmao").decode()}, + ], + } + ) + + with pytest.raises(InvalidEnvelope, match="one signature"): + dsse.Envelope._from_json(raw)