|
20 | 20 | import logging
|
21 | 21 | import os
|
22 | 22 | import sys
|
| 23 | +from concurrent import futures |
23 | 24 | from dataclasses import dataclass
|
24 | 25 | from pathlib import Path
|
25 |
| -from typing import Any, NoReturn, TextIO, Union |
| 26 | +from typing import Any, NoReturn, Union |
26 | 27 |
|
27 | 28 | from cryptography.hazmat.primitives.serialization import Encoding
|
28 | 29 | from cryptography.x509 import load_pem_x509_certificate
|
|
56 | 57 | Issuer,
|
57 | 58 | detect_credential,
|
58 | 59 | )
|
59 |
| -from sigstore.sign import SigningContext |
| 60 | +from sigstore.sign import Signer, SigningContext |
60 | 61 | from sigstore.verify import (
|
61 | 62 | Verifier,
|
62 | 63 | policy,
|
@@ -636,6 +637,57 @@ def _get_identity_token(args: argparse.Namespace) -> None:
|
636 | 637 | _invalid_arguments(args, "No identity token supplied or detected!")
|
637 | 638 |
|
638 | 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 | + |
639 | 691 | def _sign_common(
|
640 | 692 | args: argparse.Namespace, output_map: OutputMap, predicate: dict[str, Any] | None
|
641 | 693 | ) -> None:
|
@@ -666,63 +718,37 @@ def _sign_common(
|
666 | 718 | if not identity:
|
667 | 719 | _invalid_arguments(args, "No identity token supplied or detected!")
|
668 | 720 |
|
669 |
| - with signing_ctx.signer(identity) as signer: |
670 |
| - for file, outputs in output_map.items(): |
671 |
| - _logger.debug(f"signing for {file.name}") |
672 |
| - with file.open(mode="rb") as io: |
673 |
| - # The input can be indefinitely large, so we perform a streaming |
674 |
| - # digest and sign the prehash rather than buffering it fully. |
675 |
| - digest = sha256_digest(io) |
676 |
| - try: |
677 |
| - if predicate is None: |
678 |
| - result = signer.sign_artifact(input_=digest) |
679 |
| - else: |
680 |
| - subject = Subject( |
681 |
| - name=file.name, digest={"sha256": digest.digest.hex()} |
682 |
| - ) |
683 |
| - predicate_type = args.predicate_type |
684 |
| - statement_builder = StatementBuilder( |
685 |
| - subjects=[subject], |
686 |
| - predicate_type=predicate_type, |
687 |
| - predicate=predicate, |
688 |
| - ) |
689 |
| - result = signer.sign_dsse(statement_builder.build()) |
690 |
| - except ExpiredIdentity as exp_identity: |
691 |
| - print("Signature failed: identity token has expired") |
692 |
| - raise exp_identity |
693 |
| - |
694 |
| - except ExpiredCertificate as exp_certificate: |
695 |
| - print("Signature failed: Fulcio signing certificate has expired") |
696 |
| - raise exp_certificate |
697 |
| - |
698 |
| - print("Using ephemeral certificate:") |
699 |
| - cert = result.signing_certificate |
700 |
| - cert_pem = cert.public_bytes(Encoding.PEM).decode() |
701 |
| - print(cert_pem) |
702 |
| - |
703 |
| - print( |
704 |
| - f"Transparency log entry created at index: {result.log_entry.log_index}" |
705 |
| - ) |
| 721 | + # Not all commands provide --predicate-type |
| 722 | + predicate_type = getattr(args, "predicate_type", None) |
706 | 723 |
|
707 |
| - sig_output: TextIO |
708 |
| - if outputs.signature is not None: |
709 |
| - sig_output = outputs.signature.open("w") |
710 |
| - else: |
711 |
| - sig_output = sys.stdout |
| 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) |
| 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() as executor: |
| 732 | + jobs = [ |
| 733 | + executor.submit( |
| 734 | + _sign_file_threaded, |
| 735 | + signer, |
| 736 | + predicate_type, |
| 737 | + predicate, |
| 738 | + file, |
| 739 | + outputs, |
| 740 | + ) |
| 741 | + for file, outputs in output_map.items() |
| 742 | + ] |
| 743 | + for job in futures.as_completed(jobs): |
| 744 | + job.result() |
712 | 745 |
|
713 |
| - signature = base64.b64encode(result.signature).decode() |
714 |
| - print(signature, file=sig_output) |
| 746 | + for file, outputs in output_map.items(): |
715 | 747 | if outputs.signature is not None:
|
716 | 748 | print(f"Signature written to {outputs.signature}")
|
717 |
| - |
718 | 749 | if outputs.certificate is not None:
|
719 |
| - with outputs.certificate.open(mode="w") as io: |
720 |
| - print(cert_pem, file=io) |
721 | 750 | print(f"Certificate written to {outputs.certificate}")
|
722 |
| - |
723 | 751 | if outputs.bundle is not None:
|
724 |
| - with outputs.bundle.open(mode="w") as io: |
725 |
| - print(result.to_json(), file=io) |
726 | 752 | print(f"Sigstore bundle written to {outputs.bundle}")
|
727 | 753 |
|
728 | 754 |
|
|
0 commit comments