Skip to content

Commit 0fc73bd

Browse files
authored
Initial DSSE verify APIs (#962)
* verifier: hackety hack on DSSE support Signed-off-by: William Woodruff <[email protected]> * hackety hack Signed-off-by: William Woodruff <[email protected]> * sigstore, test: initial DSSE verify APIs Signed-off-by: William Woodruff <[email protected]> * sigstore: lintage Signed-off-by: William Woodruff <[email protected]> * test: fix test_sign_prehashed Signed-off-by: William Woodruff <[email protected]> * sigstore: cap off DSSE verification Signed-off-by: William Woodruff <[email protected]> * sigstore: judicious ignores Signed-off-by: William Woodruff <[email protected]> * test: add a DSSE roundtrip test Signed-off-by: William Woodruff <[email protected]> * missing call Signed-off-by: William Woodruff <[email protected]> * fix more types Signed-off-by: William Woodruff <[email protected]> * fix test Signed-off-by: William Woodruff <[email protected]> * verifier: typo Signed-off-by: William Woodruff <[email protected]> * rekor: remove _dsse_from_parts This was unused. Signed-off-by: William Woodruff <[email protected]> * dsse, verifier: handle multiple sigs gracefully Signed-off-by: William Woodruff <[email protected]> --------- Signed-off-by: William Woodruff <[email protected]>
1 parent 4f60595 commit 0fc73bd

File tree

9 files changed

+311
-134
lines changed

9 files changed

+311
-134
lines changed

sigstore/_cli.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -813,7 +813,7 @@ def _verify_identity(args: argparse.Namespace) -> None:
813813
)
814814

815815
try:
816-
verifier.verify(
816+
verifier.verify_artifact(
817817
input_=hashed,
818818
bundle=bundle,
819819
policy=policy_,
@@ -851,7 +851,7 @@ def _verify_github(args: argparse.Namespace) -> None:
851851
verifier, materials = _collect_verification_state(args)
852852
for file, hashed, bundle in materials:
853853
try:
854-
verifier.verify(input_=hashed, bundle=bundle, policy=policy_)
854+
verifier.verify_artifact(input_=hashed, bundle=bundle, policy=policy_)
855855
print(f"OK: {file}")
856856
except VerificationError as exc:
857857
_logger.error(f"FAIL: {file}")

sigstore/_internal/rekor/__init__.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,11 @@
2727
from .checkpoint import SignedCheckpoint
2828
from .client import RekorClient
2929

30-
__all__ = ["RekorClient", "SignedCheckpoint"]
30+
__all__ = [
31+
"RekorClient",
32+
"SignedCheckpoint",
33+
"_hashedrekord_from_parts",
34+
]
3135

3236

3337
# TODO: This should probably live somewhere better.

sigstore/dsse.py

+44-6
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,15 @@
2121
import logging
2222
from typing import Any, Dict, List, Literal, Optional, Union
2323

24+
from cryptography.exceptions import InvalidSignature
2425
from cryptography.hazmat.primitives import hashes
2526
from cryptography.hazmat.primitives.asymmetric import ec
2627
from pydantic import BaseModel, ConfigDict, Field, RootModel, StrictStr, ValidationError
2728
from sigstore_protobuf_specs.io.intoto import Envelope as _Envelope
2829
from sigstore_protobuf_specs.io.intoto import Signature
2930

31+
from sigstore.errors import VerificationError
32+
3033
_logger = logging.getLogger(__name__)
3134

3235
_Digest = Union[
@@ -103,12 +106,7 @@ def _pae(self) -> bytes:
103106
Construct the PAE encoding for this statement.
104107
"""
105108

106-
# See:
107-
# https://github.com/secure-systems-lab/dsse/blob/v1.0.0/envelope.md
108-
# https://github.com/in-toto/attestation/blob/v1.0/spec/v1.0/envelope.md
109-
pae = f"DSSEv1 {len(Envelope._TYPE)} {Envelope._TYPE} ".encode()
110-
pae += b" ".join([str(len(self._contents)).encode(), self._contents])
111-
return pae
109+
return _pae(Envelope._TYPE, self._contents)
112110

113111

114112
class _StatementBuilder:
@@ -193,6 +191,19 @@ def to_json(self) -> str:
193191
return self._inner.to_json() # type: ignore[no-any-return]
194192

195193

194+
def _pae(type_: str, body: bytes) -> bytes:
195+
"""
196+
Compute the PAE encoding for the given `type_` and `body`.
197+
"""
198+
199+
# See:
200+
# https://github.com/secure-systems-lab/dsse/blob/v1.0.0/envelope.md
201+
# https://github.com/in-toto/attestation/blob/v1.0/spec/v1.0/envelope.md
202+
pae = f"DSSEv1 {len(type_)} {type_} ".encode()
203+
pae += b" ".join([str(len(body)).encode(), body])
204+
return pae
205+
206+
196207
def _sign(key: ec.EllipticCurvePrivateKey, stmt: Statement) -> Envelope:
197208
"""
198209
Sign for the given in-toto `Statement`, and encapsulate the resulting
@@ -209,3 +220,30 @@ def _sign(key: ec.EllipticCurvePrivateKey, stmt: Statement) -> Envelope:
209220
signatures=[Signature(sig=signature, keyid=None)],
210221
)
211222
)
223+
224+
225+
def _verify(key: ec.EllipticCurvePublicKey, evp: Envelope) -> bytes:
226+
"""
227+
Verify the given in-toto `Envelope`, returning the verified inner payload.
228+
229+
This function does **not** check the envelope's payload type. The caller
230+
is responsible for performing this check.
231+
"""
232+
233+
pae = _pae(evp._inner.payload_type, evp._inner.payload)
234+
235+
if not evp._inner.signatures:
236+
raise VerificationError("DSSE: envelope contains no signatures")
237+
238+
# In practice checking more than one signature here is frivolous, since
239+
# they're all being checked against the same key. But there's no
240+
# particular harm in checking them all either.
241+
for signature in evp._inner.signatures:
242+
try:
243+
key.verify(signature.sig, pae, ec.ECDSA(hashes.SHA256()))
244+
except InvalidSignature:
245+
raise VerificationError("DSSE: invalid signature")
246+
247+
# TODO: Remove ignore when protobuf-specs contains a py.typed marker.
248+
# See: <https://github.com/sigstore/protobuf-specs/pull/287>
249+
return evp._inner.payload # type: ignore[no-any-return]

sigstore/verify/models.py

+11
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,17 @@ def log_entry(self) -> LogEntry:
245245
"""
246246
return self._log_entry
247247

248+
@property
249+
def _dsse_envelope(self) -> dsse.Envelope | None:
250+
"""
251+
Returns the DSSE envelope within this Bundle as a `dsse.Envelope`.
252+
253+
@private
254+
"""
255+
if self._inner.dsse_envelope:
256+
return dsse.Envelope(self._inner.dsse_envelope)
257+
return None
258+
248259
@classmethod
249260
def from_json(cls, raw: bytes | str) -> Bundle:
250261
"""

0 commit comments

Comments
 (0)