Skip to content

Commit 5d53e26

Browse files
authored
API: remove SigningResult (#862)
* remove SigningResult Signed-off-by: William Woodruff <william@trailofbits.com> * CHANGELOG: links Signed-off-by: William Woodruff <william@trailofbits.com> * test: annotations Signed-off-by: William Woodruff <william@trailofbits.com> * test_sign: begin fixing tests Signed-off-by: William Woodruff <william@trailofbits.com> * more test fixes Signed-off-by: William Woodruff <william@trailofbits.com> * test: b64 Signed-off-by: William Woodruff <william@trailofbits.com> * test: more munging Signed-off-by: William Woodruff <william@trailofbits.com> * sigstore: fixup `sign` Signed-off-by: William Woodruff <william@trailofbits.com> --------- Signed-off-by: William Woodruff <william@trailofbits.com>
1 parent e0168a7 commit 5d53e26

File tree

8 files changed

+98
-102
lines changed

8 files changed

+98
-102
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,13 @@ All versions prior to 0.9.0 are untracked.
88

99
## [Unreleased]
1010

11+
### Changed
12+
13+
* **BREAKING API CHANGE**: `sigstore.sign.SigningResult` has been removed
14+
([#862](https://github.com/sigstore/sigstore-python/pull/862))
15+
* **BREAKING API CHANGE**: The `Signer.sign(...)` API now returns a `Bundle`,
16+
instead of a `SigningResult` ([#862](https://github.com/sigstore/sigstore-python/pull/862))
17+
1118
## [2.1.0]
1219

1320
### Added

pyproject.toml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,7 @@ lint = [
6060
# and let Dependabot periodically perform this update.
6161
"ruff < 0.1.14",
6262
"types-requests",
63-
# TODO(ww): Re-enable once dependency on types-cryptography is dropped.
64-
# See: https://github.com/python/typeshed/issues/8699
65-
# "types-pyOpenSSL",
63+
"types-pyOpenSSL",
6664
]
6765
doc = ["pdoc"]
6866
dev = ["build", "bump >= 1.3.2", "sigstore[doc,test,lint]"]

sigstore/_cli.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
RekorKeyring,
4242
)
4343
from sigstore._internal.trustroot import TrustedRoot
44-
from sigstore._utils import PEMCert
44+
from sigstore._utils import PEMCert, cert_der_to_pem
4545
from sigstore.errors import Error
4646
from sigstore.oidc import (
4747
DEFAULT_OAUTH_ISSUER_URL,
@@ -698,10 +698,12 @@ def _sign(args: argparse.Namespace) -> None:
698698
raise exp_certificate
699699

700700
print("Using ephemeral certificate:")
701-
print(result.cert_pem)
701+
cert = result.verification_material.x509_certificate_chain.certificates[0]
702+
cert_pem = cert_der_to_pem(cert.raw_bytes)
703+
print(cert_pem)
702704

703705
print(
704-
f"Transparency log entry created at index: {result.log_entry.log_index}"
706+
f"Transparency log entry created at index: {result.verification_material.tlog_entries[0].log_index}"
705707
)
706708

707709
sig_output: TextIO
@@ -710,18 +712,19 @@ def _sign(args: argparse.Namespace) -> None:
710712
else:
711713
sig_output = sys.stdout
712714

713-
print(result.b64_signature, file=sig_output)
715+
signature = base64.b64encode(result.message_signature.signature).decode()
716+
print(signature, file=sig_output)
714717
if outputs["sig"] is not None:
715718
print(f"Signature written to {outputs['sig']}")
716719

717720
if outputs["cert"] is not None:
718721
with outputs["cert"].open(mode="w") as io:
719-
print(result.cert_pem, file=io)
722+
print(cert_pem, file=io)
720723
print(f"Certificate written to {outputs['cert']}")
721724

722725
if outputs["bundle"] is not None:
723726
with outputs["bundle"].open(mode="w") as io:
724-
print(result.to_bundle().to_json(), file=io)
727+
print(result.to_json(), file=io)
725728
print(f"Sigstore bundle written to {outputs['bundle']}")
726729

727730

sigstore/_utils.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,12 @@
2525

2626
from cryptography.hazmat.primitives import serialization
2727
from cryptography.hazmat.primitives.asymmetric import ec, rsa
28-
from cryptography.x509 import Certificate, ExtensionNotFound, Version
28+
from cryptography.x509 import (
29+
Certificate,
30+
ExtensionNotFound,
31+
Version,
32+
load_der_x509_certificate,
33+
)
2934
from cryptography.x509.oid import ExtendedKeyUsageOID, ExtensionOID
3035

3136
from sigstore.errors import Error
@@ -126,6 +131,19 @@ def base64_encode_pem_cert(cert: Certificate) -> B64Str:
126131
)
127132

128133

134+
def cert_der_to_pem(der: bytes) -> str:
135+
"""
136+
Converts a DER-encoded X.509 certificate into its PEM encoding.
137+
138+
Returns a string containing a PEM-encoded X.509 certificate.
139+
"""
140+
141+
# NOTE: Technically we don't have to round-trip like this, since
142+
# the DER-to-PEM transformation is entirely mechanical.
143+
cert = load_der_x509_certificate(der)
144+
return cert.public_bytes(serialization.Encoding.PEM).decode()
145+
146+
129147
def key_id(key: PublicKey) -> KeyID:
130148
"""
131149
Returns an RFC 6962-style "key ID" for the given public key.

sigstore/sign.py

Lines changed: 51 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,6 @@
5151
from cryptography.hazmat.primitives.asymmetric import ec
5252
from cryptography.hazmat.primitives.asymmetric.utils import Prehashed
5353
from cryptography.x509.oid import NameOID
54-
from pydantic import BaseModel
5554
from sigstore_protobuf_specs.dev.sigstore.bundle.v1 import (
5655
Bundle,
5756
VerificationMaterial,
@@ -175,7 +174,7 @@ def _signing_cert(
175174
def sign(
176175
self,
177176
input_: IO[bytes],
178-
) -> SigningResult:
177+
) -> Bundle:
179178
"""Public API for signing blobs"""
180179
input_digest = sha256_streaming(input_)
181180
private_key = self._private_key
@@ -233,7 +232,7 @@ def sign(
233232

234233
logger.debug(f"Transparency log entry created with index: {entry.log_index}")
235234

236-
return SigningResult(
235+
return _make_bundle(
237236
input_digest=HexStr(input_digest.hex()),
238237
cert_pem=PEMCert(
239238
cert.public_bytes(encoding=serialization.Encoding.PEM).decode()
@@ -308,90 +307,60 @@ def signer(
308307
yield Signer(identity_token, self, cache)
309308

310309

311-
class SigningResult(BaseModel):
310+
def _make_bundle(
311+
input_digest: HexStr, cert_pem: PEMCert, b64_signature: B64Str, log_entry: LogEntry
312+
) -> Bundle:
312313
"""
313-
Represents the artifacts of a signing operation.
314+
Convert the raw results of a Sigstore signing operation into a Sigstore bundle.
314315
"""
315316

316-
input_digest: HexStr
317-
"""
318-
The hex-encoded SHA256 digest of the input that was signed for.
319-
"""
320-
321-
cert_pem: PEMCert
322-
"""
323-
The PEM-encoded public half of the certificate used for signing.
324-
"""
325-
326-
b64_signature: B64Str
327-
"""
328-
The base64-encoded signature.
329-
"""
330-
331-
log_entry: LogEntry
332-
"""
333-
A record of the Rekor log entry for the signing operation.
334-
"""
335-
336-
def to_bundle(self) -> Bundle:
337-
"""
338-
Creates a Sigstore bundle (as defined by Sigstore's protobuf specs)
339-
from this `SigningResult`.
340-
"""
341-
342-
# NOTE: We explicitly only include the leaf certificate in the bundle's "chain"
343-
# here: the specs explicitly forbid the inclusion of the root certificate,
344-
# and discourage inclusion of any intermediates (since they're in the root of
345-
# trust already).
346-
cert = x509.load_pem_x509_certificate(self.cert_pem.encode())
347-
cert_der = cert.public_bytes(encoding=serialization.Encoding.DER)
348-
chain = X509CertificateChain(certificates=[X509Certificate(raw_bytes=cert_der)])
349-
350-
inclusion_proof: InclusionProof | None = None
351-
if self.log_entry.inclusion_proof is not None:
352-
inclusion_proof = InclusionProof(
353-
log_index=self.log_entry.inclusion_proof.log_index,
354-
root_hash=bytes.fromhex(self.log_entry.inclusion_proof.root_hash),
355-
tree_size=self.log_entry.inclusion_proof.tree_size,
356-
hashes=[
357-
bytes.fromhex(h) for h in self.log_entry.inclusion_proof.hashes
358-
],
359-
checkpoint=Checkpoint(
360-
envelope=self.log_entry.inclusion_proof.checkpoint
361-
),
362-
)
363-
364-
tlog_entry = TransparencyLogEntry(
365-
log_index=self.log_entry.log_index,
366-
log_id=LogId(key_id=bytes.fromhex(self.log_entry.log_id)),
367-
kind_version=KindVersion(kind="hashedrekord", version="0.0.1"),
368-
integrated_time=self.log_entry.integrated_time,
369-
inclusion_promise=InclusionPromise(
370-
signed_entry_timestamp=base64.b64decode(
371-
self.log_entry.inclusion_promise
372-
)
373-
)
374-
if self.log_entry.inclusion_promise
375-
else None,
376-
inclusion_proof=inclusion_proof,
377-
canonicalized_body=base64.b64decode(self.log_entry.body),
317+
# NOTE: We explicitly only include the leaf certificate in the bundle's "chain"
318+
# here: the specs explicitly forbid the inclusion of the root certificate,
319+
# and discourage inclusion of any intermediates (since they're in the root of
320+
# trust already).
321+
cert = x509.load_pem_x509_certificate(cert_pem.encode())
322+
cert_der = cert.public_bytes(encoding=serialization.Encoding.DER)
323+
chain = X509CertificateChain(certificates=[X509Certificate(raw_bytes=cert_der)])
324+
325+
inclusion_proof: InclusionProof | None = None
326+
if log_entry.inclusion_proof is not None:
327+
inclusion_proof = InclusionProof(
328+
log_index=log_entry.inclusion_proof.log_index,
329+
root_hash=bytes.fromhex(log_entry.inclusion_proof.root_hash),
330+
tree_size=log_entry.inclusion_proof.tree_size,
331+
hashes=[bytes.fromhex(h) for h in log_entry.inclusion_proof.hashes],
332+
checkpoint=Checkpoint(envelope=log_entry.inclusion_proof.checkpoint),
378333
)
379334

380-
material = VerificationMaterial(
381-
x509_certificate_chain=chain,
382-
tlog_entries=[tlog_entry],
335+
tlog_entry = TransparencyLogEntry(
336+
log_index=log_entry.log_index,
337+
log_id=LogId(key_id=bytes.fromhex(log_entry.log_id)),
338+
kind_version=KindVersion(kind="hashedrekord", version="0.0.1"),
339+
integrated_time=log_entry.integrated_time,
340+
inclusion_promise=InclusionPromise(
341+
signed_entry_timestamp=base64.b64decode(log_entry.inclusion_promise)
383342
)
384-
385-
bundle = Bundle(
386-
media_type="application/vnd.dev.sigstore.bundle+json;version=0.2",
387-
verification_material=material,
388-
message_signature=MessageSignature(
389-
message_digest=HashOutput(
390-
algorithm=HashAlgorithm.SHA2_256,
391-
digest=bytes.fromhex(self.input_digest),
392-
),
393-
signature=base64.b64decode(self.b64_signature),
343+
if log_entry.inclusion_promise
344+
else None,
345+
inclusion_proof=inclusion_proof,
346+
canonicalized_body=base64.b64decode(log_entry.body),
347+
)
348+
349+
material = VerificationMaterial(
350+
x509_certificate_chain=chain,
351+
tlog_entries=[tlog_entry],
352+
)
353+
354+
bundle = Bundle(
355+
media_type="application/vnd.dev.sigstore.bundle+json;version=0.2",
356+
verification_material=material,
357+
message_signature=MessageSignature(
358+
message_digest=HashOutput(
359+
algorithm=HashAlgorithm.SHA2_256,
360+
digest=bytes.fromhex(input_digest),
394361
),
395-
)
362+
signature=base64.b64decode(b64_signature),
363+
),
364+
)
396365

397-
return bundle
366+
return bundle

sigstore/verify/verifier.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
from cryptography.hazmat.primitives.asymmetric.utils import Prehashed
3030
from cryptography.x509 import Certificate, ExtendedKeyUsage, KeyUsage
3131
from cryptography.x509.oid import ExtendedKeyUsageOID
32-
from OpenSSL.crypto import ( # type: ignore[import-untyped]
32+
from OpenSSL.crypto import (
3333
X509,
3434
X509Store,
3535
X509StoreContext,

test/unit/conftest.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
from __future__ import annotations
16+
1517
import base64
1618
import os
1719
import re
@@ -238,7 +240,7 @@ def tuf_dirs(monkeypatch, tmp_path):
238240
],
239241
ids=["production", "staging"],
240242
)
241-
def id_config(request):
243+
def id_config(request) -> tuple[SigningContext, IdentityToken]:
242244
env, signer = request.param
243245
# Detect env variable for local interactive tests.
244246
token = os.getenv(f"SIGSTORE_IDENTITY_TOKEN_{env}")

test/unit/test_sign.py

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
import base64
1516
import io
1617
import secrets
1718

@@ -45,14 +46,13 @@ def test_sign_rekor_entry_consistent(id_config):
4546

4647
payload = io.BytesIO(secrets.token_bytes(32))
4748
with ctx.signer(identity) as signer:
48-
expected_entry = signer.sign(payload).log_entry
49+
expected_entry = signer.sign(payload).verification_material.tlog_entries[0]
4950

5051
actual_entry = ctx._rekor.log.entries.get(log_index=expected_entry.log_index)
5152

52-
assert expected_entry.uuid == actual_entry.uuid
53-
assert expected_entry.body == actual_entry.body
53+
assert expected_entry.canonicalized_body == base64.b64decode(actual_entry.body)
5454
assert expected_entry.integrated_time == actual_entry.integrated_time
55-
assert expected_entry.log_id == actual_entry.log_id
55+
assert expected_entry.log_id.key_id == bytes.fromhex(actual_entry.log_id)
5656
assert expected_entry.log_index == actual_entry.log_index
5757

5858

@@ -109,11 +109,10 @@ def test_identity_proof_claim_lookup(id_config, monkeypatch):
109109
payload = io.BytesIO(secrets.token_bytes(32))
110110

111111
with ctx.signer(identity) as signer:
112-
expected_entry = signer.sign(payload).log_entry
112+
expected_entry = signer.sign(payload).verification_material.tlog_entries[0]
113113
actual_entry = ctx._rekor.log.entries.get(log_index=expected_entry.log_index)
114114

115-
assert expected_entry.uuid == actual_entry.uuid
116-
assert expected_entry.body == actual_entry.body
115+
assert expected_entry.canonicalized_body == base64.b64decode(actual_entry.body)
117116
assert expected_entry.integrated_time == actual_entry.integrated_time
118-
assert expected_entry.log_id == actual_entry.log_id
117+
assert expected_entry.log_id.key_id == bytes.fromhex(actual_entry.log_id)
119118
assert expected_entry.log_index == actual_entry.log_index

0 commit comments

Comments
 (0)