Skip to content

Commit b3fdb31

Browse files
feat: Enable signing with pre-computed hash (#860)
Added prehashing support to Signer.sign(): caller can now precompute the hash and provide a Hashed object instead of plain input bytes. API changes: * Changed sign.Signer.sign() backwards-compatibly * Added hashes module The algorithm given to rekor remains unchanged (but capability of providing other algorithms in the future now exists). Signed-off-by: laurentsimon <laurentsimon@google.com> Signed-off-by: William Woodruff <william@trailofbits.com> Co-authored-by: William Woodruff <william@trailofbits.com>
1 parent bb9b1a0 commit b3fdb31

File tree

8 files changed

+153
-36
lines changed

8 files changed

+153
-36
lines changed

CHANGELOG.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,14 @@ All versions prior to 0.9.0 are untracked.
1010

1111
### Added
1212

13+
* API: `Signer.sign()` can now take a `Hashed` as an input,
14+
performing a signature on a pre-computed hash value
15+
([#860](https://github.com/sigstore/sigstore-python/pull/860))
16+
1317
* API: `Signer.sign()` can now take an in-toto `Statement` as an input,
1418
producing a DSSE-formatted signature rather than a "bare" signature
1519
([#804](https://github.com/sigstore/sigstore-python/pull/804))
1620

17-
1821
* API: `SigningResult.content` has been added, representing either the
1922
`hashedrekord` entry's message signature or the `dsse` entry's envelope
2023
([#804](https://github.com/sigstore/sigstore-python/pull/804))

sigstore/_utils.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@
3232
load_der_x509_certificate,
3333
)
3434
from cryptography.x509.oid import ExtendedKeyUsageOID, ExtensionOID
35+
from sigstore_protobuf_specs.dev.sigstore.common.v1 import HashAlgorithm
3536

37+
from sigstore import hashes as sigstore_hashes
3638
from sigstore.errors import Error
3739

3840
if sys.version_info < (3, 11):
@@ -158,6 +160,19 @@ def key_id(key: PublicKey) -> KeyID:
158160
return KeyID(hashlib.sha256(public_bytes).digest())
159161

160162

163+
def get_digest(input_: IO[bytes] | sigstore_hashes.Hashed) -> sigstore_hashes.Hashed:
164+
"""
165+
Compute the SHA256 digest of an input stream or, if given a `Hashed`,
166+
return it directly.
167+
"""
168+
if isinstance(input_, sigstore_hashes.Hashed):
169+
return input_
170+
171+
return sigstore_hashes.Hashed(
172+
digest=sha256_streaming(input_), algorithm=HashAlgorithm.SHA2_256
173+
)
174+
175+
161176
def sha256_streaming(io: IO[bytes]) -> bytes:
162177
"""
163178
Compute the SHA256 of a stream.

sigstore/hashes.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Copyright 2023 The Sigstore Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""
16+
Hashing APIs.
17+
"""
18+
19+
import rekor_types
20+
from cryptography.hazmat.primitives import hashes
21+
from cryptography.hazmat.primitives.asymmetric.utils import Prehashed
22+
from pydantic import BaseModel
23+
from sigstore_protobuf_specs.dev.sigstore.common.v1 import HashAlgorithm
24+
25+
26+
class Hashed(BaseModel):
27+
"""
28+
Represents a hashed value.
29+
"""
30+
31+
algorithm: HashAlgorithm
32+
"""
33+
The digest algorithm uses to compute the digest.
34+
"""
35+
36+
digest: bytes
37+
"""
38+
The digest representing the hash value.
39+
"""
40+
41+
def _as_hashedrekord_algorithm(self) -> rekor_types.hashedrekord.Algorithm:
42+
"""
43+
Returns an appropriate `hashedrekord.Algorithm` for this `Hashed`.
44+
"""
45+
if self.algorithm == HashAlgorithm.SHA2_256:
46+
return rekor_types.hashedrekord.Algorithm.SHA256
47+
raise ValueError(f"unknown hash algorithm: {self.algorithm}")
48+
49+
def _as_prehashed(self) -> Prehashed:
50+
"""
51+
Returns an appropriate Cryptography `Prehashed` for this `Hashed`.
52+
"""
53+
if self.algorithm == HashAlgorithm.SHA2_256:
54+
return Prehashed(hashes.SHA256())
55+
raise ValueError(f"unknown hash algorithm: {self.algorithm}")

sigstore/sign.py

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -49,15 +49,13 @@
4949
import rekor_types
5050
from cryptography.hazmat.primitives import hashes, serialization
5151
from cryptography.hazmat.primitives.asymmetric import ec
52-
from cryptography.hazmat.primitives.asymmetric.utils import Prehashed
5352
from cryptography.x509.oid import NameOID
5453
from in_toto_attestation.v1.statement import Statement
5554
from sigstore_protobuf_specs.dev.sigstore.bundle.v1 import (
5655
Bundle,
5756
VerificationMaterial,
5857
)
5958
from sigstore_protobuf_specs.dev.sigstore.common.v1 import (
60-
HashAlgorithm,
6159
HashOutput,
6260
LogId,
6361
MessageSignature,
@@ -73,6 +71,7 @@
7371
)
7472
from sigstore_protobuf_specs.io.intoto import Envelope
7573

74+
from sigstore import hashes as sigstore_hashes
7675
from sigstore._internal import dsse
7776
from sigstore._internal.fulcio import (
7877
ExpiredCertificate,
@@ -82,7 +81,7 @@
8281
from sigstore._internal.rekor.client import RekorClient
8382
from sigstore._internal.sct import verify_sct
8483
from sigstore._internal.trustroot import TrustedRoot
85-
from sigstore._utils import PEMCert, sha256_streaming
84+
from sigstore._utils import PEMCert, get_digest
8685
from sigstore.oidc import ExpiredIdentity, IdentityToken
8786
from sigstore.transparency import LogEntry
8887

@@ -176,7 +175,7 @@ def _signing_cert(
176175

177176
def sign(
178177
self,
179-
input_: IO[bytes] | Statement,
178+
input_: IO[bytes] | Statement | sigstore_hashes.Hashed,
180179
) -> Bundle:
181180
"""Public API for signing blobs"""
182181
private_key = self._private_key
@@ -219,16 +218,16 @@ def sign(
219218
),
220219
)
221220
else:
222-
input_digest = sha256_streaming(input_)
221+
hashed_input = get_digest(input_)
223222

224223
artifact_signature = private_key.sign(
225-
input_digest, ec.ECDSA(Prehashed(hashes.SHA256()))
224+
hashed_input.digest, ec.ECDSA(hashed_input._as_prehashed())
226225
)
227226

228227
content = MessageSignature(
229228
message_digest=HashOutput(
230-
algorithm=HashAlgorithm.SHA2_256,
231-
digest=input_digest,
229+
algorithm=hashed_input.algorithm,
230+
digest=hashed_input.digest,
232231
),
233232
signature=artifact_signature,
234233
)
@@ -244,8 +243,8 @@ def sign(
244243
),
245244
data=rekor_types.hashedrekord.Data(
246245
hash=rekor_types.hashedrekord.Hash(
247-
algorithm=rekor_types.hashedrekord.Algorithm.SHA256,
248-
value=input_digest.hex(),
246+
algorithm=hashed_input._as_hashedrekord_algorithm(),
247+
value=hashed_input.digest.hex(),
249248
)
250249
),
251250
),

sigstore/verify/models.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@
3838
VerificationMaterial,
3939
)
4040
from sigstore_protobuf_specs.dev.sigstore.common.v1 import (
41-
HashAlgorithm,
4241
HashOutput,
4342
LogId,
4443
MessageSignature,
@@ -54,14 +53,15 @@
5453
TransparencyLogEntry,
5554
)
5655

56+
from sigstore import hashes as sigstore_hashes
5757
from sigstore._internal.rekor import RekorClient
5858
from sigstore._utils import (
5959
B64Str,
6060
PEMCert,
6161
base64_encode_pem_cert,
6262
cert_is_leaf,
6363
cert_is_root_ca,
64-
sha256_streaming,
64+
get_digest,
6565
)
6666
from sigstore.errors import Error
6767
from sigstore.transparency import LogEntry, LogInclusionProof
@@ -179,9 +179,9 @@ class VerificationMaterials:
179179
Represents the materials needed to perform a Sigstore verification.
180180
"""
181181

182-
input_digest: bytes
182+
hashed_input: sigstore_hashes.Hashed
183183
"""
184-
The SHA256 hash of the verification input, as raw bytes.
184+
The hash of the verification input.
185185
"""
186186

187187
certificate: Certificate
@@ -227,7 +227,7 @@ class VerificationMaterials:
227227
def __init__(
228228
self,
229229
*,
230-
input_: IO[bytes],
230+
input_: IO[bytes] | sigstore_hashes.Hashed,
231231
cert_pem: PEMCert,
232232
signature: bytes,
233233
offline: bool = False,
@@ -246,7 +246,7 @@ def __init__(
246246
Effect: `input_` is consumed as part of construction.
247247
"""
248248

249-
self.input_digest = sha256_streaming(input_)
249+
self.hashed_input = get_digest(input_)
250250
self.certificate = load_pem_x509_certificate(cert_pem.encode())
251251
self.signature = signature
252252

@@ -416,8 +416,8 @@ def rekor_entry(self, client: RekorClient) -> LogEntry:
416416
),
417417
data=rekor_types.hashedrekord.Data(
418418
hash=rekor_types.hashedrekord.Hash(
419-
algorithm=rekor_types.hashedrekord.Algorithm.SHA256,
420-
value=self.input_digest.hex(),
419+
algorithm=self.hashed_input._as_hashedrekord_algorithm(),
420+
value=self.hashed_input.digest.hex(),
421421
),
422422
),
423423
),
@@ -510,8 +510,8 @@ def to_bundle(self) -> Bundle:
510510
),
511511
message_signature=MessageSignature(
512512
message_digest=HashOutput(
513-
algorithm=HashAlgorithm.SHA2_256,
514-
digest=self.input_digest,
513+
algorithm=self.hashed_input.algorithm,
514+
digest=self.hashed_input.digest,
515515
),
516516
signature=self.signature,
517517
),

sigstore/verify/verifier.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,7 @@
2424
from typing import List, cast
2525

2626
from cryptography.exceptions import InvalidSignature
27-
from cryptography.hazmat.primitives import hashes
2827
from cryptography.hazmat.primitives.asymmetric import ec
29-
from cryptography.hazmat.primitives.asymmetric.utils import Prehashed
3028
from cryptography.x509 import Certificate, ExtendedKeyUsage, KeyUsage
3129
from cryptography.x509.oid import ExtendedKeyUsageOID
3230
from OpenSSL.crypto import (
@@ -225,8 +223,8 @@ def verify(
225223
signing_key = cast(ec.EllipticCurvePublicKey, signing_key)
226224
signing_key.verify(
227225
materials.signature,
228-
materials.input_digest,
229-
ec.ECDSA(Prehashed(hashes.SHA256())),
226+
materials.hashed_input.digest,
227+
ec.ECDSA(materials.hashed_input._as_prehashed()),
230228
)
231229
except InvalidSignature:
232230
return VerificationFailure(reason="Signature is invalid for input")
@@ -241,7 +239,7 @@ def verify(
241239
except RekorEntryMissingError:
242240
return LogEntryMissing(
243241
signature=B64Str(base64.b64encode(materials.signature).decode()),
244-
artifact_hash=HexStr(materials.input_digest.hex()),
242+
artifact_hash=HexStr(materials.hashed_input.digest.hex()),
245243
)
246244
except InvalidRekorEntryError:
247245
return VerificationFailure(

test/unit/conftest.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
from sigstore.sign import SigningContext
4141
from sigstore.verify import VerificationMaterials
4242
from sigstore.verify.policy import VerificationSuccess
43+
from sigstore.verify.verifier import Verifier
4344

4445
_ASSETS = (Path(__file__).parent / "assets").resolve()
4546
assert _ASSETS.is_dir()
@@ -50,7 +51,9 @@
5051

5152
def _has_oidc_id():
5253
# If there are tokens manually defined for us in the environment, use them.
53-
if os.getenv("SIGSTORE_IDENTITY_TOKEN_production") is not None:
54+
if os.getenv("SIGSTORE_IDENTITY_TOKEN_production") or os.getenv(
55+
"SIGSTORE_IDENTITY_TOKEN_staging"
56+
):
5457
return True
5558

5659
try:
@@ -240,7 +243,7 @@ def tuf_dirs(monkeypatch, tmp_path):
240243
],
241244
ids=["production", "staging"],
242245
)
243-
def id_config(request) -> tuple[SigningContext, IdentityToken]:
246+
def signer_and_ident(request) -> tuple[type[SigningContext], type[IdentityToken]]:
244247
env, signer = request.param
245248
# Detect env variable for local interactive tests.
246249
token = os.getenv(f"SIGSTORE_IDENTITY_TOKEN_{env}")
@@ -251,6 +254,20 @@ def id_config(request) -> tuple[SigningContext, IdentityToken]:
251254
return signer, IdentityToken(token)
252255

253256

257+
@pytest.fixture
258+
def staging() -> tuple[type[SigningContext], type[Verifier], IdentityToken]:
259+
signer = SigningContext.staging
260+
verifier = Verifier.staging
261+
262+
# Detect env variable for local interactive tests.
263+
token = os.getenv("SIGSTORE_IDENTITY_TOKEN_staging")
264+
if not token:
265+
# If the variable is not defined, try getting an ambient token.
266+
token = detect_credential(_DEFAULT_AUDIENCE)
267+
268+
return signer, verifier, IdentityToken(token)
269+
270+
254271
@pytest.fixture
255272
def dummy_jwt():
256273
def _dummy_jwt(claims: dict):

0 commit comments

Comments
 (0)