Skip to content

Commit 916f925

Browse files
committed
cli: allow DSSE verification
Signed-off-by: William Woodruff <william@trailofbits.com>
1 parent 2cb68fe commit 916f925

File tree

2 files changed

+72
-8
lines changed

2 files changed

+72
-8
lines changed

sigstore/_cli.py

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
from cryptography.x509 import load_pem_x509_certificate
2727
from rich.logging import RichHandler
2828

29-
from sigstore import __version__
29+
from sigstore import __version__, dsse
3030
from sigstore._internal.fulcio.client import (
3131
DEFAULT_FULCIO_URL,
3232
ExpiredCertificate,
@@ -813,11 +813,31 @@ def _verify_identity(args: argparse.Namespace) -> None:
813813
)
814814

815815
try:
816-
verifier.verify_artifact(
817-
input_=hashed,
818-
bundle=bundle,
819-
policy=policy_,
820-
)
816+
# If the bundle specifies a DSSE envelope, perform DSSE verification
817+
# and assert that the inner payload is an in-toto statement bound
818+
# to a subject matching the input's digest.
819+
if bundle._dsse_envelope:
820+
with file.open(mode="rb", buffering=0) as io:
821+
digest = sha256_digest(io)
822+
823+
type_, payload = verifier.verify_dsse(bundle=bundle, policy=policy_)
824+
if type_ != dsse.Envelope._TYPE:
825+
raise VerificationError(
826+
f"expected JSON payload for DSSE, got {type_}"
827+
)
828+
829+
stmt = dsse.Statement(payload)
830+
if not stmt._matches_digest(digest):
831+
raise VerificationError(
832+
f"in-toto statement has no subject for digest {digest.digest.hex()}"
833+
)
834+
else:
835+
verifier.verify_artifact(
836+
input_=hashed,
837+
bundle=bundle,
838+
policy=policy_,
839+
)
840+
821841
print(f"OK: {file}")
822842
except VerificationError as exc:
823843
_logger.error(f"FAIL: {file}")
@@ -851,7 +871,30 @@ def _verify_github(args: argparse.Namespace) -> None:
851871
verifier, materials = _collect_verification_state(args)
852872
for file, hashed, bundle in materials:
853873
try:
854-
verifier.verify_artifact(input_=hashed, bundle=bundle, policy=policy_)
874+
# If the bundle specifies a DSSE envelope, perform DSSE verification
875+
# and assert that the inner payload is an in-toto statement bound
876+
# to a subject matching the input's digest.
877+
if bundle._dsse_envelope:
878+
with file.open(mode="rb", buffering=0) as io:
879+
digest = sha256_digest(io)
880+
881+
type_, payload = verifier.verify_dsse(bundle=bundle, policy=policy_)
882+
if type_ != dsse.Envelope._TYPE:
883+
raise VerificationError(
884+
f"expected JSON payload for DSSE, got {type_}"
885+
)
886+
887+
stmt = dsse.Statement(payload)
888+
if not stmt._matches_digest(digest):
889+
raise VerificationError(
890+
f"in-toto statement has no subject for digest {digest.digest.hex()}"
891+
)
892+
else:
893+
verifier.verify_artifact(
894+
input_=hashed,
895+
bundle=bundle,
896+
policy=policy_,
897+
)
855898
print(f"OK: {file}")
856899
except VerificationError as exc:
857900
_logger.error(f"FAIL: {file}")

sigstore/dsse.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,12 @@
2525
from cryptography.hazmat.primitives import hashes
2626
from cryptography.hazmat.primitives.asymmetric import ec
2727
from pydantic import BaseModel, ConfigDict, Field, RootModel, StrictStr, ValidationError
28+
from sigstore_protobuf_specs.dev.sigstore.common.v1 import HashAlgorithm
2829
from sigstore_protobuf_specs.io.intoto import Envelope as _Envelope
2930
from sigstore_protobuf_specs.io.intoto import Signature
3031

3132
from sigstore.errors import VerificationError
33+
from sigstore.hashes import Hashed
3234

3335
_logger = logging.getLogger(__name__)
3436

@@ -97,10 +99,29 @@ def __init__(self, contents: bytes) -> None:
9799
"""
98100
self._contents = contents
99101
try:
100-
self._statement = _Statement.model_validate_json(contents)
102+
self._inner = _Statement.model_validate_json(contents)
101103
except ValidationError:
102104
raise ValueError("malformed in-toto statement")
103105

106+
def _matches_digest(self, digest: Hashed) -> bool:
107+
"""
108+
Returns a boolean indicating whether this in-toto Statement contains a subject
109+
matching the given digest. The subject's name is **not** checked.
110+
111+
No digests other than SHA256 are currently supported.
112+
"""
113+
if digest.algorithm != HashAlgorithm.SHA2_256:
114+
raise VerificationError(f"unexpected digest algorithm: {digest.algorithm}")
115+
116+
for sub in self._inner.subjects:
117+
sub_digest = sub.digest.root.get("sha256")
118+
if sub_digest is None:
119+
continue
120+
if sub_digest == digest.digest.hex():
121+
return True
122+
123+
return False
124+
104125
def _pae(self) -> bytes:
105126
"""
106127
Construct the PAE encoding for this statement.

0 commit comments

Comments
 (0)