32
32
from sigstore_protobuf_specs .dev .sigstore .bundle .v1 import (
33
33
Bundle as RawBundle ,
34
34
)
35
+ from sigstore_protobuf_specs .dev .sigstore .common .v1 import HashAlgorithm
35
36
from typing_extensions import TypeAlias
36
37
37
38
from sigstore import __version__ , dsse
@@ -81,6 +82,21 @@ class SigningOutputs:
81
82
bundle : Optional [Path ] = None
82
83
83
84
85
+ @dataclass (frozen = True )
86
+ class VerificationUnbundledMaterials :
87
+ certificate : Path
88
+ signature : Path
89
+
90
+
91
+ @dataclass (frozen = True )
92
+ class VerificationBundledMaterials :
93
+ bundle : Path
94
+
95
+
96
+ VerificationMaterials : TypeAlias = Union [
97
+ VerificationUnbundledMaterials , VerificationBundledMaterials
98
+ ]
99
+
84
100
# Map of inputs -> outputs for signing operations
85
101
OutputMap : TypeAlias = Dict [Path , SigningOutputs ]
86
102
@@ -149,12 +165,25 @@ def _add_shared_verify_input_options(group: argparse._ArgumentGroup) -> None:
149
165
default = os .getenv ("SIGSTORE_BUNDLE" ),
150
166
help = ("The Sigstore bundle to verify with; not used with multiple inputs" ),
151
167
)
168
+
169
+ def file_or_digest (arg : str ) -> Hashed | Path :
170
+ if arg .startswith ("sha256:" ):
171
+ digest = bytes .fromhex (arg [len ("sha256:" ) :])
172
+ if len (digest ) != 32 :
173
+ raise ValueError ()
174
+ return Hashed (
175
+ digest = digest ,
176
+ algorithm = HashAlgorithm .SHA2_256 ,
177
+ )
178
+ else :
179
+ return Path (arg )
180
+
152
181
group .add_argument (
153
- "files " ,
154
- metavar = "FILE " ,
155
- type = Path ,
182
+ "files_or_digest " ,
183
+ metavar = "FILE_OR_DIGEST " ,
184
+ type = file_or_digest ,
156
185
nargs = "+" ,
157
- help = "The file to verify" ,
186
+ help = "The file path or the digest to verify. The digest should start with the 'sha256:' prefix. " ,
158
187
)
159
188
160
189
@@ -826,7 +855,7 @@ def _sign(args: argparse.Namespace) -> None:
826
855
827
856
def _collect_verification_state (
828
857
args : argparse .Namespace ,
829
- ) -> tuple [Verifier , list [tuple [Path , Hashed , Bundle ]]]:
858
+ ) -> tuple [Verifier , list [tuple [Path | Hashed , Hashed , Bundle ]]]:
830
859
"""
831
860
Performs CLI functionality common across all `sigstore verify` subcommands.
832
861
@@ -835,13 +864,15 @@ def _collect_verification_state(
835
864
pre-hashed input to the file being verified and `bundle` is the `Bundle` to verify with.
836
865
"""
837
866
838
- # Fail if --certificate, --signature, or --bundle is specified and we
867
+ # Fail if --certificate, --signature, or --bundle is specified, and we
839
868
# have more than one input.
840
- if (args .certificate or args .signature or args .bundle ) and len (args .files ) > 1 :
869
+ if (args .certificate or args .signature or args .bundle ) and len (
870
+ args .files_or_digest
871
+ ) > 1 :
841
872
_invalid_arguments (
842
873
args ,
843
874
"--certificate, --signature, or --bundle can only be used "
844
- "with a single input file" ,
875
+ "with a single input file or digest " ,
845
876
)
846
877
847
878
# Fail if `--certificate` or `--signature` is used with `--bundle`.
@@ -850,6 +881,14 @@ def _collect_verification_state(
850
881
args , "--bundle cannot be used with --certificate or --signature"
851
882
)
852
883
884
+ # Fail if digest input is not used with `--bundle` or both `--certificate` and `--signature`.
885
+ if any ((isinstance (x , Hashed ) for x in args .files_or_digest )):
886
+ if not args .bundle and not (args .certificate and args .signature ):
887
+ _invalid_arguments (
888
+ args ,
889
+ "verifying a digest input (sha256:*) needs either --bundle or both --certificate and --signature" ,
890
+ )
891
+
853
892
# Fail if `--certificate` or `--signature` is used with `--offline`.
854
893
if args .offline and (args .certificate or args .signature ):
855
894
_invalid_arguments (
@@ -858,8 +897,8 @@ def _collect_verification_state(
858
897
859
898
# The converse of `sign`: we build up an expected input map and check
860
899
# that we have everything so that we can fail early.
861
- input_map = {}
862
- for file in args .files :
900
+ input_map : dict [ Path | Hashed , VerificationMaterials ] = {}
901
+ for file in ( f for f in args .files_or_digest if isinstance ( f , Path )) :
863
902
if not file .is_file ():
864
903
_invalid_arguments (args , f"Input must be a file: { file } " )
865
904
@@ -900,21 +939,61 @@ def _collect_verification_state(
900
939
missing .append (str (sig ))
901
940
if not cert .is_file ():
902
941
missing .append (str (cert ))
903
- input_map [file ] = {"cert" : cert , "sig" : sig }
942
+ input_map [file ] = VerificationUnbundledMaterials (
943
+ certificate = cert , signature = sig
944
+ )
904
945
else :
905
946
# If a user hasn't explicitly supplied `--signature` or `--certificate`,
906
947
# we expect a bundle either supplied via `--bundle` or with the
907
948
# default `{input}.sigstore(.json)?` name.
908
949
if not bundle .is_file ():
909
950
missing .append (str (bundle ))
910
951
911
- input_map [file ] = { " bundle" : bundle }
952
+ input_map [file ] = VerificationBundledMaterials ( bundle = bundle )
912
953
913
954
if missing :
914
955
_invalid_arguments (
915
956
args ,
916
957
f"Missing verification materials for { (file )} : { ', ' .join (missing )} " ,
917
958
)
959
+
960
+ if not input_map :
961
+ if len (args .files_or_digest ) != 1 :
962
+ # This should never happen, since if `input_map` is empty that means there
963
+ # were no file inputs, and therefore exactly one digest input should be
964
+ # present.
965
+ _invalid_arguments (
966
+ args , "Internal error: Found multiple digests in CLI arguments"
967
+ )
968
+ hashed = args .files_or_digest [0 ]
969
+ sig , cert , bundle = (
970
+ args .signature ,
971
+ args .certificate ,
972
+ args .bundle ,
973
+ )
974
+ missing = []
975
+ if args .signature or args .certificate :
976
+ if not sig .is_file ():
977
+ missing .append (str (sig ))
978
+ if not cert .is_file ():
979
+ missing .append (str (cert ))
980
+ input_map [hashed ] = VerificationUnbundledMaterials (
981
+ certificate = cert , signature = sig
982
+ )
983
+ else :
984
+ # If a user hasn't explicitly supplied `--signature` or `--certificate`,
985
+ # we expect a bundle supplied via `--bundle`
986
+ if not bundle .is_file ():
987
+ missing .append (str (bundle ))
988
+
989
+ input_map [hashed ] = VerificationBundledMaterials (bundle = bundle )
990
+
991
+ if missing :
992
+ _invalid_arguments (
993
+ args ,
994
+ f"Missing verification materials for { (hashed )} : { ', ' .join (missing )} " ,
995
+ )
996
+
918
997
if args .staging :
919
998
_logger .debug ("verify: staging instances requested" )
920
999
verifier = Verifier .staging ()
@@ -925,24 +1004,27 @@ def _collect_verification_state(
925
1004
verifier = Verifier .production ()
926
1005
927
1006
all_materials = []
928
- for file , inputs in input_map .items ():
929
- with file .open (mode = "rb" ) as io :
930
- hashed = sha256_digest (io )
1007
+ for file_or_hashed , materials in input_map .items ():
1008
+ if isinstance (file_or_hashed , Path ):
1009
+ with file_or_hashed .open (mode = "rb" ) as io :
1010
+ hashed = sha256_digest (io )
1011
+ else :
1012
+ hashed = file_or_hashed
931
1013
932
- if "bundle" in inputs :
1014
+ if isinstance ( materials , VerificationBundledMaterials ) :
933
1015
# Load the bundle
934
- _logger .debug (f"Using bundle from: { inputs [ ' bundle' ] } " )
1016
+ _logger .debug (f"Using bundle from: { materials . bundle } " )
935
1017
936
- bundle_bytes = inputs [ " bundle" ] .read_bytes ()
1018
+ bundle_bytes = materials . bundle .read_bytes ()
937
1019
bundle = Bundle .from_json (bundle_bytes )
938
1020
else :
939
1021
# Load the signing certificate
940
- _logger .debug (f"Using certificate from: { inputs [ 'cert' ] } " )
941
- cert = load_pem_x509_certificate (inputs [ "cert" ] .read_bytes ())
1022
+ _logger .debug (f"Using certificate from: { materials . certificate } " )
1023
+ cert = load_pem_x509_certificate (materials . certificate .read_bytes ())
942
1024
943
1025
# Load the signature
944
- _logger .debug (f"Using signature from: { inputs [ 'sig' ] } " )
945
- b64_signature = inputs [ "sig" ] .read_text ()
1026
+ _logger .debug (f"Using signature from: { materials . signature } " )
1027
+ b64_signature = materials . signature .read_text ()
946
1028
signature = base64 .b64decode (b64_signature )
947
1029
948
1030
# When using "detached" materials, we *must* retrieve the log
@@ -953,33 +1035,34 @@ def _collect_verification_state(
953
1035
)
954
1036
if log_entry is None :
955
1037
_invalid_arguments (
956
- args , f"No matching log entry for { file } 's verification materials"
1038
+ args ,
1039
+ f"No matching log entry for { file_or_hashed } 's verification materials" ,
957
1040
)
958
1041
bundle = Bundle .from_parts (cert , signature , log_entry )
959
1042
960
- _logger .debug (f"Verifying contents from: { file } " )
1043
+ _logger .debug (f"Verifying contents from: { file_or_hashed } " )
961
1044
962
- all_materials .append ((file , hashed , bundle ))
1045
+ all_materials .append ((file_or_hashed , hashed , bundle ))
963
1046
964
1047
return (verifier , all_materials )
965
1048
966
1049
967
1050
def _verify_identity (args : argparse .Namespace ) -> None :
968
1051
verifier , materials = _collect_verification_state (args )
969
1052
970
- for file , hashed , bundle in materials :
1053
+ for file_or_digest , hashed , bundle in materials :
971
1054
policy_ = policy .Identity (
972
1055
identity = args .cert_identity ,
973
1056
issuer = args .cert_oidc_issuer ,
974
1057
)
975
1058
976
1059
try :
977
1060
statement = _verify_common (verifier , hashed , bundle , policy_ )
978
- print (f"OK: { file } " , file = sys .stderr )
1061
+ print (f"OK: { file_or_digest } " , file = sys .stderr )
979
1062
if statement is not None :
980
1063
print (statement ._contents .decode ())
981
1064
except Error as exc :
982
- _logger .error (f"FAIL: { file } " )
1065
+ _logger .error (f"FAIL: { file_or_digest } " )
983
1066
exc .log_and_exit (_logger , args .verbose >= 1 )
984
1067
985
1068
@@ -1020,14 +1103,14 @@ def _verify_github(args: argparse.Namespace) -> None:
1020
1103
policy_ = policy .AllOf (inner_policies )
1021
1104
1022
1105
verifier , materials = _collect_verification_state (args )
1023
- for file , hashed , bundle in materials :
1106
+ for file_or_digest , hashed , bundle in materials :
1024
1107
try :
1025
1108
statement = _verify_common (verifier , hashed , bundle , policy_ )
1026
- print (f"OK: { file } " , file = sys .stderr )
1109
+ print (f"OK: { file_or_digest } " , file = sys .stderr )
1027
1110
if statement is not None :
1028
1111
print (statement ._contents )
1029
1112
except Error as exc :
1030
- _logger .error (f"FAIL: { file } " )
1113
+ _logger .error (f"FAIL: { file_or_digest } " )
1031
1114
exc .log_and_exit (_logger , args .verbose >= 1 )
1032
1115
1033
1116
0 commit comments