Skip to content
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
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
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,14 @@ All versions prior to 0.9.0 are untracked.

### Added

* API: `Signer.sign()` can now take a `Hashed` as an input,
performing a signature on a pre-computed hash value
([#860](https://github.com/sigstore/sigstore-python/pull/860))

* 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))
Expand Down
16 changes: 14 additions & 2 deletions sigstore/_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -686,9 +686,21 @@ def _sign(args: argparse.Namespace) -> None:
with signing_ctx.signer(identity) as signer:
for file, outputs in output_map.items():
logger.debug(f"signing for {file.name}")
with file.open(mode="rb", buffering=0) as io:
with file.open(mode="rb", buffering=0) as iof:
try:
result = signer.sign(input_=io)
import hashlib
import io
from sigstore import hashes as sigstore_hashes
from sigstore_protobuf_specs.dev.sigstore.common.v1 import HashAlgorithm
Comment thread
laurentsimon marked this conversation as resolved.
Outdated

hash = hashlib.sha256()
hash.update(iof.read())
digest = hash.digest()
print(digest.hex())
hashed = sigstore_hashes.Hashed(digest=digest, algorithm=HashAlgorithm.SHA2_256)
result = signer.sign(hashed)

#result = signer.sign(input_=io)
Comment thread
laurentsimon marked this conversation as resolved.
Outdated
except ExpiredIdentity as exp_identity:
print("Signature failed: identity token has expired")
raise exp_identity
Expand Down
11 changes: 11 additions & 0 deletions sigstore/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@
load_der_x509_certificate,
)
from cryptography.x509.oid import ExtendedKeyUsageOID, ExtensionOID
from sigstore_protobuf_specs.dev.sigstore.common.v1 import HashAlgorithm

from sigstore import hashes as sigstore_hashes
from sigstore.errors import Error

if sys.version_info < (3, 11):
Expand Down Expand Up @@ -158,6 +160,15 @@ def key_id(key: PublicKey) -> KeyID:
return KeyID(hashlib.sha256(public_bytes).digest())


def get_digest(input_: IO[bytes] | sigstore_hashes.Hashed) -> sigstore_hashes.Hashed:
if isinstance(input_, sigstore_hashes.Hashed):
return input_

return sigstore_hashes.Hashed(
digest=sha256_streaming(input_), algorithm=HashAlgorithm.SHA2_256
)


def sha256_streaming(io: IO[bytes]) -> bytes:
"""
Compute the SHA256 of a stream.
Expand Down
46 changes: 46 additions & 0 deletions sigstore/hashes.py
Comment thread
laurentsimon marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Copyright 2023 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.

from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric.utils import Prehashed
from pydantic import BaseModel
import rekor_types
from sigstore_protobuf_specs.dev.sigstore.common.v1 import HashAlgorithm


class Hashed(BaseModel):
"""
Represents a hashed value.
"""

algorithm: HashAlgorithm
"""
The digest algorithm uses to compute the digest.
"""

digest: bytes
"""
The digest representing the hash value.
"""

def _as_hashedrekord_algorithm(self) -> rekor_types.hashedrekord.Algorithm:
if self.algorithm == HashAlgorithm.SHA2_256:
return rekor_types.hashedrekord.Algorithm.SHA256
raise ValueError(f"unknown hash algorithm: {self.algorithm}")

def _as_prehashed(self) -> Prehashed:
if self.algorithm == HashAlgorithm.SHA2_256:
return Prehashed(hashes.SHA256())
raise ValueError(f"unknown hash algorithm: {self.algorithm}")

19 changes: 9 additions & 10 deletions sigstore/sign.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,15 +49,13 @@
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
from cryptography.x509.oid import NameOID
from in_toto_attestation.v1.statement import Statement
from sigstore_protobuf_specs.dev.sigstore.bundle.v1 import (
Bundle,
VerificationMaterial,
)
from sigstore_protobuf_specs.dev.sigstore.common.v1 import (
HashAlgorithm,
HashOutput,
LogId,
MessageSignature,
Expand All @@ -73,6 +71,7 @@
)
from sigstore_protobuf_specs.io.intoto import Envelope

from sigstore import hashes as sigstore_hashes
from sigstore._internal import dsse
from sigstore._internal.fulcio import (
ExpiredCertificate,
Expand All @@ -82,7 +81,7 @@
from sigstore._internal.rekor.client import RekorClient
from sigstore._internal.sct import verify_sct
from sigstore._internal.trustroot import TrustedRoot
from sigstore._utils import PEMCert, sha256_streaming
from sigstore._utils import PEMCert, get_digest
from sigstore.oidc import ExpiredIdentity, IdentityToken
from sigstore.transparency import LogEntry

Expand Down Expand Up @@ -176,7 +175,7 @@ def _signing_cert(

def sign(
self,
input_: IO[bytes] | Statement,
input_: IO[bytes] | Statement | sigstore_hashes.Hashed,
) -> Bundle:
"""Public API for signing blobs"""
private_key = self._private_key
Expand Down Expand Up @@ -219,16 +218,16 @@ def sign(
),
)
else:
input_digest = sha256_streaming(input_)
hashed_input = get_digest(input_)

artifact_signature = private_key.sign(
input_digest, ec.ECDSA(Prehashed(hashes.SHA256()))
hashed_input.digest, ec.ECDSA(hashed_input._as_prehashed())
)

content = MessageSignature(
message_digest=HashOutput(
algorithm=HashAlgorithm.SHA2_256,
digest=input_digest,
algorithm=hashed_input.algorithm,
digest=hashed_input.digest,
),
signature=artifact_signature,
)
Expand All @@ -244,8 +243,8 @@ def sign(
),
data=rekor_types.hashedrekord.Data(
hash=rekor_types.hashedrekord.Hash(
algorithm=rekor_types.hashedrekord.Algorithm.SHA256,
value=input_digest.hex(),
algorithm=hashed_input._as_hashedrekord_algorithm(),
value=hashed_input.digest.hex(),
)
),
),
Expand Down
20 changes: 10 additions & 10 deletions sigstore/verify/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@
VerificationMaterial,
)
from sigstore_protobuf_specs.dev.sigstore.common.v1 import (
HashAlgorithm,
HashOutput,
LogId,
MessageSignature,
Expand All @@ -54,14 +53,15 @@
TransparencyLogEntry,
)

from sigstore import hashes as sigstore_hashes
from sigstore._internal.rekor import RekorClient
from sigstore._utils import (
B64Str,
PEMCert,
base64_encode_pem_cert,
cert_is_leaf,
cert_is_root_ca,
sha256_streaming,
get_digest,
)
from sigstore.errors import Error
from sigstore.transparency import LogEntry, LogInclusionProof
Expand Down Expand Up @@ -179,9 +179,9 @@ class VerificationMaterials:
Represents the materials needed to perform a Sigstore verification.
"""

input_digest: bytes
hashed_input: sigstore_hashes.Hashed
Comment thread
laurentsimon marked this conversation as resolved.
"""
The SHA256 hash of the verification input, as raw bytes.
The hash of the verification input.
"""

certificate: Certificate
Expand Down Expand Up @@ -227,7 +227,7 @@ class VerificationMaterials:
def __init__(
self,
*,
input_: IO[bytes],
input_: IO[bytes] | sigstore_hashes.Hashed,
cert_pem: PEMCert,
signature: bytes,
offline: bool = False,
Expand All @@ -246,7 +246,7 @@ def __init__(
Effect: `input_` is consumed as part of construction.
"""

self.input_digest = sha256_streaming(input_)
self.hashed_input = get_digest(input_)
self.certificate = load_pem_x509_certificate(cert_pem.encode())
self.signature = signature

Expand Down Expand Up @@ -416,8 +416,8 @@ def rekor_entry(self, client: RekorClient) -> LogEntry:
),
data=rekor_types.hashedrekord.Data(
hash=rekor_types.hashedrekord.Hash(
algorithm=rekor_types.hashedrekord.Algorithm.SHA256,
value=self.input_digest.hex(),
algorithm=self.hashed_input._as_hashedrekord_algorithm(),
value=self.hashed_input.digest.hex(),
),
),
),
Expand Down Expand Up @@ -510,8 +510,8 @@ def to_bundle(self) -> Bundle:
),
message_signature=MessageSignature(
message_digest=HashOutput(
algorithm=HashAlgorithm.SHA2_256,
digest=self.input_digest,
algorithm=self.hashed_input.algorithm,
digest=self.hashed_input.digest,
),
signature=self.signature,
),
Expand Down
8 changes: 3 additions & 5 deletions sigstore/verify/verifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,7 @@
from typing import List, cast

from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import ec
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 (
Expand Down Expand Up @@ -225,8 +223,8 @@ def verify(
signing_key = cast(ec.EllipticCurvePublicKey, signing_key)
signing_key.verify(
materials.signature,
materials.input_digest,
ec.ECDSA(Prehashed(hashes.SHA256())),
materials.hashed_input.digest,
ec.ECDSA(materials.hashed_input._as_prehashed()),
)
except InvalidSignature:
return VerificationFailure(reason="Signature is invalid for input")
Expand All @@ -241,7 +239,7 @@ def verify(
except RekorEntryMissingError:
return LogEntryMissing(
signature=B64Str(base64.b64encode(materials.signature).decode()),
artifact_hash=HexStr(materials.input_digest.hex()),
artifact_hash=HexStr(materials.hashed_input.digest.hex()),
)
except InvalidRekorEntryError:
return VerificationFailure(
Expand Down