@@ -48,7 +48,8 @@ def initialize
4848 end
4949
5050 add_option ( "--attestation FILE" ,
51- "Push with sigstore attestations" ) do |value , options |
51+ "Push with sigstore attestations" ,
52+ " (FILE must be a JSON sigstore bundle)" ) do |value , options |
5253 options [ :attestations ] << value
5354 end
5455
@@ -96,7 +97,7 @@ def send_gem(name)
9697 def send_push_request ( name , args )
9798 # Always honor explicit --attestation option
9899 # Auto-attestation is only supported on rubygems.org with GitHub Actions (not JRuby)
99- if options [ :attestations ] . any? || ( RUBY_ENGINE != "jruby" && attestation_supported_host? && ENV [ "GITHUB_ACTIONS" ] )
100+ if options [ :attestations ] . any? || ( RUBY_ENGINE != "jruby" && attestation_supported_host? && ENV [ "GITHUB_ACTIONS" ] == "true" )
100101 send_push_request_with_attestation ( name , args )
101102 else
102103 send_push_request_without_attestation ( name , args )
@@ -117,14 +118,23 @@ def send_push_request_without_attestation(name, args)
117118 def send_push_request_with_attestation ( name , args )
118119 attestations = if options [ :attestations ] . any?
119120 options [ :attestations ] . map do |attestation |
120- Gem . read_binary ( attestation )
121+ load_attestation ( attestation )
121122 end
122123 else
123- bundle_path = attest! ( name )
124+ # Only the opportunistic signing step falls back. The request below stays
125+ # outside this rescue because once the server may have seen the attested
126+ # push, a network error must not trigger an unattested retry.
124127 begin
125- [ Gem . read_binary ( bundle_path ) ]
126- ensure
127- File . unlink ( bundle_path ) if bundle_path && File . exist? ( bundle_path )
128+ [ attest! ( name ) ]
129+ rescue StandardError => e
130+ message = "Failed to create an attestation, pushing without one.\n "
131+ message += if Gem . configuration . really_verbose
132+ e . full_message
133+ else
134+ e . message
135+ end
136+ alert_warning message
137+ return send_push_request_without_attestation ( name , args )
128138 end
129139 end
130140 bundles = "[" + attestations . join ( "," ) + "]"
@@ -136,38 +146,52 @@ def send_push_request_with_attestation(name, args)
136146 ] , "multipart/form-data" )
137147 request . add_field "Authorization" , api_key
138148 end
139- rescue StandardError => e
140- message = "Failed to push with attestation, retrying without attestation.\n "
141- message += if Gem . configuration . really_verbose
142- e . full_message
143- else
144- e . message
149+ end
150+
151+ def load_attestation ( file )
152+ data = begin
153+ Gem . read_binary ( file )
154+ rescue SystemCallError , IOError , ArgumentError => e
155+ raise Gem ::Exception , "Failed to read attestation #{ file } : #{ e . message } "
156+ end
157+ validate_attestation_json ( data , file )
158+ end
159+
160+ def validate_attestation_json ( data , source )
161+ require "json"
162+
163+ parsed = begin
164+ JSON . parse ( data )
165+ rescue JSON ::ParserError => e
166+ raise Gem ::Exception , "Attestation #{ source } is not valid JSON: #{ e . message } "
145167 end
146- alert_warning message
147- send_push_request_without_attestation ( name , args )
168+ raise Gem :: Exception , "Attestation #{ source } is not a JSON object" unless parsed . is_a? ( Hash )
169+ data
148170 end
149171
150172 def attest! ( name )
151173 require "open3"
152174 require "shellwords"
153175 require "tempfile"
154176
155- tempfile = Tempfile . new ( [ File . basename ( name , ".*" ) , ".sigstore.json" ] )
156- bundle = tempfile . path
157- tempfile . close ( false )
158-
159177 env = defined? ( Bundler . unbundled_env ) ? Bundler . unbundled_env : ENV . to_h
160- # Gem.ruby is quoted if it contains whitespace, so split it into argv
161- # elements to keep the quotes out of the spawned command.
162- out , st = Open3 . capture2e (
163- env ,
164- *Shellwords . split ( Gem . ruby ) , "-S" , "gem" , "exec" , "--conservative" ,
165- "sigstore-cli" , "sign" , name , "--bundle" , bundle ,
166- unsetenv_others : true
167- )
168- raise Gem ::Exception , "Failed to sign gem:\n \n #{ out } " unless st . success?
169-
170- bundle
178+
179+ Tempfile . create ( [ File . basename ( name , ".*" ) , ".sigstore.json" ] ) do |tempfile |
180+ tempfile . close
181+ bundle = tempfile . path
182+
183+ # Gem.ruby is quoted if it contains whitespace, so split it into argv
184+ # elements to keep the quotes out of the spawned command.
185+ out , st = Open3 . capture2e (
186+ env ,
187+ *Shellwords . split ( Gem . ruby ) , "-S" , "gem" , "exec" , "--conservative" ,
188+ "sigstore-cli" , "sign" , name , "--bundle" , bundle ,
189+ unsetenv_others : true
190+ )
191+ raise Gem ::Exception , "Failed to sign gem:\n \n #{ out } " unless st . success?
192+
193+ validate_attestation_json ( Gem . read_binary ( bundle ) , "generated by sigstore-cli" )
194+ end
171195 end
172196
173197 def get_hosts_for ( name )
0 commit comments