Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

API-level DSSE signing support #804

Merged
merged 28 commits into from
Jan 17, 2024
Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
e2ce0eb
hackety hack
woodruffw Oct 13, 2023
2c873ff
Merge branch 'main' into ww/dsse
woodruffw Oct 18, 2023
476d527
hackety hack
woodruffw Oct 27, 2023
d3862c0
Merge branch 'main' into ww/dsse
woodruffw Dec 4, 2023
0da7215
Merge branch 'main' into ww/dsse
woodruffw Dec 6, 2023
f6899be
sigstore: hackety hack
woodruffw Dec 6, 2023
77526da
hackety hack
woodruffw Dec 6, 2023
63f9b29
hackety hack
woodruffw Dec 6, 2023
82a6fee
sigstore: don't double encode
woodruffw Dec 7, 2023
ff8d358
fixup DSSE signing, refactor RekorClientError
woodruffw Dec 7, 2023
470232c
sigstore: docs
woodruffw Dec 7, 2023
b46eac9
sigstore: lintage
woodruffw Dec 7, 2023
35a1e7f
make SigningResult generic over contents
woodruffw Dec 7, 2023
b3a6099
simplify condition
woodruffw Dec 7, 2023
09499f3
sign: drop kw_only
woodruffw Dec 7, 2023
cb99b93
sigstore: cleanup
woodruffw Dec 7, 2023
ea548a7
Merge branch 'main' into ww/dsse
woodruffw Dec 7, 2023
b24c9b7
firmly pin in-toto-attestation, fix KindVersion
woodruffw Dec 11, 2023
ffd3400
Merge branch 'main' into ww/dsse
woodruffw Dec 12, 2023
ee92e32
bump sigstore-rekor-types
woodruffw Dec 13, 2023
47ea07f
Merge branch 'main' into ww/dsse
woodruffw Dec 13, 2023
bdaef2d
pyproject: bump in-toto-attestation
woodruffw Dec 13, 2023
4728b32
Merge remote-tracking branch 'origin/main' into ww/dsse
woodruffw Dec 13, 2023
bbe07ac
Merge branch 'main' into ww/dsse
woodruffw Dec 19, 2023
d087a39
remove testing script
woodruffw Dec 19, 2023
0b23bc2
CHANGELOG: record changes
woodruffw Dec 19, 2023
ffcfa5b
Merge branch 'main' into ww/dsse
woodruffw Jan 9, 2024
86daf93
Merge branch 'main' into ww/dsse
woodruffw Jan 16, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 4 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,15 @@ dependencies = [
"cryptography >= 39",
"id >= 1.1.0",
"importlib_resources ~= 5.7; python_version < '3.11'",
"in-toto-attestation == 0.9.3",
"pydantic >= 2,< 3",
"pyjwt >= 2.1",
"pyOpenSSL >= 23.0.0",
"requests",
"rich ~= 13.0",
"securesystemslib",
"sigstore-protobuf-specs ~= 0.2.2",
# NOTE(ww): Under active development, so strictly pinned.
"sigstore-rekor-types == 0.0.12",
"tuf >= 2.1,< 4.0",
]
Expand All @@ -60,9 +62,8 @@ lint = [
# and let Dependabot periodically perform this update.
"ruff < 0.1.11",
"types-requests",
# TODO(ww): Re-enable once dependency on types-cryptography is dropped.
# See: https://github.com/python/typeshed/issues/8699
# "types-pyOpenSSL",
"types-protobuf",
"types-pyOpenSSL",
]
doc = ["pdoc"]
dev = ["build", "bump >= 1.3.2", "sigstore[doc,test,lint]"]
Expand Down
49 changes: 49 additions & 0 deletions sigstore/_internal/dsse.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# 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.
"""

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


def sign_intoto(key: ec.EllipticCurvePrivateKey, payload: Statement) -> 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_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()}"
)

signature = key.sign(pae.encode(), ec.ECDSA(hashes.SHA256()))
return Envelope(
payload=payload_encoded,
payload_type=type_,
signatures=[Signature(sig=signature, keyid=None)],
)
41 changes: 29 additions & 12 deletions sigstore/_internal/rekor/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

from __future__ import annotations

import json
import logging
from abc import ABC
from dataclasses import dataclass
Expand Down Expand Up @@ -72,7 +73,20 @@ class RekorClientError(Exception):
A generic error in the Rekor client.
"""

pass
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)
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):
Expand All @@ -94,7 +108,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
Expand All @@ -120,7 +134,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

Expand All @@ -132,26 +146,29 @@ 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(
self,
proposed_entry: rekor_types.Hashedrekord,
proposed_entry: rekor_types.Hashedrekord | rekor_types.Dsse,
) -> LogEntry:
"""
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(f"proposed: {json.dumps(payload)}")

resp: requests.Response = self.session.post(self.url, json=payload)
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())
integrated_entry = resp.json()
logger.debug(f"integrated: {integrated_entry}")
return LogEntry._from_response(integrated_entry)

@property
def retrieve(self) -> RekorEntriesRetrieve:
Expand All @@ -170,7 +187,7 @@ class RekorEntriesRetrieve(_Endpoint):

def post(
self,
expected_entry: rekor_types.Hashedrekord,
expected_entry: rekor_types.Hashedrekord | rekor_types.Dsse,
) -> Optional[LogEntry]:
"""
Retrieves an extant Rekor entry, identified by its artifact signature,
Expand All @@ -187,7 +204,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()

Expand Down
Loading