Skip to content

Commit d926803

Browse files
committed
CLI: Refactor terminal output
* Separate the thread method so it's easier to see potential safety issues * Using print() with the same file object is generally not thread safe: Avoid it from the threaded method The output remains effectively the same except: * The b64 encoded signature is no longer printed to terminal * Some print()s are now logger.info(): e.g. Transparency log entry created at index: 5562 * Other print()s happen in a batch now, after he signing has finished Signed-off-by: Jussi Kukkonen <[email protected]>
1 parent 064d24c commit d926803

File tree

1 file changed

+78
-62
lines changed

1 file changed

+78
-62
lines changed

sigstore/_cli.py

Lines changed: 78 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
from concurrent import futures
2424
from dataclasses import dataclass
2525
from pathlib import Path
26-
from typing import Any, NoReturn, TextIO, Union
26+
from typing import Any, NoReturn, Union
2727

2828
from cryptography.hazmat.primitives.serialization import Encoding
2929
from cryptography.x509 import load_pem_x509_certificate
@@ -57,7 +57,7 @@
5757
Issuer,
5858
detect_credential,
5959
)
60-
from sigstore.sign import SigningContext
60+
from sigstore.sign import Signer, SigningContext
6161
from sigstore.verify import (
6262
Verifier,
6363
policy,
@@ -637,6 +637,57 @@ def _get_identity_token(args: argparse.Namespace) -> None:
637637
_invalid_arguments(args, "No identity token supplied or detected!")
638638

639639

640+
def _sign_file_threaded(
641+
signer: Signer,
642+
predicate_type: str | None,
643+
predicate: dict[str, Any] | None,
644+
file: Path,
645+
outputs: SigningOutputs,
646+
) -> None:
647+
"""sign method to be called from signing thread"""
648+
_logger.debug(f"signing for {file.name}")
649+
with file.open(mode="rb") as io:
650+
# The input can be indefinitely large, so we perform a streaming
651+
# digest and sign the prehash rather than buffering it fully.
652+
digest = sha256_digest(io)
653+
try:
654+
if predicate is None:
655+
result = signer.sign_artifact(input_=digest)
656+
else:
657+
subject = Subject(name=file.name, digest={"sha256": digest.digest.hex()})
658+
statement_builder = StatementBuilder(
659+
subjects=[subject],
660+
predicate_type=predicate_type,
661+
predicate=predicate,
662+
)
663+
result = signer.sign_dsse(statement_builder.build())
664+
except ExpiredIdentity as exp_identity:
665+
_logger.error("Signature failed: identity token has expired")
666+
raise exp_identity
667+
668+
except ExpiredCertificate as exp_certificate:
669+
_logger.error("Signature failed: Fulcio signing certificate has expired")
670+
raise exp_certificate
671+
672+
_logger.info(
673+
f"Transparency log entry created at index: {result.log_entry.log_index}"
674+
)
675+
676+
if outputs.signature is not None:
677+
signature = base64.b64encode(result.signature).decode()
678+
with outputs.signature.open(mode="w") as io:
679+
print(signature, file=io)
680+
681+
if outputs.certificate is not None:
682+
cert_pem = signer._signing_cert().public_bytes(Encoding.PEM).decode()
683+
with outputs.certificate.open(mode="w") as io:
684+
print(cert_pem, file=io)
685+
686+
if outputs.bundle is not None:
687+
with outputs.bundle.open(mode="w") as io:
688+
print(result.to_json(), file=io)
689+
690+
640691
def _sign_common(
641692
args: argparse.Namespace, output_map: OutputMap, predicate: dict[str, Any] | None
642693
) -> None:
@@ -667,74 +718,39 @@ def _sign_common(
667718
if not identity:
668719
_invalid_arguments(args, "No identity token supplied or detected!")
669720

670-
with signing_ctx.signer(identity) as signer:
671-
with futures.ThreadPoolExecutor(max_workers=10) as executor:
672-
673-
def _sign_file(file: Path, outputs: SigningOutputs) -> None:
674-
_logger.debug(f"signing for {file.name}")
675-
with file.open(mode="rb") as io:
676-
# The input can be indefinitely large, so we perform a streaming
677-
# digest and sign the prehash rather than buffering it fully.
678-
digest = sha256_digest(io)
679-
try:
680-
if predicate is None:
681-
result = signer.sign_artifact(input_=digest)
682-
else:
683-
subject = Subject(
684-
name=file.name, digest={"sha256": digest.digest.hex()}
685-
)
686-
predicate_type = args.predicate_type
687-
statement_builder = StatementBuilder(
688-
subjects=[subject],
689-
predicate_type=predicate_type,
690-
predicate=predicate,
691-
)
692-
result = signer.sign_dsse(statement_builder.build())
693-
except ExpiredIdentity as exp_identity:
694-
print("Signature failed: identity token has expired")
695-
raise exp_identity
696-
697-
except ExpiredCertificate as exp_certificate:
698-
print("Signature failed: Fulcio signing certificate has expired")
699-
raise exp_certificate
700-
701-
print("Using ephemeral certificate:")
702-
cert = result.signing_certificate
703-
cert_pem = cert.public_bytes(Encoding.PEM).decode()
704-
print(cert_pem)
705-
706-
print(
707-
f"Transparency log entry created at index: {result.log_entry.log_index}"
708-
)
709-
710-
sig_output: TextIO
711-
if outputs.signature is not None:
712-
sig_output = outputs.signature.open("w")
713-
else:
714-
sig_output = sys.stdout
715-
716-
signature = base64.b64encode(result.signature).decode()
717-
print(signature, file=sig_output)
718-
if outputs.signature is not None:
719-
print(f"Signature written to {outputs.signature}")
721+
# Not all commands provide --predicate-type
722+
predicate_type = getattr(args, "predicate_type", None)
720723

721-
if outputs.certificate is not None:
722-
with outputs.certificate.open(mode="w") as io:
723-
print(cert_pem, file=io)
724-
print(f"Certificate written to {outputs.certificate}")
725-
726-
if outputs.bundle is not None:
727-
with outputs.bundle.open(mode="w") as io:
728-
print(result.to_json(), file=io)
729-
print(f"Sigstore bundle written to {outputs.bundle}")
724+
with signing_ctx.signer(identity) as signer:
725+
print("Using ephemeral certificate:")
726+
cert_pem = signer._signing_cert().public_bytes(Encoding.PEM).decode()
727+
print(cert_pem)
730728

729+
# sign in threads: this is relevant for especially Rekor v2 as otherwise we wait
730+
# for log inclusion for each signature separately
731+
with futures.ThreadPoolExecutor(max_workers=10) as executor:
731732
jobs = [
732-
executor.submit(_sign_file, file, outputs)
733+
executor.submit(
734+
_sign_file_threaded,
735+
signer,
736+
predicate_type,
737+
predicate,
738+
file,
739+
outputs,
740+
)
733741
for file, outputs in output_map.items()
734742
]
735743
for job in futures.as_completed(jobs):
736744
job.result()
737745

746+
for file, outputs in output_map.items():
747+
if outputs.signature is not None:
748+
print(f"Signature written to {outputs.signature}")
749+
if outputs.certificate is not None:
750+
print(f"Certificate written to {outputs.certificate}")
751+
if outputs.bundle is not None:
752+
print(f"Sigstore bundle written to {outputs.bundle}")
753+
738754

739755
def _attest(args: argparse.Namespace) -> None:
740756
predicate_path = args.predicate

0 commit comments

Comments
 (0)