From e2ce0eb4b90985148af4b447c31203659831a746 Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Fri, 13 Oct 2023 17:26:32 +0200 Subject: [PATCH 01/18] hackety hack Signed-off-by: William Woodruff --- sigstore/_internal/dsse.py | 44 ++++++++++++++++++++++++++++++ sigstore/_internal/rekor/client.py | 6 ++-- sigstore/sign.py | 4 +-- 3 files changed, 49 insertions(+), 5 deletions(-) create mode 100644 sigstore/_internal/dsse.py diff --git a/sigstore/_internal/dsse.py b/sigstore/_internal/dsse.py new file mode 100644 index 00000000..38b78702 --- /dev/null +++ b/sigstore/_internal/dsse.py @@ -0,0 +1,44 @@ +# Copyright 2022 The Sigstore Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Functionality for building and manipulating DSSE envelopes. +""" + +import base64 +from cryptography.hazmat.primitives.asymmetric import ec +from cryptography.hazmat.primitives import hashes +from sigstore_protobuf_specs.io.intoto import Envelope, Signature + + +def sign_intoto(key: ec.EllipticCurvePrivateKey, payload: bytes) -> Envelope: + """ + Create a DSSE envelope containing a signature over an in-toto formatted + attestation. + """ + + # See: + # https://github.com/secure-systems-lab/dsse/blob/v1.0.0/envelope.md + # https://github.com/in-toto/attestation/blob/v1.0/spec/v1.0/envelope.md + + type_ = "application/vnd.in-toto+json" + payload_b64 = base64.b64encode(payload).decode() + pae = f"DSSEv1 {len(type_)} {type} {len(payload_b64)} {payload_b64}" + + signature = key.sign(pae.encode(), ec.ECDSA(hashes.SHA256)) + return Envelope( + payload=payload, + payload_type=type_, + signatures=Signature(sig=signature, keyid=None), + ) diff --git a/sigstore/_internal/rekor/client.py b/sigstore/_internal/rekor/client.py index 576a6cb7..12ec2775 100644 --- a/sigstore/_internal/rekor/client.py +++ b/sigstore/_internal/rekor/client.py @@ -25,7 +25,7 @@ from urllib.parse import urljoin import requests -import sigstore_rekor_types +import sigstore_rekor_types as rekor_types from sigstore._internal.ctfe import CTKeyring from sigstore._internal.keyring import Keyring @@ -137,7 +137,7 @@ def get( def post( self, - proposed_entry: sigstore_rekor_types.Hashedrekord, + proposed_entry: rekor_types.Hashedrekord | rekor_types.Dsse, ) -> LogEntry: """ Submit a new entry for inclusion in the Rekor log. @@ -170,7 +170,7 @@ class RekorEntriesRetrieve(_Endpoint): def post( self, - expected_entry: sigstore_rekor_types.Hashedrekord, + expected_entry: rekor_types.Hashedrekord | rekor_types.Dsse, ) -> Optional[LogEntry]: """ Retrieves an extant Rekor entry, identified by its artifact signature, diff --git a/sigstore/sign.py b/sigstore/sign.py index bbde5cd2..3428c0ea 100644 --- a/sigstore/sign.py +++ b/sigstore/sign.py @@ -175,6 +175,8 @@ def _signing_cert( def sign( self, input_: IO[bytes], + *, + dsse=False, ) -> SigningResult: """Public API for signing blobs""" input_digest = sha256_streaming(input_) @@ -188,8 +190,6 @@ def sign( except ExpiredCertificate as e: raise e - # TODO(alex): Retrieve the public key via TUF - # # Verify the SCT sct = certificate_response.sct # noqa cert = certificate_response.cert # noqa From 476d527b81914e98cf9270706443272a75c19067 Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Fri, 27 Oct 2023 02:30:09 +0200 Subject: [PATCH 02/18] hackety hack Signed-off-by: William Woodruff --- pyproject.toml | 5 ++--- sigstore/_internal/dsse.py | 10 ++++++---- sigstore/sign.py | 5 +++-- sigstore/verify/verifier.py | 2 +- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 6e4391f5..5b2c8483 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,6 +29,7 @@ dependencies = [ "cryptography >= 39", "id >= 1.1.0", "importlib_resources ~= 5.7; python_version < '3.11'", + "in-toto-attestation ~= 0.9", "pydantic >= 2,< 3", "pyjwt >= 2.1", "pyOpenSSL >= 23.0.0", @@ -61,9 +62,7 @@ lint = [ # and let Dependabot periodically perform this update. "ruff < 0.1.1", "types-requests", - # TODO(ww): Re-enable once dependency on types-cryptography is dropped. - # See: https://github.com/python/typeshed/issues/8699 - # "types-pyOpenSSL", + "types-pyOpenSSL", ] doc = ["pdoc"] dev = ["build", "bump >= 1.3.2", "sigstore[doc,test,lint]"] diff --git a/sigstore/_internal/dsse.py b/sigstore/_internal/dsse.py index 38b78702..601a31b6 100644 --- a/sigstore/_internal/dsse.py +++ b/sigstore/_internal/dsse.py @@ -17,12 +17,14 @@ """ import base64 -from cryptography.hazmat.primitives.asymmetric import ec + from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.asymmetric import ec +from in_toto_attestation.v1.statement import Statement from sigstore_protobuf_specs.io.intoto import Envelope, Signature -def sign_intoto(key: ec.EllipticCurvePrivateKey, payload: bytes) -> Envelope: +def sign_intoto(key: ec.EllipticCurvePrivateKey, payload: Statement) -> Envelope: """ Create a DSSE envelope containing a signature over an in-toto formatted attestation. @@ -36,9 +38,9 @@ def sign_intoto(key: ec.EllipticCurvePrivateKey, payload: bytes) -> Envelope: payload_b64 = base64.b64encode(payload).decode() pae = f"DSSEv1 {len(type_)} {type} {len(payload_b64)} {payload_b64}" - signature = key.sign(pae.encode(), ec.ECDSA(hashes.SHA256)) + signature = key.sign(pae.encode(), ec.ECDSA(hashes.SHA256())) return Envelope( payload=payload, payload_type=type_, - signatures=Signature(sig=signature, keyid=None), + signatures=[Signature(sig=signature, keyid=None)], ) diff --git a/sigstore/sign.py b/sigstore/sign.py index 3428c0ea..c8c579ac 100644 --- a/sigstore/sign.py +++ b/sigstore/sign.py @@ -51,6 +51,7 @@ from cryptography.hazmat.primitives.asymmetric import ec from cryptography.hazmat.primitives.asymmetric.utils import Prehashed from cryptography.x509.oid import NameOID +from in_toto_attestation.v1.statement import Statement from pydantic import BaseModel from sigstore_protobuf_specs.dev.sigstore.bundle.v1 import ( Bundle, @@ -174,9 +175,9 @@ def _signing_cert( def sign( self, - input_: IO[bytes], + input_: IO[bytes] | Statement, *, - dsse=False, + dsse: bool = False, ) -> SigningResult: """Public API for signing blobs""" input_digest = sha256_streaming(input_) diff --git a/sigstore/verify/verifier.py b/sigstore/verify/verifier.py index d61052e7..e7e67ee5 100644 --- a/sigstore/verify/verifier.py +++ b/sigstore/verify/verifier.py @@ -29,7 +29,7 @@ from cryptography.hazmat.primitives.asymmetric.utils import Prehashed from cryptography.x509 import Certificate, ExtendedKeyUsage, KeyUsage from cryptography.x509.oid import ExtendedKeyUsageOID -from OpenSSL.crypto import ( # type: ignore[import-untyped] +from OpenSSL.crypto import ( X509, X509Store, X509StoreContext, From f6899be623eb0967f8a89077a0b2bb0ceeaa855d Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Wed, 6 Dec 2023 17:05:09 -0500 Subject: [PATCH 03/18] sigstore: hackety hack Signed-off-by: William Woodruff --- sigstore/_internal/dsse.py | 5 +-- sigstore/sign.py | 69 ++++++++++++++++++++++++-------------- 2 files changed, 46 insertions(+), 28 deletions(-) diff --git a/sigstore/_internal/dsse.py b/sigstore/_internal/dsse.py index 601a31b6..9d34b842 100644 --- a/sigstore/_internal/dsse.py +++ b/sigstore/_internal/dsse.py @@ -20,6 +20,7 @@ from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.asymmetric import ec +from google.protobuf.json_format import MessageToJson from in_toto_attestation.v1.statement import Statement from sigstore_protobuf_specs.io.intoto import Envelope, Signature @@ -35,12 +36,12 @@ def sign_intoto(key: ec.EllipticCurvePrivateKey, payload: Statement) -> Envelope # https://github.com/in-toto/attestation/blob/v1.0/spec/v1.0/envelope.md type_ = "application/vnd.in-toto+json" - payload_b64 = base64.b64encode(payload).decode() + payload_b64 = base64.b64encode(MessageToJson(payload.pb).encode()).decode() pae = f"DSSEv1 {len(type_)} {type} {len(payload_b64)} {payload_b64}" signature = key.sign(pae.encode(), ec.ECDSA(hashes.SHA256())) return Envelope( - payload=payload, + payload=payload_b64.encode(), payload_type=type_, signatures=[Signature(sig=signature, keyid=None)], ) diff --git a/sigstore/sign.py b/sigstore/sign.py index c8c579ac..d8bd25ef 100644 --- a/sigstore/sign.py +++ b/sigstore/sign.py @@ -73,6 +73,7 @@ TransparencyLogEntry, ) +from sigstore._internal import dsse from sigstore._internal.fulcio import ( ExpiredCertificate, FulcioCertificateSigningResponse, @@ -176,11 +177,8 @@ def _signing_cert( def sign( self, input_: IO[bytes] | Statement, - *, - dsse: bool = False, ) -> SigningResult: """Public API for signing blobs""" - input_digest = sha256_streaming(input_) private_key = self._private_key if not self._identity_token.in_validity_period(): @@ -200,36 +198,55 @@ def sign( logger.debug("Successfully verified SCT...") - # Sign artifact - artifact_signature = private_key.sign( - input_digest, ec.ECDSA(Prehashed(hashes.SHA256())) - ) - b64_artifact_signature = B64Str(base64.b64encode(artifact_signature).decode()) - # Prepare inputs b64_cert = base64.b64encode( cert.public_bytes(encoding=serialization.Encoding.PEM) ) - # Create the transparency log entry - proposed_entry = sigstore_rekor_types.Hashedrekord( - kind="hashedrekord", - api_version="0.0.1", - spec=sigstore_rekor_types.HashedrekordV001Schema( - signature=sigstore_rekor_types.Signature1( - content=b64_artifact_signature, - public_key=sigstore_rekor_types.PublicKey1( - content=b64_cert.decode() - ), - ), - data=sigstore_rekor_types.Data( - hash=sigstore_rekor_types.Hash( - algorithm=sigstore_rekor_types.Algorithm.SHA256, - value=input_digest.hex(), + # Sign artifact + proposed_entry: sigstore_rekor_types.Hashedrekord | sigstore_rekor_types.Dsse + if isinstance(input_, Statement): + envelope = dsse.sign_intoto(private_key, input_) + + proposed_entry = sigstore_rekor_types.Dsse( + kind="dsse", + api_version="0.0.1", + spec=sigstore_rekor_types.DsseV001Schema( + proposed_content=sigstore_rekor_types.ProposedContent( + envelope=envelope.to_json(), + verifiers=[b64_cert.decode()], ) ), - ), - ) + ) + else: + input_digest = sha256_streaming(input_) + + artifact_signature = private_key.sign( + input_digest, ec.ECDSA(Prehashed(hashes.SHA256())) + ) + b64_artifact_signature = B64Str( + base64.b64encode(artifact_signature).decode() + ) + + # Create the transparency log entry + proposed_entry = sigstore_rekor_types.Hashedrekord( + kind="hashedrekord", + api_version="0.0.1", + spec=sigstore_rekor_types.HashedrekordV001Schema( + signature=sigstore_rekor_types.Signature1( + content=b64_artifact_signature, + public_key=sigstore_rekor_types.PublicKey1( + content=b64_cert.decode() + ), + ), + data=sigstore_rekor_types.Data( + hash=sigstore_rekor_types.Hash( + algorithm=sigstore_rekor_types.Algorithm.SHA256, + value=input_digest.hex(), + ) + ), + ), + ) entry = self._signing_ctx._rekor.log.entries.post(proposed_entry) logger.debug(f"Transparency log entry created with index: {entry.log_index}") From 77526da03c2ce285a6e5c02a8ea6dc503e3df50c Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Wed, 6 Dec 2023 18:36:13 -0500 Subject: [PATCH 04/18] hackety hack Signed-off-by: William Woodruff --- sigstore/_internal/dsse.py | 6 +++--- sigstore/_internal/rekor/client.py | 8 +++++--- sigstore/sign.py | 3 ++- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/sigstore/_internal/dsse.py b/sigstore/_internal/dsse.py index 9d34b842..acd7a8c5 100644 --- a/sigstore/_internal/dsse.py +++ b/sigstore/_internal/dsse.py @@ -36,12 +36,12 @@ def sign_intoto(key: ec.EllipticCurvePrivateKey, payload: Statement) -> Envelope # https://github.com/in-toto/attestation/blob/v1.0/spec/v1.0/envelope.md type_ = "application/vnd.in-toto+json" - payload_b64 = base64.b64encode(MessageToJson(payload.pb).encode()).decode() - pae = f"DSSEv1 {len(type_)} {type} {len(payload_b64)} {payload_b64}" + payload_encoded = MessageToJson(payload.pb).encode() + pae = f"DSSEv1 {len(type_)} {type_} {len(payload_encoded)} {payload_encoded}" signature = key.sign(pae.encode(), ec.ECDSA(hashes.SHA256())) return Envelope( - payload=payload_b64.encode(), + payload=base64.b64encode(payload_encoded), payload_type=type_, signatures=[Signature(sig=signature, keyid=None)], ) diff --git a/sigstore/_internal/rekor/client.py b/sigstore/_internal/rekor/client.py index 8cc60c70..9aa3f451 100644 --- a/sigstore/_internal/rekor/client.py +++ b/sigstore/_internal/rekor/client.py @@ -143,12 +143,14 @@ def post( Submit a new entry for inclusion in the Rekor log. """ - resp: requests.Response = self.session.post( - self.url, json=proposed_entry.model_dump(mode="json", by_alias=True) - ) + payload = proposed_entry.model_dump(mode="json", by_alias=True) + logger.debug(payload) + + resp: requests.Response = self.session.post(self.url, json=payload) try: resp.raise_for_status() except requests.HTTPError as http_error: + logger.debug(http_error.response.content) raise RekorClientError from http_error return LogEntry._from_response(resp.json()) diff --git a/sigstore/sign.py b/sigstore/sign.py index d8bd25ef..6caa955f 100644 --- a/sigstore/sign.py +++ b/sigstore/sign.py @@ -40,6 +40,7 @@ from __future__ import annotations import base64 +import hashlib import logging from contextlib import contextmanager from datetime import datetime, timezone @@ -215,7 +216,7 @@ def sign( proposed_content=sigstore_rekor_types.ProposedContent( envelope=envelope.to_json(), verifiers=[b64_cert.decode()], - ) + ), ), ) else: From 63f9b295e81fd7cfbe258fbcf4b022fefe03c962 Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Wed, 6 Dec 2023 18:36:58 -0500 Subject: [PATCH 05/18] hackety hack Signed-off-by: William Woodruff --- sigstore/_internal/rekor/client.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sigstore/_internal/rekor/client.py b/sigstore/_internal/rekor/client.py index 9aa3f451..262f8feb 100644 --- a/sigstore/_internal/rekor/client.py +++ b/sigstore/_internal/rekor/client.py @@ -18,6 +18,7 @@ from __future__ import annotations +import json import logging from abc import ABC from dataclasses import dataclass @@ -144,7 +145,7 @@ def post( """ payload = proposed_entry.model_dump(mode="json", by_alias=True) - logger.debug(payload) + logger.debug(json.dumps(payload)) resp: requests.Response = self.session.post(self.url, json=payload) try: From 82a6fee6c6a8ca77c9816f5c8339267e9b2813c2 Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Wed, 6 Dec 2023 19:00:23 -0500 Subject: [PATCH 06/18] sigstore: don't double encode Signed-off-by: William Woodruff --- sigstore/_internal/dsse.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/sigstore/_internal/dsse.py b/sigstore/_internal/dsse.py index acd7a8c5..e89e8736 100644 --- a/sigstore/_internal/dsse.py +++ b/sigstore/_internal/dsse.py @@ -16,8 +16,6 @@ Functionality for building and manipulating DSSE envelopes. """ -import base64 - from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.asymmetric import ec from google.protobuf.json_format import MessageToJson @@ -41,7 +39,7 @@ def sign_intoto(key: ec.EllipticCurvePrivateKey, payload: Statement) -> Envelope signature = key.sign(pae.encode(), ec.ECDSA(hashes.SHA256())) return Envelope( - payload=base64.b64encode(payload_encoded), + payload=payload_encoded, payload_type=type_, signatures=[Signature(sig=signature, keyid=None)], ) From ff8d358106de99d68fc41b46b5506082d9bdf19d Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Thu, 7 Dec 2023 14:34:21 -0500 Subject: [PATCH 07/18] fixup DSSE signing, refactor RekorClientError Signed-off-by: William Woodruff --- sigstore/_internal/dsse.py | 6 ++++-- sigstore/_internal/rekor/client.py | 22 ++++++++++++++-------- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/sigstore/_internal/dsse.py b/sigstore/_internal/dsse.py index e89e8736..8a0bc30a 100644 --- a/sigstore/_internal/dsse.py +++ b/sigstore/_internal/dsse.py @@ -34,8 +34,10 @@ def sign_intoto(key: ec.EllipticCurvePrivateKey, payload: Statement) -> Envelope # https://github.com/in-toto/attestation/blob/v1.0/spec/v1.0/envelope.md type_ = "application/vnd.in-toto+json" - payload_encoded = MessageToJson(payload.pb).encode() - pae = f"DSSEv1 {len(type_)} {type_} {len(payload_encoded)} {payload_encoded}" + payload_encoded = MessageToJson(payload.pb, sort_keys=True).encode() + pae = ( + f"DSSEv1 {len(type_)} {type_} {len(payload_encoded)} {payload_encoded.decode()}" + ) signature = key.sign(pae.encode(), ec.ECDSA(hashes.SHA256())) return Envelope( diff --git a/sigstore/_internal/rekor/client.py b/sigstore/_internal/rekor/client.py index 262f8feb..1d554f27 100644 --- a/sigstore/_internal/rekor/client.py +++ b/sigstore/_internal/rekor/client.py @@ -73,7 +73,14 @@ class RekorClientError(Exception): A generic error in the Rekor client. """ - pass + def __init__(self, http_error: requests.HTTPError): + try: + error = rekor_types.Error.model_validate_json(http_error.response.text) + super().__init__(f"{error.code}: {error.message}") + except Exception: + super().__init__( + f"Rekor returned an unknown error with HTTP {http_error.response.status_code}" + ) class _Endpoint(ABC): @@ -95,7 +102,7 @@ def get(self) -> RekorLogInfo: try: resp.raise_for_status() except requests.HTTPError as http_error: - raise RekorClientError from http_error + raise RekorClientError(http_error) return RekorLogInfo.from_response(resp.json()) @property @@ -121,7 +128,7 @@ def get( Either `uuid` or `log_index` must be present, but not both. """ if not (bool(uuid) ^ bool(log_index)): - raise RekorClientError("uuid or log_index required, but not both") + raise ValueError("uuid or log_index required, but not both") resp: requests.Response @@ -133,7 +140,7 @@ def get( try: resp.raise_for_status() except requests.HTTPError as http_error: - raise RekorClientError from http_error + raise RekorClientError(http_error) return LogEntry._from_response(resp.json()) def post( @@ -145,14 +152,13 @@ def post( """ payload = proposed_entry.model_dump(mode="json", by_alias=True) - logger.debug(json.dumps(payload)) + logger.debug(f"PROPOSED ENTRY: {json.dumps(payload)}") resp: requests.Response = self.session.post(self.url, json=payload) try: resp.raise_for_status() except requests.HTTPError as http_error: - logger.debug(http_error.response.content) - raise RekorClientError from http_error + raise RekorClientError(http_error) return LogEntry._from_response(resp.json()) @@ -190,7 +196,7 @@ def post( except requests.HTTPError as http_error: if http_error.response and http_error.response.status_code == 404: return None - raise RekorClientError(resp.text) from http_error + raise RekorClientError(http_error) results = resp.json() From 470232c9026e91968a4cc1873fee10dab3f791ef Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Thu, 7 Dec 2023 14:44:19 -0500 Subject: [PATCH 08/18] sigstore: docs Signed-off-by: William Woodruff --- sigstore/_internal/dsse.py | 2 ++ sigstore/sign.py | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/sigstore/_internal/dsse.py b/sigstore/_internal/dsse.py index 8a0bc30a..5dc3d961 100644 --- a/sigstore/_internal/dsse.py +++ b/sigstore/_internal/dsse.py @@ -35,6 +35,8 @@ def sign_intoto(key: ec.EllipticCurvePrivateKey, payload: Statement) -> Envelope type_ = "application/vnd.in-toto+json" payload_encoded = MessageToJson(payload.pb, sort_keys=True).encode() + # NOTE: `payload_encoded.decode()` to avoid printing `repr(bytes)`, which would + # add `b'...'` around the formatted payload. pae = ( f"DSSEv1 {len(type_)} {type_} {len(payload_encoded)} {payload_encoded.decode()}" ) diff --git a/sigstore/sign.py b/sigstore/sign.py index 6caa955f..d789d7eb 100644 --- a/sigstore/sign.py +++ b/sigstore/sign.py @@ -209,6 +209,7 @@ def sign( if isinstance(input_, Statement): envelope = dsse.sign_intoto(private_key, input_) + # Create the proposed DSSE entry proposed_entry = sigstore_rekor_types.Dsse( kind="dsse", api_version="0.0.1", @@ -229,7 +230,7 @@ def sign( base64.b64encode(artifact_signature).decode() ) - # Create the transparency log entry + # Create the proposed hashedrekord entry proposed_entry = sigstore_rekor_types.Hashedrekord( kind="hashedrekord", api_version="0.0.1", @@ -248,6 +249,8 @@ def sign( ), ), ) + + # Submit the proposed entry to the transparency log entry = self._signing_ctx._rekor.log.entries.post(proposed_entry) logger.debug(f"Transparency log entry created with index: {entry.log_index}") From b46eac9a04774644dc50139f8839cb0c266c2549 Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Thu, 7 Dec 2023 15:05:57 -0500 Subject: [PATCH 09/18] sigstore: lintage Signed-off-by: William Woodruff --- sigstore/_internal/rekor/client.py | 17 ++++++++++------- sigstore/sign.py | 5 ++--- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/sigstore/_internal/rekor/client.py b/sigstore/_internal/rekor/client.py index 1d554f27..7f2c74d2 100644 --- a/sigstore/_internal/rekor/client.py +++ b/sigstore/_internal/rekor/client.py @@ -74,13 +74,16 @@ class RekorClientError(Exception): """ def __init__(self, http_error: requests.HTTPError): - try: - error = rekor_types.Error.model_validate_json(http_error.response.text) - super().__init__(f"{error.code}: {error.message}") - except Exception: - super().__init__( - f"Rekor returned an unknown error with HTTP {http_error.response.status_code}" - ) + if http_error.response: + try: + error = rekor_types.Error.model_validate_json(http_error.response.text) + super().__init__(f"{error.code}: {error.message}") + except Exception: + super().__init__( + f"Rekor returned an unknown error with HTTP {http_error.response.status_code}" + ) + else: + super().__init__(f"Unexpected Rekor error: {http_error}") class _Endpoint(ABC): diff --git a/sigstore/sign.py b/sigstore/sign.py index d789d7eb..f08a7536 100644 --- a/sigstore/sign.py +++ b/sigstore/sign.py @@ -40,7 +40,6 @@ from __future__ import annotations import base64 -import hashlib import logging from contextlib import contextmanager from datetime import datetime, timezone @@ -191,8 +190,8 @@ def sign( raise e # Verify the SCT - sct = certificate_response.sct # noqa - cert = certificate_response.cert # noqa + sct = certificate_response.sct + cert = certificate_response.cert chain = certificate_response.chain verify_sct(sct, cert, chain, self._signing_ctx._rekor._ct_keyring) From 35a1e7f0be53ff264b7a03ebbc920eacbc946b06 Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Thu, 7 Dec 2023 16:35:01 -0500 Subject: [PATCH 10/18] make SigningResult generic over contents Signed-off-by: William Woodruff --- pyproject.toml | 1 + sigstore/sign.py | 61 +++++++++++++++++++++++++++++------------------- 2 files changed, 38 insertions(+), 24 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 3c10429b..03978f17 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,6 +61,7 @@ lint = [ # and let Dependabot periodically perform this update. "ruff < 0.1.7", "types-requests", + "types-protobuf", "types-pyOpenSSL", ] doc = ["pdoc"] diff --git a/sigstore/sign.py b/sigstore/sign.py index f08a7536..856b1811 100644 --- a/sigstore/sign.py +++ b/sigstore/sign.py @@ -42,6 +42,7 @@ import base64 import logging from contextlib import contextmanager +from dataclasses import dataclass from datetime import datetime, timezone from typing import IO, Iterator, Optional @@ -52,7 +53,6 @@ from cryptography.hazmat.primitives.asymmetric.utils import Prehashed from cryptography.x509.oid import NameOID from in_toto_attestation.v1.statement import Statement -from pydantic import BaseModel from sigstore_protobuf_specs.dev.sigstore.bundle.v1 import ( Bundle, VerificationMaterial, @@ -72,6 +72,7 @@ KindVersion, TransparencyLogEntry, ) +from sigstore_protobuf_specs.io.intoto import Envelope from sigstore._internal import dsse from sigstore._internal.fulcio import ( @@ -82,7 +83,7 @@ from sigstore._internal.rekor.client import RekorClient from sigstore._internal.sct import verify_sct from sigstore._internal.tuf import TrustUpdater -from sigstore._utils import B64Str, HexStr, PEMCert, sha256_streaming +from sigstore._utils import B64Str, PEMCert, sha256_streaming from sigstore.oidc import ExpiredIdentity, IdentityToken from sigstore.transparency import LogEntry @@ -204,9 +205,10 @@ def sign( ) # Sign artifact + content: MessageSignature | Envelope proposed_entry: sigstore_rekor_types.Hashedrekord | sigstore_rekor_types.Dsse if isinstance(input_, Statement): - envelope = dsse.sign_intoto(private_key, input_) + content = dsse.sign_intoto(private_key, input_) # Create the proposed DSSE entry proposed_entry = sigstore_rekor_types.Dsse( @@ -214,7 +216,7 @@ def sign( api_version="0.0.1", spec=sigstore_rekor_types.DsseV001Schema( proposed_content=sigstore_rekor_types.ProposedContent( - envelope=envelope.to_json(), + envelope=content.to_json(), verifiers=[b64_cert.decode()], ), ), @@ -225,8 +227,13 @@ def sign( artifact_signature = private_key.sign( input_digest, ec.ECDSA(Prehashed(hashes.SHA256())) ) - b64_artifact_signature = B64Str( - base64.b64encode(artifact_signature).decode() + + content = MessageSignature( + message_digest=HashOutput( + algorithm=HashAlgorithm.SHA2_256, + digest=input_digest, + ), + signature=artifact_signature, ) # Create the proposed hashedrekord entry @@ -235,7 +242,7 @@ def sign( api_version="0.0.1", spec=sigstore_rekor_types.HashedrekordV001Schema( signature=sigstore_rekor_types.Signature1( - content=b64_artifact_signature, + content=base64.b64encode(artifact_signature).decode(), public_key=sigstore_rekor_types.PublicKey1( content=b64_cert.decode() ), @@ -255,11 +262,10 @@ def sign( logger.debug(f"Transparency log entry created with index: {entry.log_index}") return SigningResult( - input_digest=HexStr(input_digest.hex()), + content=content, cert_pem=PEMCert( cert.public_bytes(encoding=serialization.Encoding.PEM).decode() ), - b64_signature=B64Str(b64_artifact_signature), log_entry=entry, ) @@ -329,14 +335,16 @@ def signer( yield Signer(identity_token, self, cache) -class SigningResult(BaseModel): +@dataclass(kw_only=True) +class SigningResult: """ Represents the artifacts of a signing operation. """ - input_digest: HexStr + content: MessageSignature | Envelope """ - The hex-encoded SHA256 digest of the input that was signed for. + The signed-over content, either signature-and-digest pair or + as a DSSE envelope. """ cert_pem: PEMCert @@ -344,16 +352,21 @@ class SigningResult(BaseModel): The PEM-encoded public half of the certificate used for signing. """ - b64_signature: B64Str - """ - The base64-encoded signature. - """ - log_entry: LogEntry """ A record of the Rekor log entry for the signing operation. """ + @property + def b64_signature(self) -> B64Str: + """ + Returns the base64-encoded signature in this `SigningResult`. + """ + if isinstance(self.content, MessageSignature): + return B64Str(base64.b64encode(self.content.signature).decode()) + else: + return B64Str(base64.b64encode(self.content.signatures[0].sig).decode()) + def to_bundle(self) -> Bundle: """ Creates a Sigstore bundle (as defined by Sigstore's protobuf specs) @@ -406,13 +419,13 @@ def to_bundle(self) -> Bundle: bundle = Bundle( media_type="application/vnd.dev.sigstore.bundle+json;version=0.2", verification_material=material, - message_signature=MessageSignature( - message_digest=HashOutput( - algorithm=HashAlgorithm.SHA2_256, - digest=bytes.fromhex(self.input_digest), - ), - signature=base64.b64decode(self.b64_signature), - ), ) + if isinstance(self.content, MessageSignature): + bundle.message_signature = self.content + elif isinstance(self.content, Envelope): + bundle.dsse_envelope = self.content + else: + raise ValueError + return bundle From b3a60996ef6799772c05fc42ffe771d957971aff Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Thu, 7 Dec 2023 16:48:44 -0500 Subject: [PATCH 11/18] simplify condition Signed-off-by: William Woodruff --- sigstore/sign.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/sigstore/sign.py b/sigstore/sign.py index 856b1811..1257eeca 100644 --- a/sigstore/sign.py +++ b/sigstore/sign.py @@ -423,9 +423,7 @@ def to_bundle(self) -> Bundle: if isinstance(self.content, MessageSignature): bundle.message_signature = self.content - elif isinstance(self.content, Envelope): - bundle.dsse_envelope = self.content else: - raise ValueError + bundle.dsse_envelope = self.content return bundle From 09499f3abefc982d9c47db9763a6dd8948128770 Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Thu, 7 Dec 2023 16:50:15 -0500 Subject: [PATCH 12/18] sign: drop kw_only Not supported until 3.10+ Signed-off-by: William Woodruff --- sigstore/sign.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sigstore/sign.py b/sigstore/sign.py index 1257eeca..49d31c82 100644 --- a/sigstore/sign.py +++ b/sigstore/sign.py @@ -335,7 +335,7 @@ def signer( yield Signer(identity_token, self, cache) -@dataclass(kw_only=True) +@dataclass class SigningResult: """ Represents the artifacts of a signing operation. From cb99b93bd7e8525bc20eda5d45e1c546d8d9d33f Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Thu, 7 Dec 2023 16:59:43 -0500 Subject: [PATCH 13/18] sigstore: cleanup Signed-off-by: William Woodruff --- sigstore/_internal/rekor/client.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/sigstore/_internal/rekor/client.py b/sigstore/_internal/rekor/client.py index 7f2c74d2..b1961877 100644 --- a/sigstore/_internal/rekor/client.py +++ b/sigstore/_internal/rekor/client.py @@ -74,6 +74,9 @@ class RekorClientError(Exception): """ def __init__(self, http_error: requests.HTTPError): + """ + Create a new `RekorClientError` from the given `requests.HTTPError`. + """ if http_error.response: try: error = rekor_types.Error.model_validate_json(http_error.response.text) @@ -155,7 +158,7 @@ def post( """ payload = proposed_entry.model_dump(mode="json", by_alias=True) - logger.debug(f"PROPOSED ENTRY: {json.dumps(payload)}") + logger.debug(f"proposed: {json.dumps(payload)}") resp: requests.Response = self.session.post(self.url, json=payload) try: @@ -163,7 +166,9 @@ def post( except requests.HTTPError as http_error: raise RekorClientError(http_error) - return LogEntry._from_response(resp.json()) + integrated_entry = resp.json() + logger.debug(f"integrated: {integrated_entry}") + return LogEntry._from_response(integrated_entry) @property def retrieve(self) -> RekorEntriesRetrieve: From b24c9b77068ad2d35a2ebf2927bdcf93999c05cc Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Mon, 11 Dec 2023 15:36:00 -0500 Subject: [PATCH 14/18] firmly pin in-toto-attestation, fix KindVersion Signed-off-by: William Woodruff --- pyproject.toml | 2 +- sigstore/sign.py | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 21338df3..691da1fe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,7 @@ dependencies = [ "cryptography >= 39", "id >= 1.1.0", "importlib_resources ~= 5.7; python_version < '3.11'", - "in-toto-attestation ~= 0.9", + "in-toto-attestation == 0.9.2", "pydantic >= 2,< 3", "pyjwt >= 2.1", "pyOpenSSL >= 23.0.0", diff --git a/sigstore/sign.py b/sigstore/sign.py index 49d31c82..031670cb 100644 --- a/sigstore/sign.py +++ b/sigstore/sign.py @@ -357,6 +357,14 @@ class SigningResult: A record of the Rekor log entry for the signing operation. """ + @property + def _kind_version(self) -> KindVersion: + # TODO: This is kind of a hack. + if isinstance(self.content, MessageSignature): + return KindVersion(kind="hashedrekord", version="0.0.1") + else: + return KindVersion(kind="dsse", version="0.0.1") + @property def b64_signature(self) -> B64Str: """ @@ -398,7 +406,7 @@ def to_bundle(self) -> Bundle: tlog_entry = TransparencyLogEntry( log_index=self.log_entry.log_index, log_id=LogId(key_id=bytes.fromhex(self.log_entry.log_id)), - kind_version=KindVersion(kind="hashedrekord", version="0.0.1"), + kind_version=self._kind_version, integrated_time=self.log_entry.integrated_time, inclusion_promise=InclusionPromise( signed_entry_timestamp=base64.b64decode( From ee92e32d054f52fb4f2409ff087159575b7a236f Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Tue, 12 Dec 2023 23:25:26 -0500 Subject: [PATCH 15/18] bump sigstore-rekor-types Signed-off-by: William Woodruff --- pyproject.toml | 2 +- sigstore/_internal/rekor/client.py | 2 +- sigstore/sign.py | 28 ++++++++++++---------------- sigstore/verify/models.py | 18 ++++++++---------- 4 files changed, 22 insertions(+), 28 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 691da1fe..236d9e40 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,7 +37,7 @@ dependencies = [ "rich ~= 13.0", "securesystemslib", "sigstore-protobuf-specs ~= 0.2.2", - "sigstore-rekor-types >= 0.0.11", + "sigstore-rekor-types >= 0.0.12", "tuf >= 2.1,< 4.0", ] requires-python = ">=3.8" diff --git a/sigstore/_internal/rekor/client.py b/sigstore/_internal/rekor/client.py index b1961877..90706800 100644 --- a/sigstore/_internal/rekor/client.py +++ b/sigstore/_internal/rekor/client.py @@ -25,8 +25,8 @@ from typing import Any, Dict, NewType, Optional from urllib.parse import urljoin +import rekor_types import requests -import sigstore_rekor_types as rekor_types from sigstore._internal.ctfe import CTKeyring from sigstore._internal.keyring import Keyring diff --git a/sigstore/sign.py b/sigstore/sign.py index 031670cb..0ab9cec0 100644 --- a/sigstore/sign.py +++ b/sigstore/sign.py @@ -47,7 +47,7 @@ from typing import IO, Iterator, Optional import cryptography.x509 as x509 -import sigstore_rekor_types +import rekor_types from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import ec from cryptography.hazmat.primitives.asymmetric.utils import Prehashed @@ -206,16 +206,14 @@ def sign( # Sign artifact content: MessageSignature | Envelope - proposed_entry: sigstore_rekor_types.Hashedrekord | sigstore_rekor_types.Dsse + proposed_entry: rekor_types.Hashedrekord | rekor_types.Dsse if isinstance(input_, Statement): content = dsse.sign_intoto(private_key, input_) # Create the proposed DSSE entry - proposed_entry = sigstore_rekor_types.Dsse( - kind="dsse", - api_version="0.0.1", - spec=sigstore_rekor_types.DsseV001Schema( - proposed_content=sigstore_rekor_types.ProposedContent( + proposed_entry = rekor_types.Dsse( + spec=rekor_types.dsse.DsseV001Schema( + proposed_content=rekor_types.dsse.ProposedContent( envelope=content.to_json(), verifiers=[b64_cert.decode()], ), @@ -237,19 +235,17 @@ def sign( ) # Create the proposed hashedrekord entry - proposed_entry = sigstore_rekor_types.Hashedrekord( - kind="hashedrekord", - api_version="0.0.1", - spec=sigstore_rekor_types.HashedrekordV001Schema( - signature=sigstore_rekor_types.Signature1( + proposed_entry = rekor_types.Hashedrekord( + spec=rekor_types.hashedrekord.HashedrekordV001Schema( + signature=rekor_types.hashedrekord.Signature( content=base64.b64encode(artifact_signature).decode(), - public_key=sigstore_rekor_types.PublicKey1( + public_key=rekor_types.hashedrekord.PublicKey( content=b64_cert.decode() ), ), - data=sigstore_rekor_types.Data( - hash=sigstore_rekor_types.Hash( - algorithm=sigstore_rekor_types.Algorithm.SHA256, + data=rekor_types.hashedrekord.Data( + hash=rekor_types.hashedrekord.Hash( + algorithm=rekor_types.hashedrekord.Algorithm.SHA256, value=input_digest.hex(), ) ), diff --git a/sigstore/verify/models.py b/sigstore/verify/models.py index 53118ae4..3bb7d1ea 100644 --- a/sigstore/verify/models.py +++ b/sigstore/verify/models.py @@ -25,7 +25,7 @@ from textwrap import dedent from typing import IO -import sigstore_rekor_types +import rekor_types from cryptography.hazmat.primitives.serialization import Encoding from cryptography.x509 import ( Certificate, @@ -406,19 +406,17 @@ def rekor_entry(self, client: RekorClient) -> LogEntry: # This "expected" entry is used both to retrieve the Rekor entry # (if we don't have one) *and* to cross-check whatever response # we receive. See below. - expected_entry = sigstore_rekor_types.Hashedrekord( - kind="hashedrekord", - api_version="0.0.1", - spec=sigstore_rekor_types.HashedrekordV001Schema( - signature=sigstore_rekor_types.Signature1( + expected_entry = rekor_types.Hashedrekord( + spec=rekor_types.hashedrekord.HashedrekordV001Schema( + signature=rekor_types.hashedrekord.Signature( content=base64.b64encode(self.signature).decode(), - public_key=sigstore_rekor_types.PublicKey1( + public_key=rekor_types.hashedrekord.PublicKey( content=base64_encode_pem_cert(self.certificate) ), ), - data=sigstore_rekor_types.Data( - hash=sigstore_rekor_types.Hash( - algorithm=sigstore_rekor_types.Algorithm.SHA256, + data=rekor_types.hashedrekord.Data( + hash=rekor_types.hashedrekord.Hash( + algorithm=rekor_types.hashedrekord.Algorithm.SHA256, value=self.input_digest.hex(), ), ), From bdaef2da735fffe9d623e1a404f2e0cb46994fa0 Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Wed, 13 Dec 2023 16:06:19 -0500 Subject: [PATCH 16/18] pyproject: bump in-toto-attestation Signed-off-by: William Woodruff --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index e37da59d..f8eb9321 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,7 @@ dependencies = [ "cryptography >= 39", "id >= 1.1.0", "importlib_resources ~= 5.7; python_version < '3.11'", - "in-toto-attestation == 0.9.2", + "in-toto-attestation == 0.9.3", "pydantic >= 2,< 3", "pyjwt >= 2.1", "pyOpenSSL >= 23.0.0", From d087a39c875e434dd97a5bb4a7b9d26994289e89 Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Tue, 19 Dec 2023 14:36:08 -0500 Subject: [PATCH 17/18] remove testing script Signed-off-by: William Woodruff --- test_sign.py | 28 ---------------------------- 1 file changed, 28 deletions(-) delete mode 100755 test_sign.py diff --git a/test_sign.py b/test_sign.py deleted file mode 100755 index bba38e03..00000000 --- a/test_sign.py +++ /dev/null @@ -1,28 +0,0 @@ -#!/usr/bin/env python - -import logging - -from in_toto_attestation.v1.resource_descriptor import ResourceDescriptor -from in_toto_attestation.v1.statement import Statement - -from sigstore.oidc import Issuer -from sigstore.sign import SigningContext - -logging.getLogger().setLevel(logging.DEBUG) - -ctx = SigningContext.staging() -with ctx.signer(identity_token=Issuer.staging().identity_token()) as signer: - stmt = Statement( - subjects=[ - ResourceDescriptor( - name="null", - digest={ - "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" - }, - ).pb, - ], - predicate={"Data": "", "Timestamp": "2023-12-07T00:37:58Z"}, - predicate_type="https://cosign.sigstore.dev/attestation/v1", - ) - res = signer.sign(stmt) - print(res) From 0b23bc2cc6f16005c100789b922cc6d904377af2 Mon Sep 17 00:00:00 2001 From: William Woodruff Date: Tue, 19 Dec 2023 14:42:06 -0500 Subject: [PATCH 18/18] CHANGELOG: record changes Signed-off-by: William Woodruff --- CHANGELOG.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e20cafa4..3a60c5d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,25 @@ All versions prior to 0.9.0 are untracked. ## [Unreleased] +### Added + +* API: `Signer.sign()` can now take an in-toto `Statement` as an input, + producing a DSSE-formatted signature rather than a "bare" signature + ([#804](https://github.com/sigstore/sigstore-python/pull/804)) + + +* API: `SigningResult.content` has been added, representing either the + `hashedrekord` entry's message signature or the `dsse` entry's envelope + ([#804](https://github.com/sigstore/sigstore-python/pull/804)) + + +### Removed + +* API: `SigningResult.input_digest` has been removed; users who expect + to access the input digest may do so by inspecting the `hashedrekord` + or `dsse`-specific `SigningResult.content` + ([#804](https://github.com/sigstore/sigstore-python/pull/804)) + ## [2.1.0] ### Added