Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 54 additions & 30 deletions lib/rubygems/commands/push_command.rb
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ def initialize
end

add_option("--attestation FILE",
"Push with sigstore attestations") do |value, options|
"Push with sigstore attestations",
" (FILE must be a JSON sigstore bundle)") do |value, options|
options[:attestations] << value
end

Expand Down Expand Up @@ -94,7 +95,7 @@ def send_gem(name)
def send_push_request(name, args)
# Always honor explicit --attestation option
# Auto-attestation is only supported on rubygems.org with GitHub Actions (not JRuby)
if options[:attestations].any? || (RUBY_ENGINE != "jruby" && attestation_supported_host? && ENV["GITHUB_ACTIONS"])
if options[:attestations].any? || (RUBY_ENGINE != "jruby" && attestation_supported_host? && ENV["GITHUB_ACTIONS"] == "true")
send_push_request_with_attestation(name, args)
else
send_push_request_without_attestation(name, args)
Expand All @@ -115,14 +116,23 @@ def send_push_request_without_attestation(name, args)
def send_push_request_with_attestation(name, args)
attestations = if options[:attestations].any?
options[:attestations].map do |attestation|
Gem.read_binary(attestation)
load_attestation(attestation)
end
else
bundle_path = attest!(name)
# Only the opportunistic signing step falls back. The request below stays
# outside this rescue because once the server may have seen the attested
# push, a network error must not trigger an unattested retry.
begin
[Gem.read_binary(bundle_path)]
ensure
File.unlink(bundle_path) if bundle_path && File.exist?(bundle_path)
[attest!(name)]
rescue StandardError => e
message = "Failed to create an attestation, pushing without one.\n"
message += if Gem.configuration.really_verbose
e.full_message
else
e.message
end
alert_warning message
return send_push_request_without_attestation(name, args)
end
end
bundles = "[" + attestations.join(",") + "]"
Expand All @@ -134,38 +144,52 @@ def send_push_request_with_attestation(name, args)
], "multipart/form-data")
request.add_field "Authorization", api_key
end
rescue StandardError => e
message = "Failed to push with attestation, retrying without attestation.\n"
message += if Gem.configuration.really_verbose
e.full_message
else
e.message
end

def load_attestation(file)
data = begin
Gem.read_binary(file)
rescue SystemCallError, IOError, ArgumentError => e
raise Gem::Exception, "Failed to read attestation #{file}: #{e.message}"
end
validate_attestation_json(data, file)
end

def validate_attestation_json(data, source)
require "json"

parsed = begin
JSON.parse(data)
rescue JSON::ParserError => e
raise Gem::Exception, "Attestation #{source} is not valid JSON: #{e.message}"
end
alert_warning message
send_push_request_without_attestation(name, args)
raise Gem::Exception, "Attestation #{source} is not a JSON object" unless parsed.is_a?(Hash)
data
end

def attest!(name)
require "open3"
require "shellwords"
require "tempfile"

tempfile = Tempfile.new([File.basename(name, ".*"), ".sigstore.json"])
bundle = tempfile.path
tempfile.close(false)

env = defined?(Bundler.unbundled_env) ? Bundler.unbundled_env : ENV.to_h
# Gem.ruby is quoted if it contains whitespace, so split it into argv
# elements to keep the quotes out of the spawned command.
out, st = Open3.capture2e(
env,
*Shellwords.split(Gem.ruby), "-S", "gem", "exec", "--conservative",
"sigstore-cli", "sign", name, "--bundle", bundle,
unsetenv_others: true
)
raise Gem::Exception, "Failed to sign gem:\n\n#{out}" unless st.success?

bundle

Tempfile.create([File.basename(name, ".*"), ".sigstore.json"]) do |tempfile|
tempfile.close
bundle = tempfile.path

# Gem.ruby is quoted if it contains whitespace, so split it into argv
# elements to keep the quotes out of the spawned command.
out, st = Open3.capture2e(
env,
*Shellwords.split(Gem.ruby), "-S", "gem", "exec", "--conservative",
"sigstore-cli", "sign", name, "--bundle", bundle,
unsetenv_others: true
)
raise Gem::Exception, "Failed to sign gem:\n\n#{out}" unless st.success?

validate_attestation_json(Gem.read_binary(bundle), "generated by sigstore-cli")
end
end

def get_hosts_for(name)
Expand Down
1 change: 1 addition & 0 deletions test/rubygems/helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,7 @@ def setup
ENV["XDG_STATE_HOME"] = nil
ENV["MAKEFLAGS"] = nil
ENV["SOURCE_DATE_EPOCH"] = nil
ENV["GITHUB_ACTIONS"] = nil
ENV["BUNDLER_VERSION"] = nil
ENV["BUNDLE_CONFIG"] = nil
ENV["BUNDLE_USER_CONFIG"] = nil
Expand Down
Loading