|
23 | 23 | from concurrent import futures
|
24 | 24 | from dataclasses import dataclass
|
25 | 25 | from pathlib import Path
|
26 |
| -from typing import Any, NoReturn, TextIO, Union |
| 26 | +from typing import Any, NoReturn, Union |
27 | 27 |
|
28 | 28 | from cryptography.hazmat.primitives.serialization import Encoding
|
29 | 29 | from cryptography.x509 import load_pem_x509_certificate
|
|
57 | 57 | Issuer,
|
58 | 58 | detect_credential,
|
59 | 59 | )
|
60 |
| -from sigstore.sign import SigningContext |
| 60 | +from sigstore.sign import Signer, SigningContext |
61 | 61 | from sigstore.verify import (
|
62 | 62 | Verifier,
|
63 | 63 | policy,
|
@@ -637,6 +637,57 @@ def _get_identity_token(args: argparse.Namespace) -> None:
|
637 | 637 | _invalid_arguments(args, "No identity token supplied or detected!")
|
638 | 638 |
|
639 | 639 |
|
| 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 | + |
640 | 691 | def _sign_common(
|
641 | 692 | args: argparse.Namespace, output_map: OutputMap, predicate: dict[str, Any] | None
|
642 | 693 | ) -> None:
|
@@ -667,74 +718,39 @@ def _sign_common(
|
667 | 718 | if not identity:
|
668 | 719 | _invalid_arguments(args, "No identity token supplied or detected!")
|
669 | 720 |
|
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) |
720 | 723 |
|
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) |
730 | 728 |
|
| 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: |
731 | 732 | 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 | + ) |
733 | 741 | for file, outputs in output_map.items()
|
734 | 742 | ]
|
735 | 743 | for job in futures.as_completed(jobs):
|
736 | 744 | job.result()
|
737 | 745 |
|
| 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 | + |
738 | 754 |
|
739 | 755 | def _attest(args: argparse.Namespace) -> None:
|
740 | 756 | predicate_path = args.predicate
|
|
0 commit comments