[rubygems/rubygems] Improve errors and register checksums reliably

Improve error reporting for checksums, raises a new error class.

Solve for multi-source checksum errors.

Add CHECKSUMS to tool/bundler/(dev|standard|rubocop)26_gems.rb

https://github.com/rubygems/rubygems/commit/26ceee0e76

Co-authored-by: Samuel Giddins <segiddins@segiddins.me>
This commit is contained in:
Martin Emde 2023-09-01 15:15:49 -07:00 committed by Hiroshi SHIBATA
parent 6362bfdc33
commit c667de72ff
No known key found for this signature in database
GPG Key ID: F9CF13417264FAC2
24 changed files with 543 additions and 273 deletions

View File

@ -2,37 +2,38 @@
module Bundler module Bundler
class Checksum class Checksum
DEFAULT_ALGORITHM = "sha256"
private_constant :DEFAULT_ALGORITHM
DEFAULT_BLOCK_SIZE = 16_384 DEFAULT_BLOCK_SIZE = 16_384
private_constant :DEFAULT_BLOCK_SIZE private_constant :DEFAULT_BLOCK_SIZE
class << self class << self
def from_gem_source(source, digest_algorithms: %w[sha256]) def from_gem(io, pathname, algo = DEFAULT_ALGORITHM)
raise ArgumentError, "not a valid gem source: #{source}" unless source.respond_to?(:with_read_io) digest = Bundler::SharedHelpers.digest(algo.upcase).new
buf = String.new(:capacity => DEFAULT_BLOCK_SIZE)
source.with_read_io do |io| digest << io.readpartial(DEFAULT_BLOCK_SIZE, buf) until io.eof?
checksums = from_io(io, "#{source.path || source.inspect} gem source hexdigest", :digest_algorithms => digest_algorithms) Checksum.new(algo, digest.hexdigest!, Source.new(:gem, pathname))
io.rewind
return checksums
end
end end
def from_io(io, source, digest_algorithms: %w[sha256]) def from_api(digest, source_uri)
digests = digest_algorithms.to_h do |algo| # transform the bytes from base64 to hex, switch to unpack1 when we drop older rubies
[algo.to_s, Bundler::SharedHelpers.digest(algo.upcase).new] hexdigest = digest.length == 44 ? digest.unpack("m0").first.unpack("H*").first : digest
if hexdigest.length != 64
raise ArgumentError, "#{digest.inspect} is not a valid SHA256 hexdigest nor base64digest"
end end
until io.eof? Checksum.new(DEFAULT_ALGORITHM, hexdigest, Source.new(:api, source_uri))
ret = io.read DEFAULT_BLOCK_SIZE
digests.each_value {|digest| digest << ret }
end end
digests.map do |algo, digest| def from_lock(lock_checksum, lockfile_location)
Checksum.new(algo, digest.hexdigest!, source) algo, digest = lock_checksum.strip.split("-", 2)
end Checksum.new(algo, digest, Source.new(:lock, lockfile_location))
end end
end end
attr_reader :algo, :digest, :sources attr_reader :algo, :digest, :sources
def initialize(algo, digest, source) def initialize(algo, digest, source)
@algo = algo @algo = algo
@digest = digest @digest = digest
@ -62,20 +63,81 @@ module Bundler
end end
def merge!(other) def merge!(other)
raise ArgumentError, "cannot merge checksums of different algorithms" unless algo == other.algo return nil unless match?(other)
unless digest == other.digest
raise SecurityError, <<~MESSAGE
#{other}
#{to_lock} from:
* #{sources.join("\n* ")}
MESSAGE
end
@sources.concat(other.sources).uniq! @sources.concat(other.sources).uniq!
self self
end end
def formatted_sources
sources.join("\n and ").concat("\n")
end
def removable?
sources.all?(&:removable?)
end
def removal_instructions
msg = +""
i = 1
sources.each do |source|
msg << " #{i}. #{source.removal}\n"
i += 1
end
msg << " #{i}. run `bundle install`\n"
end
def inspect
abbr = "#{algo}-#{digest[0, 8]}"
from = "from #{sources.join(" and ")}"
"#<#{self.class}:#{object_id} #{abbr} #{from}>"
end
class Source
attr_reader :type, :location
def initialize(type, location)
@type = type
@location = location
end
def removable?
type == :lock || type == :gem
end
def ==(other)
other.is_a?(self.class) && other.type == type && other.location == location
end
# phrased so that the usual string format is grammatically correct
# rake (10.3.2) sha256-abc123 from #{to_s}
def to_s
case type
when :lock
"the lockfile CHECKSUMS at #{location}"
when :gem
"the gem at #{location}"
when :api
"the API at #{location}"
else
"#{location} (#{type})"
end
end
# A full sentence describing how to remove the checksum
def removal
case type
when :lock
"remove the matching checksum in #{location}"
when :gem
"remove the gem at #{location}"
when :api
"checksums from #{location} cannot be locally modified, you may need to update your sources"
else
"remove #{location} (#{type})"
end
end
end
class Store class Store
attr_reader :store attr_reader :store
protected :store protected :store
@ -86,89 +148,81 @@ module Bundler
def initialize_copy(other) def initialize_copy(other)
@store = {} @store = {}
other.store.each do |full_name, checksums| other.store.each do |name_tuple, checksums|
store[full_name] = checksums.dup store[name_tuple] = checksums.dup
end end
end end
def checksums(full_name) def inspect
store[full_name] "#<#{self.class}:#{object_id} size=#{store.size}>"
end end
def register_gem_package(spec, source) def fetch(spec, algo = DEFAULT_ALGORITHM)
new_checksums = Checksum.from_gem_source(source) store[spec.name_tuple]&.fetch(algo, nil)
new_checksums.each do |checksum|
register spec.full_name, checksum
end
rescue SecurityError
expected = checksums(spec.full_name)
gem_lock_name = GemHelpers.lock_name(spec.name, spec.version, spec.platform)
raise SecurityError, <<~MESSAGE
Bundler cannot continue installing #{gem_lock_name}.
The checksum for the downloaded `#{spec.full_name}.gem` does not match \
the known checksum for the gem.
This means the contents of the downloaded \
gem is different from what was uploaded to the server \
or first used by your teammates, and could be a potential security issue.
To resolve this issue:
1. delete the downloaded gem located at: `#{source.path}`
2. run `bundle install`
If you are sure that the new checksum is correct, you can \
remove the `#{gem_lock_name}` entry under the lockfile `CHECKSUMS` \
section and rerun `bundle install`.
If you wish to continue installing the downloaded gem, and are certain it does not pose a \
security issue despite the mismatching checksum, do the following:
1. run `bundle config set --local disable_checksum_validation true` to turn off checksum verification
2. run `bundle install`
#{expected.map do |checksum|
next unless actual = new_checksums.find {|c| c.algo == checksum.algo }
next if actual.digest == checksum.digest
"(More info: The expected #{checksum.algo.upcase} checksum was #{checksum.digest.inspect}, but the " \
"checksum for the downloaded gem was #{actual.digest.inspect}. The expected checksum came from: #{checksum.sources.join(", ")})"
end.compact.join("\n")}
MESSAGE
end end
def register(full_name, checksum) # Replace when the new checksum is from the same source.
# The primary purpose of this registering checksums from gems where there are
# duplicates of the same gem (according to full_name) in the index.
# In particular, this is when 2 gems have two similar platforms, e.g.
# "darwin20" and "darwin-20", both of which resolve to darwin-20.
# In the Index, the later gem replaces the former, so we do that here.
#
# However, if the new checksum is from a different source, we register like normal.
# This ensures a mismatch error where there are multiple top level sources
# that contain the same gem with different checksums.
def replace(spec, checksum)
return if Bundler.settings[:disable_checksum_validation]
return unless checksum return unless checksum
sums = (store[full_name] ||= []) name_tuple = spec.name_tuple
sums.find {|c| c.algo == checksum.algo }&.merge!(checksum) || sums << checksum checksums = (store[name_tuple] ||= {})
rescue SecurityError => e existing = checksums[checksum.algo]
raise e.exception(<<~MESSAGE)
Bundler found multiple different checksums for #{full_name}.
This means that there are multiple different `#{full_name}.gem` files.
This is a potential security issue, since Bundler could be attempting \
to install a different gem than what you expect.
#{e.message} # we assume only one source because this is used while building the index
To resolve this issue: if !existing || existing.sources.first == checksum.sources.first
1. delete any downloaded gems referenced above checksums[checksum.algo] = checksum
2. run `bundle install` else
register_checksum(name_tuple, checksum)
If you are sure that the new checksum is correct, you can \ end
remove the `#{full_name}` entry under the lockfile `CHECKSUMS` \
section and rerun `bundle install`.
If you wish to continue installing the downloaded gem, and are certain it does not pose a \
security issue despite the mismatching checksum, do the following:
1. run `bundle config set --local disable_checksum_validation true` to turn off checksum verification
2. run `bundle install`
MESSAGE
end end
def replace(full_name, checksum) def register(spec, checksum)
store[full_name] = checksum ? [checksum] : nil return if Bundler.settings[:disable_checksum_validation]
return unless checksum
register_checksum(spec.name_tuple, checksum)
end end
def register_store(other) def merge!(other)
other.store.each do |full_name, checksums| other.store.each do |name_tuple, checksums|
checksums.each {|checksum| register(full_name, checksum) } checksums.each do |_algo, checksum|
register_checksum(name_tuple, checksum)
end
end
end
def to_lock(spec)
name_tuple = spec.name_tuple
if checksums = store[name_tuple]
"#{name_tuple.lock_name} #{checksums.values.map(&:to_lock).sort.join(",")}"
else
name_tuple.lock_name
end
end
private
def register_checksum(name_tuple, checksum)
return unless checksum
checksums = (store[name_tuple] ||= {})
existing = checksums[checksum.algo]
if !existing
checksums[checksum.algo] = checksum
elsif existing.merge!(checksum)
checksum
else
raise ChecksumMismatchError.new(name_tuple, existing, checksum)
end end
end end
end end

View File

@ -751,8 +751,8 @@ module Bundler
sources.all_sources.each do |source| sources.all_sources.each do |source|
# has to be done separately, because we want to keep the locked checksum # has to be done separately, because we want to keep the locked checksum
# store for a source, even when doing a full update # store for a source, even when doing a full update
if @locked_gems && locked_source = @locked_gems.sources.find {|s| s == source } if @locked_gems && locked_source = @locked_gems.sources.find {|s| s == source && !s.equal?(source) }
source.checksum_store.register_store(locked_source.checksum_store) source.checksum_store.merge!(locked_source.checksum_store)
end end
# If the source is unlockable and the current command allows an unlock of # If the source is unlockable and the current command allows an unlock of
# the source (for example, you are doing a `bundle update <foo>` of a git-pinned # the source (for example, you are doing a `bundle update <foo>` of a git-pinned

View File

@ -126,16 +126,11 @@ module Bundler
case k.to_s case k.to_s
when "checksum" when "checksum"
next if Bundler.settings[:disable_checksum_validation] next if Bundler.settings[:disable_checksum_validation]
digest = v.last begin
if digest.length == 64 @checksum = Checksum.from_api(v.last, @spec_fetcher.uri)
# nothing to do, it's a hexdigest rescue ArgumentError => e
elsif digest.length == 44 raise ArgumentError, "Invalid checksum for #{full_name}: #{e.message}"
# transform the bytes from base64 to hex
digest = digest.unpack("m0").first.unpack("H*").first
else
raise ArgumentError, "The given checksum for #{full_name} (#{digest.inspect}) is not a valid SHA256 hexdigest nor base64digest"
end end
@checksum = Checksum.new("sha256", digest, "API response from #{@spec_fetcher.uri}")
when "rubygems" when "rubygems"
@required_rubygems_version = Gem::Requirement.new(v) @required_rubygems_version = Gem::Requirement.new(v)
when "ruby" when "ruby"

View File

@ -52,6 +52,49 @@ module Bundler
class GemfileEvalError < GemfileError; end class GemfileEvalError < GemfileError; end
class MarshalError < StandardError; end class MarshalError < StandardError; end
class ChecksumMismatchError < SecurityError
def initialize(name_tuple, existing, checksum)
@name_tuple = name_tuple
@existing = existing
@checksum = checksum
end
def message
<<~MESSAGE
Bundler found mismatched checksums. This is a potential security risk.
#{@name_tuple.lock_name} #{@existing.to_lock}
from #{@existing.sources.join("\n and ")}
#{@name_tuple.lock_name} #{@checksum.to_lock}
from #{@checksum.sources.join("\n and ")}
#{mismatch_resolution_instructions}
To ignore checksum security warnings, disable checksum validation with
`bundle config set --local disable_checksum_validation true`
MESSAGE
end
def mismatch_resolution_instructions
removable, remote = [@existing, @checksum].partition(&:removable?)
case removable.size
when 0
msg = +"Mismatched checksums each have an authoritative source:\n"
msg << " 1. #{@existing.sources.reject(&:removable?).map(&:to_s).join(" and ")}\n"
msg << " 2. #{@checksum.sources.reject(&:removable?).map(&:to_s).join(" and ")}\n"
msg << "You may need to alter your Gemfile sources to resolve this issue.\n"
when 1
msg = +"If you trust #{remote.first.sources.first}, to resolve this issue you can:\n"
msg << removable.first.removal_instructions
when 2
msg = +"To resolve this issue you can either:\n"
msg << @checksum.removal_instructions
msg << "or if you are sure that the new checksum from #{@checksum.sources.first} is correct:\n"
msg << @existing.removal_instructions
end
end
status_code(37)
end
class PermissionError < BundlerError class PermissionError < BundlerError
def initialize(path, permission_type = :write) def initialize(path, permission_type = :write)
@path = path @path = path

View File

@ -140,11 +140,7 @@ module Bundler
fetch_specs(gem_names).each do |name, version, platform, dependencies, metadata| fetch_specs(gem_names).each do |name, version, platform, dependencies, metadata|
spec = if dependencies spec = if dependencies
EndpointSpecification.new(name, version, platform, self, dependencies, metadata).tap do |es| EndpointSpecification.new(name, version, platform, self, dependencies, metadata).tap do |es|
# Duplicate spec.full_names, different spec.original_names source.checksum_store.replace(es, es.checksum)
# index#<< ensures that the last one added wins, so if we're overriding
# here, make sure to also override the checksum, otherwise downloading the
# specs (even if that version is completely unused) will cause a SecurityError
source.checksum_store.replace(es.full_name, es.checksum)
end end
else else
RemoteSpecification.new(name, version, platform, self) RemoteSpecification.new(name, version, platform, self)

View File

@ -113,23 +113,5 @@ module Bundler
same_runtime_deps && same_metadata_deps same_runtime_deps && same_metadata_deps
end end
module_function :same_deps module_function :same_deps
def spec_full_name(name, version, platform)
if platform == Gem::Platform::RUBY
"#{name}-#{version}"
else
"#{name}-#{version}-#{platform}"
end
end
module_function :spec_full_name
def lock_name(name, version, platform)
if platform == Gem::Platform::RUBY
"#{name} (#{version})"
else
"#{name} (#{version}-#{platform})"
end
end
module_function :lock_name
end end
end end

View File

@ -20,7 +20,19 @@ module Bundler
end end
def full_name def full_name
@full_name ||= GemHelpers.spec_full_name(@name, @version, platform) @full_name ||= if platform == Gem::Platform::RUBY
"#{@name}-#{@version}"
else
"#{@name}-#{@version}-#{platform}"
end
end
def lock_name
@lock_name ||= name_tuple.lock_name
end
def name_tuple
Gem::NameTuple.new(@name, @version, @platform)
end end
def ==(other) def ==(other)
@ -57,7 +69,7 @@ module Bundler
def to_lock def to_lock
out = String.new out = String.new
out << " #{GemHelpers.lock_name(name, version, platform)}\n" out << " #{lock_name}\n"
dependencies.sort_by(&:to_s).uniq.each do |dep| dependencies.sort_by(&:to_s).uniq.each do |dep|
next if dep.type == :development next if dep.type == :development
@ -113,7 +125,7 @@ module Bundler
end end
def to_s def to_s
@to_s ||= GemHelpers.lock_name(name, version, platform) lock_name
end end
def git_version def git_version

View File

@ -67,15 +67,10 @@ module Bundler
end end
def add_checksums def add_checksums
out << "\nCHECKSUMS\n" checksums = definition.resolve.map do |spec|
spec.source.checksum_store.to_lock(spec)
definition.resolve.sort_by(&:full_name).each do |spec|
lock_name = GemHelpers.lock_name(spec.name, spec.version, spec.platform)
out << " #{lock_name}"
checksums = spec.source.checksum_store.checksums(spec.full_name)
out << " #{checksums.map(&:to_lock).sort.join(",")}" if checksums
out << "\n"
end end
add_section("CHECKSUMS", checksums)
end end
def add_locked_ruby_version def add_locked_ruby_version

View File

@ -101,7 +101,10 @@ module Bundler
"Run `git checkout HEAD -- #{@lockfile_path}` first to get a clean lock." "Run `git checkout HEAD -- #{@lockfile_path}` first to get a clean lock."
end end
lockfile.split(/((?:\r?\n)+)/).each_slice(2) do |line, whitespace| lockfile.split(/((?:\r?\n)+)/) do |line|
# split alternates between the line and the following whitespace
next @pos.advance!(line) if line.match?(/^\s*$/)
if SOURCE.include?(line) if SOURCE.include?(line)
@parse_method = :parse_source @parse_method = :parse_source
parse_source(line) parse_source(line)
@ -121,7 +124,6 @@ module Bundler
send(@parse_method, line) send(@parse_method, line)
end end
@pos.advance!(line) @pos.advance!(line)
@pos.advance!(whitespace)
end end
@specs = @specs.values.sort_by!(&:full_name) @specs = @specs.values.sort_by!(&:full_name)
rescue ArgumentError => e rescue ArgumentError => e
@ -217,23 +219,23 @@ module Bundler
spaces = $1 spaces = $1
return unless spaces.size == 2 return unless spaces.size == 2
checksums = $6
return unless checksums
name = $2 name = $2
version = $3 version = $3
platform = $4 platform = $4
checksums = $6
return unless checksums
version = Gem::Version.new(version) version = Gem::Version.new(version)
platform = platform ? Gem::Platform.new(platform) : Gem::Platform::RUBY platform = platform ? Gem::Platform.new(platform) : Gem::Platform::RUBY
full_name = GemHelpers.spec_full_name(name, version, platform) full_name = Gem::NameTuple.new(name, version, platform).full_name
# Don't raise exception if there's a checksum for a gem that's not in the lockfile, # Don't raise exception if there's a checksum for a gem that's not in the lockfile,
# we prefer to heal invalid lockfiles # we prefer to heal invalid lockfiles
return unless spec = @specs[full_name] return unless spec = @specs[full_name]
checksums.split(",").each do |c| checksums.split(",") do |lock_checksum|
algo, digest = c.split("-", 2) column = line.index(lock_checksum) + 1
lock_name = GemHelpers.lock_name(spec.name, spec.version, spec.platform) checksum = Checksum.from_lock(lock_checksum, "#{@lockfile_path}:#{@pos.line}:#{column}")
spec.source.checksum_store.register(full_name, Checksum.new(algo, digest, "#{@lockfile_path}:#{@pos} CHECKSUMS #{lock_name}")) spec.source.checksum_store.register(spec, checksum)
end end
end end

View File

@ -359,6 +359,27 @@ module Gem
end end
end end
require "rubygems/name_tuple"
class NameTuple
def self.new(name, version, platform="ruby")
if Gem::Platform === platform
super(name, version, platform.to_s)
else
super
end
end
def lock_name
@lock_name ||=
if platform == Gem::Platform::RUBY
"#{name} (#{version})"
else
"#{name} (#{version}-#{platform})"
end
end
end
require "rubygems/util" require "rubygems/util"
Util.singleton_class.module_eval do Util.singleton_class.module_eval do

View File

@ -60,10 +60,6 @@ module Bundler
end end
end end
def pre_install_checks
super && validate_bundler_checksum(options[:bundler_checksum_store])
end
def build_extensions def build_extensions
extension_cache_path = options[:bundler_extension_cache_path] extension_cache_path = options[:bundler_extension_cache_path]
extension_dir = spec.extension_dir extension_dir = spec.extension_dir
@ -98,6 +94,18 @@ module Bundler
end end
end end
def gem_checksum
return nil if Bundler.settings[:disable_checksum_validation]
return nil unless source = @package.instance_variable_get(:@gem)
return nil unless source.respond_to?(:with_read_io)
source.with_read_io do |io|
Checksum.from_gem(io, source.path)
ensure
io.rewind
end
end
private private
def prepare_extension_build(extension_dir) def prepare_extension_build(extension_dir)
@ -114,14 +122,5 @@ module Bundler
raise DirectoryRemovalError.new(e, "Could not delete previous installation of `#{dir}`") raise DirectoryRemovalError.new(e, "Could not delete previous installation of `#{dir}`")
end end
def validate_bundler_checksum(checksum_store)
return true if Bundler.settings[:disable_checksum_validation]
return true unless source = @package.instance_variable_get(:@gem)
return true unless source.respond_to?(:with_read_io)
checksum_store.register_gem_package spec, source
true
end
end end
end end

View File

@ -178,7 +178,6 @@ module Bundler
:wrappers => true, :wrappers => true,
:env_shebang => true, :env_shebang => true,
:build_args => options[:build_args], :build_args => options[:build_args],
:bundler_checksum_store => spec.source.checksum_store,
:bundler_extension_cache_path => extension_cache_path(spec) :bundler_extension_cache_path => extension_cache_path(spec)
) )
@ -197,6 +196,8 @@ module Bundler
spec.__swap__(s) spec.__swap__(s)
end end
spec.source.checksum_store.register(spec, installer.gem_checksum)
message = "Installing #{version_message(spec, options[:previous_spec])}" message = "Installing #{version_message(spec, options[:previous_spec])}"
message += " with native extensions" if spec.extensions.any? message += " with native extensions" if spec.extensions.any?
Bundler.ui.confirm message Bundler.ui.confirm message

View File

@ -119,6 +119,12 @@ RSpec.describe Bundler::LockfileParser do
let(:bundler_version) { Gem::Version.new("1.12.0.rc.2") } let(:bundler_version) { Gem::Version.new("1.12.0.rc.2") }
let(:ruby_version) { "ruby 2.1.3p242" } let(:ruby_version) { "ruby 2.1.3p242" }
let(:lockfile_path) { Bundler.default_lockfile.relative_path_from(Dir.pwd) } let(:lockfile_path) { Bundler.default_lockfile.relative_path_from(Dir.pwd) }
let(:rake_checksum) do
Bundler::Checksum.from_lock(
"sha256-814828c34f1315d7e7b7e8295184577cc4e969bad6156ac069d02d63f58d82e8",
"#{lockfile_path}:??:1"
)
end
shared_examples_for "parsing" do shared_examples_for "parsing" do
it "parses correctly" do it "parses correctly" do
@ -129,11 +135,9 @@ RSpec.describe Bundler::LockfileParser do
expect(subject.platforms).to eq platforms expect(subject.platforms).to eq platforms
expect(subject.bundler_version).to eq bundler_version expect(subject.bundler_version).to eq bundler_version
expect(subject.ruby_version).to eq ruby_version expect(subject.ruby_version).to eq ruby_version
checksums = subject.sources.last.checksum_store.checksums("rake-10.3.2") checksum = subject.sources.last.checksum_store.fetch(specs.last)
expect(checksums.size).to eq(1) expect(checksum).to be_match(rake_checksum)
expected_checksum = Bundler::Checksum.new("sha256", "814828c34f1315d7e7b7e8295184577cc4e969bad6156ac069d02d63f58d82e8", "#{lockfile_path}:??:1") expect(checksum.sources.first.to_s).to match(/the lockfile CHECKSUMS at #{Regexp.escape(lockfile_path.to_s)}:\d+:\d+/)
expect(checksums.first).to be_match(expected_checksum)
expect(checksums.first.sources.first).to match(/#{Regexp.escape(lockfile_path.to_s)}:\d+:\d+/)
end end
end end
@ -159,29 +163,28 @@ RSpec.describe Bundler::LockfileParser do
include_examples "parsing" include_examples "parsing"
end end
context "when CHECKSUMS has duplicate checksums that don't match" do context "when CHECKSUMS has duplicate checksums in the lockfile that don't match" do
let(:lockfile_contents) { super().split(/(?<=CHECKSUMS\n)/m).insert(1, " rake (10.3.2) sha256-69b69b69b69b69b69b69b69b69b69b69b69b69b69b69b69b69b69b69b69b69b6\n").join } let(:bad_checksum) { "sha256-c0ffee11c0ffee11c0ffee11c0ffee11c0ffee11c0ffee11c0ffee11c0ffee11" }
let(:lockfile_contents) { super().split(/(?<=CHECKSUMS\n)/m).insert(1, " rake (10.3.2) #{bad_checksum}\n").join }
it "raises a security error" do it "raises a security error" do
expect { subject }.to raise_error(Bundler::SecurityError) do |e| expect { subject }.to raise_error(Bundler::SecurityError) do |e|
expect(e.message).to match <<~MESSAGE expect(e.message).to match <<~MESSAGE
Bundler found multiple different checksums for rake-10.3.2. Bundler found mismatched checksums. This is a potential security risk.
This means that there are multiple different `rake-10.3.2.gem` files. rake (10.3.2) #{bad_checksum}
This is a potential security issue, since Bundler could be attempting to install a different gem than what you expect. from the lockfile CHECKSUMS at #{lockfile_path}:20:17
rake (10.3.2) #{rake_checksum.to_lock}
from the lockfile CHECKSUMS at #{lockfile_path}:21:17
sha256-814828c34f1315d7e7b7e8295184577cc4e969bad6156ac069d02d63f58d82e8 (from #{lockfile_path}:21:1 CHECKSUMS rake (10.3.2)) To resolve this issue you can either:
sha256-69b69b69b69b69b69b69b69b69b69b69b69b69b69b69b69b69b69b69b69b69b6 from: 1. remove the matching checksum in #{lockfile_path}:21:17
* #{lockfile_path}:20:1 CHECKSUMS rake (10.3.2) 2. run `bundle install`
or if you are sure that the new checksum from the lockfile CHECKSUMS at #{lockfile_path}:21:17 is correct:
To resolve this issue: 1. remove the matching checksum in #{lockfile_path}:20:17
1. delete any downloaded gems referenced above
2. run `bundle install` 2. run `bundle install`
If you are sure that the new checksum is correct, you can remove the `rake-10.3.2` entry under the lockfile `CHECKSUMS` section and rerun `bundle install`. To ignore checksum security warnings, disable checksum validation with
`bundle config set --local disable_checksum_validation true`
If you wish to continue installing the downloaded gem, and are certain it does not pose a security issue despite the mismatching checksum, do the following:
1. run `bundle config set --local disable_checksum_validation true` to turn off checksum verification
2. run `bundle install`
MESSAGE MESSAGE
end end
end end

View File

@ -281,9 +281,26 @@ RSpec.describe "bundle cache" do
build_gem "rack", "1.0.0", build_gem "rack", "1.0.0",
:path => bundled_app("vendor/cache"), :path => bundled_app("vendor/cache"),
:rubygems_version => "1.3.2" :rubygems_version => "1.3.2"
# This test is only really valid if the checksum isn't saved. It otherwise can't be the same gem. Tested below.
bundled_app_lock.write remove_checksums_from_lockfile(bundled_app_lock.read, "rack (1.0.0)")
simulate_new_machine simulate_new_machine
pending "Causes checksum mismatch exception" bundle :install
expect(cached_gem("rack-1.0.0")).to exist
end
it "raises an error when the gem file is altered and produces a different checksum" do
cached_gem("rack-1.0.0").rmtree
build_gem "rack", "1.0.0", :path => bundled_app("vendor/cache")
simulate_new_machine
bundle :install, :raise_on_error => false
expect(exitstatus).to eq(37)
expect(err).to include("Bundler found mismatched checksums.")
expect(err).to include("1. remove the gem at #{cached_gem("rack-1.0.0")}")
expect(cached_gem("rack-1.0.0")).to exist
cached_gem("rack-1.0.0").rmtree
bundle :install bundle :install
expect(cached_gem("rack-1.0.0")).to exist expect(cached_gem("rack-1.0.0")).to exist
end end

View File

@ -91,8 +91,10 @@ RSpec.describe "bundle lock" do
bundle "lock --update" bundle "lock --update"
expect(read_lockfile).to eq(@lockfile) expect(read_lockfile).to eq(remove_checksums_from_lockfile(@lockfile, "(2.3.2)"))
end
it "writes a lockfile when there is an outdated lockfile using a bundle is frozen" do
lockfile @lockfile.gsub("2.3.2", "2.3.1") lockfile @lockfile.gsub("2.3.2", "2.3.1")
bundle "lock --update", :env => { "BUNDLE_FROZEN" => "true" } bundle "lock --update", :env => { "BUNDLE_FROZEN" => "true" }

View File

@ -551,7 +551,7 @@ RSpec.describe "bundle update" do
lockfile original_lockfile lockfile original_lockfile
bundle "lock --update" bundle "lock --update"
expect(the_bundle).to include_gems("activesupport 6.0.4.1", "tzinfo 1.2.9") expect(the_bundle).to include_gems("activesupport 6.0.4.1", "tzinfo 1.2.9")
expect(lockfile).to eq expected_lockfile expect(lockfile).to eq(expected_lockfile)
end end
end end

View File

@ -28,6 +28,16 @@ RSpec.describe "bundle install from an existing gemspec" do
x64_mingw_archs.join("\n ") x64_mingw_archs.join("\n ")
end end
let(:x64_mingw_checksums) do
x64_mingw_archs.map do |arch|
if arch == "x64-mingw-ucrt"
gem_no_checksum "platform_specific", "1.0", arch
else
checksum_for_repo_gem gem_repo2, "platform_specific", "1.0", arch
end
end.join("\n ")
end
it "should install runtime and development dependencies" do it "should install runtime and development dependencies" do
build_lib("foo", :path => tmp.join("foo")) do |s| build_lib("foo", :path => tmp.join("foo")) do |s|
s.write("Gemfile", "source :rubygems\ngemspec") s.write("Gemfile", "source :rubygems\ngemspec")
@ -449,12 +459,6 @@ RSpec.describe "bundle install from an existing gemspec" do
it "keeps all platform dependencies in the lockfile" do it "keeps all platform dependencies in the lockfile" do
expect(the_bundle).to include_gems "foo 1.0", "platform_specific 1.0 RUBY" expect(the_bundle).to include_gems "foo 1.0", "platform_specific 1.0 RUBY"
expected_checksums = checksum_section do |c|
c.repo_gem gem_repo2, "platform_specific", "1.0"
c.repo_gem gem_repo2, "platform_specific", "1.0", "java"
c.repo_gem gem_repo2, "platform_specific", "1.0", x64_mingw32
end
expect(lockfile).to eq <<~L expect(lockfile).to eq <<~L
PATH PATH
remote: . remote: .
@ -479,7 +483,9 @@ RSpec.describe "bundle install from an existing gemspec" do
CHECKSUMS CHECKSUMS
foo (1.0) foo (1.0)
#{expected_checksums} #{checksum_for_repo_gem gem_repo2, "platform_specific", "1.0"}
#{checksum_for_repo_gem gem_repo2, "platform_specific", "1.0", "java"}
#{x64_mingw_checksums}
BUNDLED WITH BUNDLED WITH
#{Bundler::VERSION} #{Bundler::VERSION}
@ -493,12 +499,6 @@ RSpec.describe "bundle install from an existing gemspec" do
it "keeps all platform dependencies in the lockfile" do it "keeps all platform dependencies in the lockfile" do
expect(the_bundle).to include_gems "foo 1.0", "platform_specific 1.0 RUBY" expect(the_bundle).to include_gems "foo 1.0", "platform_specific 1.0 RUBY"
expected_checksums = checksum_section do |c|
c.repo_gem gem_repo2, "platform_specific", "1.0"
c.repo_gem gem_repo2, "platform_specific", "1.0", "java"
c.repo_gem gem_repo2, "platform_specific", "1.0", x64_mingw32
end
expect(lockfile).to eq <<~L expect(lockfile).to eq <<~L
PATH PATH
remote: . remote: .
@ -523,7 +523,9 @@ RSpec.describe "bundle install from an existing gemspec" do
CHECKSUMS CHECKSUMS
foo (1.0) foo (1.0)
#{expected_checksums} #{checksum_for_repo_gem gem_repo2, "platform_specific", "1.0"}
#{checksum_for_repo_gem gem_repo2, "platform_specific", "1.0", "java"}
#{x64_mingw_checksums}
BUNDLED WITH BUNDLED WITH
#{Bundler::VERSION} #{Bundler::VERSION}
@ -538,13 +540,6 @@ RSpec.describe "bundle install from an existing gemspec" do
it "keeps all platform dependencies in the lockfile" do it "keeps all platform dependencies in the lockfile" do
expect(the_bundle).to include_gems "foo 1.0", "indirect_platform_specific 1.0", "platform_specific 1.0 RUBY" expect(the_bundle).to include_gems "foo 1.0", "indirect_platform_specific 1.0", "platform_specific 1.0 RUBY"
expected_checksums = checksum_section do |c|
c.repo_gem gem_repo2, "indirect_platform_specific", "1.0"
c.repo_gem gem_repo2, "platform_specific", "1.0"
c.repo_gem gem_repo2, "platform_specific", "1.0", "java"
c.repo_gem gem_repo2, "platform_specific", "1.0", x64_mingw32
end
expect(lockfile).to eq <<~L expect(lockfile).to eq <<~L
PATH PATH
remote: . remote: .
@ -571,7 +566,10 @@ RSpec.describe "bundle install from an existing gemspec" do
CHECKSUMS CHECKSUMS
foo (1.0) foo (1.0)
#{expected_checksums} #{checksum_for_repo_gem gem_repo2, "indirect_platform_specific", "1.0"}
#{checksum_for_repo_gem gem_repo2, "platform_specific", "1.0"}
#{checksum_for_repo_gem gem_repo2, "platform_specific", "1.0", "java"}
#{x64_mingw_checksums}
BUNDLED WITH BUNDLED WITH
#{Bundler::VERSION} #{Bundler::VERSION}

View File

@ -27,6 +27,33 @@ RSpec.describe "bundle install with gems on multiple sources" do
G G
end end
it "refuses to install mismatched checksum because one gem has been tampered with", :bundler => "< 3" do
bundle :install, :artifice => "compact_index", :raise_on_error => false
expect(exitstatus).to eq(37)
expect(err).to eq <<~E.strip
[DEPRECATED] Your Gemfile contains multiple global sources. Using `source` more than once without a block is a security risk, and may result in installing unexpected gems. To resolve this warning, use a block to indicate which gems should come from the secondary source.
Bundler found mismatched checksums. This is a potential security risk.
#{checksum_for_repo_gem(gem_repo1, "rack", "1.0.0")}
from the API at https://gem.repo1/
#{checksum_for_repo_gem(gem_repo3, "rack", "1.0.0")}
from the API at https://gem.repo3/
Mismatched checksums each have an authoritative source:
1. the API at https://gem.repo1/
2. the API at https://gem.repo3/
You may need to alter your Gemfile sources to resolve this issue.
To ignore checksum security warnings, disable checksum validation with
`bundle config set --local disable_checksum_validation true`
E
end
context "when checksum validation is disabled" do
before do
bundle "config set --local disable_checksum_validation true"
end
it "warns about ambiguous gems, but installs anyway, prioritizing sources last to first", :bundler => "< 3" do it "warns about ambiguous gems, but installs anyway, prioritizing sources last to first", :bundler => "< 3" do
bundle :install, :artifice => "compact_index" bundle :install, :artifice => "compact_index"
@ -50,6 +77,7 @@ RSpec.describe "bundle install with gems on multiple sources" do
expect(exitstatus).to eq(4) expect(exitstatus).to eq(4)
end end
end end
end
context "when different versions of the same gem are in multiple sources" do context "when different versions of the same gem are in multiple sources" do
let(:repo3_rack_version) { "1.2" } let(:repo3_rack_version) { "1.2" }
@ -101,7 +129,8 @@ RSpec.describe "bundle install with gems on multiple sources" do
end end
it "works in standalone mode", :bundler => "< 3" do it "works in standalone mode", :bundler => "< 3" do
bundle "install --standalone", :artifice => "compact_index" gem_checksum = checksum_for_repo_gem(gem_repo4, "foo", "1.0").split("-").last
bundle "install --standalone", :artifice => "compact_index", :env => { "BUNDLER_SPEC_FOO_CHECKSUM" => gem_checksum }
end end
end end
@ -279,8 +308,55 @@ RSpec.describe "bundle install with gems on multiple sources" do
G G
end end
it "installs from the other source and warns about ambiguous gems", :bundler => "< 3" do it "fails when the two sources don't have the same checksum", :bundler => "< 3" do
bundle :install, :artifice => "compact_index" bundle :install, :artifice => "compact_index", :raise_on_error => false
expect(err).to eq(<<~E.strip)
[DEPRECATED] Your Gemfile contains multiple global sources. Using `source` more than once without a block is a security risk, and may result in installing unexpected gems. To resolve this warning, use a block to indicate which gems should come from the secondary source.
Bundler found mismatched checksums. This is a potential security risk.
#{checksum_for_repo_gem(gem_repo2, "rack", "1.0.0")}
from the API at https://gem.repo2/
#{checksum_for_repo_gem(gem_repo1, "rack", "1.0.0")}
from the API at https://gem.repo1/
Mismatched checksums each have an authoritative source:
1. the API at https://gem.repo2/
2. the API at https://gem.repo1/
You may need to alter your Gemfile sources to resolve this issue.
To ignore checksum security warnings, disable checksum validation with
`bundle config set --local disable_checksum_validation true`
E
expect(exitstatus).to eq(37)
end
it "fails when the two sources agree, but the local gem calculates a different checksum", :bundler => "< 3" do
rack_checksum = "c0ffee11" * 8
bundle :install, :artifice => "compact_index", :env => { "BUNDLER_SPEC_RACK_CHECKSUM" => rack_checksum }, :raise_on_error => false
expect(err).to eq(<<~E.strip)
[DEPRECATED] Your Gemfile contains multiple global sources. Using `source` more than once without a block is a security risk, and may result in installing unexpected gems. To resolve this warning, use a block to indicate which gems should come from the secondary source.
Bundler found mismatched checksums. This is a potential security risk.
rack (1.0.0) sha256-#{rack_checksum}
from the API at https://gem.repo2/
and the API at https://gem.repo1/
#{checksum_for_repo_gem(gem_repo2, "rack", "1.0.0")}
from the gem at #{default_bundle_path("cache", "rack-1.0.0.gem")}
If you trust the API at https://gem.repo2/, to resolve this issue you can:
1. remove the gem at #{default_bundle_path("cache", "rack-1.0.0.gem")}
2. run `bundle install`
To ignore checksum security warnings, disable checksum validation with
`bundle config set --local disable_checksum_validation true`
E
expect(exitstatus).to eq(37)
end
it "installs from the other source and warns about ambiguous gems when the sources have the same checksum", :bundler => "< 3" do
gem_checksum = checksum_for_repo_gem(gem_repo2, "rack", "1.0.0").split("-").last
bundle :install, :artifice => "compact_index", :env => { "BUNDLER_SPEC_RACK_CHECKSUM" => gem_checksum, "DEBUG" => "1" }
expect(err).to include("Warning: the gem 'rack' was found in multiple sources.") expect(err).to include("Warning: the gem 'rack' was found in multiple sources.")
expect(err).to include("Installed from: https://gem.repo2") expect(err).to include("Installed from: https://gem.repo2")
@ -320,6 +396,49 @@ RSpec.describe "bundle install with gems on multiple sources" do
expect(lockfile).to eq(previous_lockfile) expect(lockfile).to eq(previous_lockfile)
end end
it "installs from the other source and warns about ambiguous gems when checksum validation is disabled", :bundler => "< 3" do
bundle "config set --local disable_checksum_validation true"
bundle :install, :artifice => "compact_index"
expect(err).to include("Warning: the gem 'rack' was found in multiple sources.")
expect(err).to include("Installed from: https://gem.repo2")
expected_checksums = checksum_section do |c|
c.no_checksum "depends_on_rack", "1.0.1"
c.no_checksum "rack", "1.0.0"
end
expect(lockfile).to eq <<~L
GEM
remote: https://gem.repo1/
remote: https://gem.repo2/
specs:
rack (1.0.0)
GEM
remote: https://gem.repo3/
specs:
depends_on_rack (1.0.1)
rack
PLATFORMS
#{local_platform}
DEPENDENCIES
depends_on_rack!
CHECKSUMS
#{expected_checksums}
BUNDLED WITH
#{Bundler::VERSION}
L
previous_lockfile = lockfile
expect(the_bundle).to include_gems("depends_on_rack 1.0.1", "rack 1.0.0")
expect(lockfile).to eq(previous_lockfile)
end
it "fails", :bundler => "3" do it "fails", :bundler => "3" do
bundle :install, :artifice => "compact_index", :raise_on_error => false bundle :install, :artifice => "compact_index", :raise_on_error => false
expect(err).to include("Each source after the first must include a block") expect(err).to include("Each source after the first must include a block")
@ -1167,8 +1286,9 @@ RSpec.describe "bundle install with gems on multiple sources" do
lockfile aggregate_gem_section_lockfile lockfile aggregate_gem_section_lockfile
end end
it "installs the existing lockfile but prints a warning", :bundler => "< 3" do it "installs the existing lockfile but prints a warning when checksum validation is disabled", :bundler => "< 3" do
bundle "config set --local deployment true" bundle "config set --local deployment true"
bundle "config set --local disable_checksum_validation true"
bundle "install", :artifice => "compact_index" bundle "install", :artifice => "compact_index"
@ -1177,6 +1297,33 @@ RSpec.describe "bundle install with gems on multiple sources" do
expect(the_bundle).to include_gems("rack 0.9.1", :source => "remote3") expect(the_bundle).to include_gems("rack 0.9.1", :source => "remote3")
end end
it "prints a checksum warning when the checksums from both sources do not match", :bundler => "< 3" do
bundle "config set --local deployment true"
bundle "install", :artifice => "compact_index", :raise_on_error => false
api_checksum1 = checksum_for_repo_gem(gem_repo1, "rack", "0.9.1").split("sha256-").last
api_checksum3 = checksum_for_repo_gem(gem_repo3, "rack", "0.9.1").split("sha256-").last
expect(exitstatus).to eq(37)
expect(err).to eq(<<~E.strip)
[DEPRECATED] Your lockfile contains a single rubygems source section with multiple remotes, which is insecure. Make sure you run `bundle install` in non frozen mode and commit the result to make your lockfile secure.
Bundler found mismatched checksums. This is a potential security risk.
rack (0.9.1) sha256-#{api_checksum3}
from the API at https://gem.repo3/
rack (0.9.1) sha256-#{api_checksum1}
from the API at https://gem.repo1/
Mismatched checksums each have an authoritative source:
1. the API at https://gem.repo3/
2. the API at https://gem.repo1/
You may need to alter your Gemfile sources to resolve this issue.
To ignore checksum security warnings, disable checksum validation with
`bundle config set --local disable_checksum_validation true`
E
end
it "refuses to install the existing lockfile and prints an error", :bundler => "3" do it "refuses to install the existing lockfile and prints an error", :bundler => "3" do
bundle "config set --local deployment true" bundle "config set --local deployment true"

View File

@ -759,6 +759,11 @@ RSpec.describe "bundle install with specific platforms" do
bundle "update" bundle "update"
expected_checksums = checksum_section do |c|
c.repo_gem gem_repo4, "nokogiri", "1.14.0", "x86_64-linux"
c.repo_gem gem_repo4, "sorbet-static", "0.5.10696", "x86_64-linux"
end
expect(lockfile).to eq <<~L expect(lockfile).to eq <<~L
GEM GEM
remote: #{file_uri_for(gem_repo4)}/ remote: #{file_uri_for(gem_repo4)}/
@ -773,6 +778,9 @@ RSpec.describe "bundle install with specific platforms" do
nokogiri nokogiri
sorbet-static sorbet-static
CHECKSUMS
#{expected_checksums}
BUNDLED WITH BUNDLED WITH
#{Bundler::VERSION} #{Bundler::VERSION}
L L

View File

@ -890,24 +890,20 @@ The checksum of /versions does not match the checksum provided by the server! So
default_cache_path.dirname.join("rack-1.0.0.gem") default_cache_path.dirname.join("rack-1.0.0.gem")
end end
expect(exitstatus).to eq(19) expect(exitstatus).to eq(37)
expect(err). expect(err).to eq <<~E.strip
to eq <<~E.strip Bundler found mismatched checksums. This is a potential security risk.
Bundler cannot continue installing rack (1.0.0). rack (1.0.0) sha256-2222222222222222222222222222222222222222222222222222222222222222
The checksum for the downloaded `rack-1.0.0.gem` does not match the known checksum for the gem. from the API at http://localgemserver.test/
This means the contents of the downloaded gem is different from what was uploaded to the server or first used by your teammates, and could be a potential security issue. rack (1.0.0) sha256-#{api_checksum}
from the gem at #{gem_path}
To resolve this issue: If you trust the API at http://localgemserver.test/, to resolve this issue you can:
1. delete the downloaded gem located at: `#{gem_path}` 1. remove the gem at #{gem_path}
2. run `bundle install` 2. run `bundle install`
If you are sure that the new checksum is correct, you can remove the `rack (1.0.0)` entry under the lockfile `CHECKSUMS` section and rerun `bundle install`. To ignore checksum security warnings, disable checksum validation with
`bundle config set --local disable_checksum_validation true`
If you wish to continue installing the downloaded gem, and are certain it does not pose a security issue despite the mismatching checksum, do the following:
1. run `bundle config set --local disable_checksum_validation true` to turn off checksum verification
2. run `bundle install`
(More info: The expected SHA256 checksum was "69b69b69b69b69b69b69b69b69b69b69b69b69b69b69b69b69b69b69b69b69b69b", but the checksum for the downloaded gem was "#{api_checksum}". The expected checksum came from: API response from http://localgemserver.test/)
E E
end end
@ -917,7 +913,7 @@ The checksum of /versions does not match the checksum provided by the server! So
gem "rack" gem "rack"
G G
expect(exitstatus).to eq(14) expect(exitstatus).to eq(14)
expect(err).to include("The given checksum for rack-0.9.1 (\"checksum!\") is not a valid SHA256 hexdigest nor base64digest") expect(err).to include("Invalid checksum for rack-0.9.1: \"checksum!\" is not a valid SHA256 hexdigest nor base64digest")
end end
it "does not raise when disable_checksum_validation is set" do it "does not raise when disable_checksum_validation is set" do

View File

@ -1769,7 +1769,7 @@ RSpec.describe "the lockfile format" do
gem "rack" gem "rack"
G G
expect(err).to match(/your lockfile contains merge conflicts/i) expect(err).to match(/your Gemfile.lock contains merge conflicts/i)
expect(err).to match(/git checkout HEAD -- Gemfile.lock/i) expect(err).to match(/git checkout HEAD -- Gemfile.lock/i)
end end

View File

@ -7,7 +7,8 @@ class CompactIndexWrongGemChecksum < CompactIndexAPI
etag_response do etag_response do
name = params[:name] name = params[:name]
gem = gems.find {|g| g.name == name } gem = gems.find {|g| g.name == name }
checksum = ENV.fetch("BUNDLER_SPEC_#{name.upcase}_CHECKSUM") { "ab" * 22 } # This generates the hexdigest "2222222222222222222222222222222222222222222222222222222222222222"
checksum = ENV.fetch("BUNDLER_SPEC_#{name.upcase}_CHECKSUM") { "IiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiI=" }
versions = gem ? gem.versions : [] versions = gem ? gem.versions : []
versions.each {|v| v.checksum = checksum } versions.each {|v| v.checksum = checksum }
CompactIndex.info(versions) CompactIndex.info(versions)

View File

@ -79,10 +79,12 @@ class CompactIndexAPI < Endpoint
reqs = d.requirement.requirements.map {|r| r.join(" ") }.join(", ") reqs = d.requirement.requirements.map {|r| r.join(" ") }.join(", ")
CompactIndex::Dependency.new(d.name, reqs) CompactIndex::Dependency.new(d.name, reqs)
end end
checksum = begin begin
checksum = ENV.fetch("BUNDLER_SPEC_#{name.upcase}_CHECKSUM") do
Digest(:SHA256).file("#{gem_repo}/gems/#{spec.original_name}.gem").hexdigest Digest(:SHA256).file("#{gem_repo}/gems/#{spec.original_name}.gem").hexdigest
end
rescue StandardError rescue StandardError
nil checksum = nil
end end
CompactIndex::GemVersion.new(spec.version.version, spec.platform.to_s, checksum, nil, CompactIndex::GemVersion.new(spec.version.version, spec.platform.to_s, checksum, nil,
deps, spec.required_ruby_version.to_s, spec.required_rubygems_version.to_s) deps, spec.required_ruby_version.to_s, spec.required_rubygems_version.to_s)

View File

@ -9,26 +9,22 @@ module Spec
end end
def repo_gem(repo, name, version, platform = Gem::Platform::RUBY) def repo_gem(repo, name, version, platform = Gem::Platform::RUBY)
gem_file = File.join(repo, "gems", "#{Bundler::GemHelpers.spec_full_name(name, version, platform)}.gem") name_tuple = Gem::NameTuple.new(name, version, platform)
gem_file = File.join(repo, "gems", "#{name_tuple.full_name}.gem")
File.open(gem_file, "rb") do |f| File.open(gem_file, "rb") do |f|
checksums = Bundler::Checksum.from_io(f, "ChecksumsBuilder") @checksums[name_tuple] = Bundler::Checksum.from_gem(f, "#{gem_file} (via ChecksumsBuilder#repo_gem)")
checksum_entry(checksums, name, version, platform)
end end
end end
def no_checksum(name, version, platform = Gem::Platform::RUBY) def no_checksum(name, version, platform = Gem::Platform::RUBY)
checksum_entry(nil, name, version, platform) name_tuple = Gem::NameTuple.new(name, version, platform)
end @checksums[name_tuple] = nil
def checksum_entry(checksums, name, version, platform = Gem::Platform::RUBY)
lock_name = Bundler::GemHelpers.lock_name(name, version, platform)
@checksums[lock_name] = checksums
end end
def to_lock def to_lock
@checksums.map do |lock_name, checksums| @checksums.map do |name_tuple, checksum|
checksums &&= " #{checksums.map(&:to_lock).join(",")}" checksum &&= " #{checksum.to_lock}"
" #{lock_name}#{checksums}\n" " #{name_tuple.lock_name}#{checksum}\n"
end.sort.join.strip end.sort.join.strip
end end
end end