[rubygems/rubygems] Refactor to checksums stored via source

This gets the specs passing, and handles the fact that we expect
checkums to be pinned only to a particular source

This also avoids reading in .gem files during lockfile generation,
instead allowing us to query the source for each resolved gem to grab
the checksum

Finally, this opens up a route to having user-stored checksum databases,
similar to how other package managers do this!

Add checksums to dev lockfiles

Handle full name conflicts from different original_platforms when adding checksums to store from compact index

Specs passing on Bundler 3

https://github.com/rubygems/rubygems/commit/86c7084e1c
This commit is contained in:
Samuel Giddins 2023-08-09 13:45:56 -07:00 committed by Hiroshi SHIBATA
parent 69d7e9a12e
commit c5fd94073f
No known key found for this signature in database
GPG Key ID: F9CF13417264FAC2
31 changed files with 536 additions and 259 deletions

View File

@ -2,31 +2,194 @@
module Bundler module Bundler
class Checksum class Checksum
class Store
attr_reader :store
protected :store
def initialize
@store = {}
end
def initialize_copy(o)
@store = {}
o.store.each do |k, v|
@store[k] = v.dup
end
end
def [](spec)
sums = @store[spec.full_name]
Checksum.new(spec.name, spec.version, spec.platform, sums&.transform_values(&:digest))
end
def register(spec, checksums)
register_full_name(spec.full_name, checksums)
end
def register_triple(name, version, platform, checksums)
register_full_name(GemHelpers.spec_full_name(name, version, platform), checksums)
end
def delete_full_name(full_name)
@store.delete(full_name)
end
def register_full_name(full_name, checksums)
sums = (@store[full_name] ||= {})
checksums.each do |checksum|
algo = checksum.algo
if multi = sums[algo]
multi.merge(checksum)
else
sums[algo] = Multi.new [checksum]
end
end
rescue SecurityError => e
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}
To resolve this issue:
1. delete any downloaded gems referenced above
2. run `bundle install`
If you are sure that the new checksum is correct, you can \
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
def use(other)
other.store.each do |k, v|
register_full_name k, v.values
end
end
end
class Single
attr_reader :algo, :digest, :source
def initialize(algo, digest, source)
@algo = algo
@digest = digest
@source = source
end
def ==(other)
other.is_a?(Single) && other.digest == digest && other.algo == algo && source == other.source
end
def hash
digest.hash
end
alias_method :eql?, :==
def to_s
"#{algo}-#{digest} (from #{source})"
end
end
class Multi
attr_reader :algo, :digest, :checksums
protected :checksums
def initialize(checksums)
@checksums = checksums
unless checksums && checksums.size > 0
raise ArgumentError, "must provide at least one checksum"
end
first = checksums.first
@algo = first.algo
@digest = first.digest
end
def initialize_copy(o)
@checksums = o.checksums.dup
@algo = o.algo
@digest = o.digest
end
def merge(other)
raise ArgumentError, "cannot merge checksums of different algorithms" unless algo == other.algo
unless digest == other.digest
raise SecurityError, <<~MESSAGE
#{other}
#{self} from:
* #{sources.join("\n* ")}
MESSAGE
end
case other
when Single
@checksums << other
when Multi
@checksums.concat(other.checksums)
else
raise ArgumentError
end
@checksums.uniq!
self
end
def sources
@checksums.map(&:source)
end
def to_s
"#{algo}-#{digest}"
end
end
attr_reader :name, :version, :platform, :checksums attr_reader :name, :version, :platform, :checksums
SHA256 = %r{\Asha256-([a-z0-9]{64}|[A-Za-z0-9+\/=]{44})\z}.freeze SHA256 = %r{\Asha256-([a-z0-9]{64}|[A-Za-z0-9+\/=]{44})\z}.freeze
private_constant :SHA256
def initialize(name, version, platform, checksums = []) def initialize(name, version, platform, checksums = {})
@name = name @name = name
@version = version @version = version
@platform = platform || Gem::Platform::RUBY @platform = platform || Gem::Platform::RUBY
@checksums = checksums @checksums = checksums || {}
# can expand this validation when we support more hashing algos later # can expand this validation when we support more hashing algos later
if @checksums.any? && @checksums.all? {|c| c !~ SHA256 } if !@checksums.is_a?(::Hash) || (@checksums.any? && !@checksums.key?("sha256"))
raise ArgumentError, "invalid checksums (#{@checksums.inspect})"
end
if @checksums.any? {|_, checksum| !checksum.is_a?(String) }
raise ArgumentError, "invalid checksums (#{@checksums})" raise ArgumentError, "invalid checksums (#{@checksums})"
end end
end end
def self.digest_from_file_source(file_source) def self.digests_from_file_source(file_source, digest_algorithms: %w[sha256])
raise ArgumentError, "not a valid file source: #{file_source}" unless file_source.respond_to?(:with_read_io) raise ArgumentError, "not a valid file source: #{file_source}" unless file_source.respond_to?(:with_read_io)
digests = digest_algorithms.map do |digest_algorithm|
[digest_algorithm.to_s, Bundler::SharedHelpers.digest(digest_algorithm.upcase).new]
end.to_h
file_source.with_read_io do |io| file_source.with_read_io do |io|
digest = Bundler::SharedHelpers.digest(:SHA256).new until io.eof?
digest << io.read(16_384) until io.eof? block = io.read(16_384)
digests.each_value {|digest| digest << block }
end
io.rewind io.rewind
digest
end end
digests
end end
def full_name def full_name
@ -42,12 +205,51 @@ module Bundler
def to_lock def to_lock
out = String.new out = String.new
out << " #{GemHelpers.lock_name(name, version, platform)}" out << " #{GemHelpers.lock_name(name, version, platform)}"
out << " #{sha256}" if sha256 checksums.sort_by(&:first).each_with_index do |(algo, checksum), idx|
out << (idx.zero? ? " " : ",")
out << algo << "-" << checksum
end
out << "\n" out << "\n"
out out
end end
def match?(other)
return false unless match_spec?(other)
match_digests?(other.checksums)
end
def match_digests?(digests)
return true if checksums.empty? && digests.empty?
common_algos = checksums.keys & digests.keys
return true if common_algos.empty?
common_algos.all? do |algo|
checksums[algo] == digests[algo]
end
end
def merge!(other)
raise ArgumentError, "can't merge checksums for different specs" unless match_spec?(other)
merge_digests!(other.checksums)
end
def merge_digests!(digests)
if digests.any? {|_, checksum| !checksum.is_a?(String) }
raise ArgumentError, "invalid checksums (#{digests})"
end
@checksums = @checksums.merge(digests) do |algo, ours, theirs|
if ours != theirs
raise ArgumentError, "Digest mismatch for #{algo}:\n\t* #{ours.inspect}\n\t* #{theirs.inspect}"
end
ours
end
self
end
private private
def sha256 def sha256

View File

@ -15,7 +15,6 @@ module Bundler
:dependencies, :dependencies,
:locked_deps, :locked_deps,
:locked_gems, :locked_gems,
:locked_checksums,
:platforms, :platforms,
:ruby_version, :ruby_version,
:lockfile, :lockfile,
@ -93,7 +92,6 @@ module Bundler
@locked_bundler_version = @locked_gems.bundler_version @locked_bundler_version = @locked_gems.bundler_version
@locked_ruby_version = @locked_gems.ruby_version @locked_ruby_version = @locked_gems.ruby_version
@originally_locked_specs = SpecSet.new(@locked_gems.specs) @originally_locked_specs = SpecSet.new(@locked_gems.specs)
@locked_checksums = @locked_gems.checksums
if unlock != true if unlock != true
@locked_deps = @locked_gems.dependencies @locked_deps = @locked_gems.dependencies
@ -114,7 +112,6 @@ module Bundler
@originally_locked_specs = @locked_specs @originally_locked_specs = @locked_specs
@locked_sources = [] @locked_sources = []
@locked_platforms = [] @locked_platforms = []
@locked_checksums = {}
end end
locked_gem_sources = @locked_sources.select {|s| s.is_a?(Source::Rubygems) } locked_gem_sources = @locked_sources.select {|s| s.is_a?(Source::Rubygems) }
@ -753,6 +750,11 @@ module Bundler
changes = sources.replace_sources!(@locked_sources) changes = sources.replace_sources!(@locked_sources)
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
# store for a source, even when doing a full update
if @locked_gems && locked_source = @locked_gems.sources.find {|s| s == source }
source.checksum_store&.use(locked_source.checksum_store)
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
# gem), unlock it. For git sources, this means to unlock the revision, which # gem), unlock it. For git sources, this means to unlock the revision, which

View File

@ -125,7 +125,17 @@ module Bundler
next unless v next unless v
case k.to_s case k.to_s
when "checksum" when "checksum"
@checksum = v.last next if Bundler.settings[:disable_checksum_validation]
digest = v.last
if digest.length == 64
# nothing to do, it's a hexdigest
elsif digest.length == 44
# 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
@checksum = Checksum::Single.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

@ -81,7 +81,7 @@ module Bundler
:HTTPRequestURITooLong, :HTTPUnauthorized, :HTTPUnprocessableEntity, :HTTPRequestURITooLong, :HTTPUnauthorized, :HTTPUnprocessableEntity,
:HTTPUnsupportedMediaType, :HTTPVersionNotSupported].freeze :HTTPUnsupportedMediaType, :HTTPVersionNotSupported].freeze
FAIL_ERRORS = begin FAIL_ERRORS = begin
fail_errors = [AuthenticationRequiredError, BadAuthenticationError, AuthenticationForbiddenError, FallbackError] fail_errors = [AuthenticationRequiredError, BadAuthenticationError, AuthenticationForbiddenError, FallbackError, SecurityError]
fail_errors << Gem::Requirement::BadRequirementError fail_errors << Gem::Requirement::BadRequirementError
fail_errors.concat(NET_ERRORS.map {|e| Net.const_get(e) }) fail_errors.concat(NET_ERRORS.map {|e| Net.const_get(e) })
end.freeze end.freeze
@ -139,7 +139,16 @@ 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) EndpointSpecification.new(name, version, platform, self, dependencies, metadata).tap do |es|
unless index.local_search(es).empty?
# Duplicate spec.full_names, different spec.original_names
# 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.delete_full_name(es.full_name)
end
source.checksum_store.register(es, [es.checksum]) if source && es.checksum
end
else else
RemoteSpecification.new(name, version, platform, self) RemoteSpecification.new(name, version, platform, self)
end end

View File

@ -67,19 +67,6 @@ module Bundler
out out
end end
def materialize_for_checksum(&blk)
#
# See comment about #ruby_platform_materializes_to_ruby_platform?
# If the old lockfile format is present where there is no specific
# platform, then we should skip locking checksums as it is not
# deterministic which platform variant is locked.
#
return unless ruby_platform_materializes_to_ruby_platform?
s = materialize_for_installation
yield s if block_given?
end
def materialize_for_installation def materialize_for_installation
source.local! source.local!
@ -126,7 +113,7 @@ module Bundler
end end
def to_s def to_s
@__to_s ||= GemHelpers.lock_name(name, version, platform) @to_s ||= GemHelpers.lock_name(name, version, platform)
end end
def git_version def git_version

View File

@ -68,16 +68,11 @@ module Bundler
def add_checksums def add_checksums
out << "\nCHECKSUMS\n" out << "\nCHECKSUMS\n"
definition.resolve.sort_by(&:full_name).each do |spec|
checksum = spec.to_checksum if spec.respond_to?(:to_checksum)
if spec.is_a?(LazySpecification)
spec.materialize_for_checksum do |materialized_spec|
checksum ||= materialized_spec.to_checksum if materialized_spec&.respond_to?(:to_checksum)
end
end
checksum ||= definition.locked_checksums[spec.full_name]
out << checksum.to_lock if checksum empty_store = Checksum::Store.new
definition.resolve.sort_by(&:full_name).each do |spec|
out << (spec.source.checksum_store || empty_store)[spec].to_lock
end end
end end

View File

@ -2,6 +2,28 @@
module Bundler module Bundler
class LockfileParser class LockfileParser
class Position
attr_reader :line, :column
def initialize(line, column)
@line = line
@column = column
end
def advance!(string)
lines = string.count("\n")
if lines > 0
@line += lines
@column = string.length - string.rindex("\n")
else
@column += string.length
end
end
def to_s
"#{line}:#{column}"
end
end
attr_reader :sources, :dependencies, :specs, :platforms, :bundler_version, :ruby_version, :checksums attr_reader :sources, :dependencies, :specs, :platforms, :bundler_version, :ruby_version, :checksums
BUNDLED = "BUNDLED WITH" BUNDLED = "BUNDLED WITH"
@ -22,7 +44,7 @@ module Bundler
Gem::Version.create("1.10") => [BUNDLED].freeze, Gem::Version.create("1.10") => [BUNDLED].freeze,
Gem::Version.create("1.12") => [RUBY].freeze, Gem::Version.create("1.12") => [RUBY].freeze,
Gem::Version.create("1.13") => [PLUGIN].freeze, Gem::Version.create("1.13") => [PLUGIN].freeze,
Gem::Version.create("2.4.0") => [CHECKSUMS].freeze, Gem::Version.create("2.5.0") => [CHECKSUMS].freeze,
}.freeze }.freeze
KNOWN_SECTIONS = SECTIONS_BY_VERSION_INTRODUCED.values.flatten!.freeze KNOWN_SECTIONS = SECTIONS_BY_VERSION_INTRODUCED.values.flatten!.freeze
@ -66,15 +88,20 @@ module Bundler
@sources = [] @sources = []
@dependencies = {} @dependencies = {}
@parse_method = nil @parse_method = nil
@checksums = {}
@specs = {} @specs = {}
@lockfile_path = begin
SharedHelpers.relative_lockfile_path
rescue GemfileNotFound
"Gemfile.lock"
end
@pos = Position.new(1, 1)
if lockfile.match?(/<<<<<<<|=======|>>>>>>>|\|\|\|\|\|\|\|/) if lockfile.match?(/<<<<<<<|=======|>>>>>>>|\|\|\|\|\|\|\|/)
raise LockfileError, "Your lockfile contains merge conflicts.\n" \ raise LockfileError, "Your #{@lockfile_path} contains merge conflicts.\n" \
"Run `git checkout HEAD -- #{SharedHelpers.relative_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)+/) do |line| lockfile.split(/((?:\r?\n)+)/).each_slice(2) do |line, whitespace|
if SOURCE.include?(line) if SOURCE.include?(line)
@parse_method = :parse_source @parse_method = :parse_source
parse_source(line) parse_source(line)
@ -93,12 +120,15 @@ module Bundler
elsif @parse_method elsif @parse_method
send(@parse_method, line) send(@parse_method, line)
end end
@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
Bundler.ui.debug(e) Bundler.ui.debug(e)
raise LockfileError, "Your lockfile is unreadable. Run `rm #{SharedHelpers.relative_lockfile_path}` " \ raise LockfileError, "Your lockfile is unreadable. Run `rm #{@lockfile_path}` " \
"and then `bundle install` to generate a new lockfile." "and then `bundle install` to generate a new lockfile. The error occurred while " \
"evaluating #{@lockfile_path}:#{@pos}"
end end
def may_include_redundant_platform_specific_gems? def may_include_redundant_platform_specific_gems?
@ -149,7 +179,7 @@ module Bundler
(?:#{space}\(([^-]*) # Space, followed by version (?:#{space}\(([^-]*) # Space, followed by version
(?:-(.*))?\))? # Optional platform (?:-(.*))?\))? # Optional platform
(!)? # Optional pinned marker (!)? # Optional pinned marker
(?:#{space}(.*))? # Optional checksum (?:#{space}([^ ]+))? # Optional checksum
$ # Line end $ # Line end
/xo.freeze /xo.freeze
@ -183,19 +213,31 @@ module Bundler
end end
def parse_checksum(line) def parse_checksum(line)
if line =~ NAME_VERSION return unless line =~ NAME_VERSION
spaces = $1
return unless spaces.size == 2
name = $2
version = $3
platform = $4
checksum = $6
version = Gem::Version.new(version) spaces = $1
platform = platform ? Gem::Platform.new(platform) : Gem::Platform::RUBY return unless spaces.size == 2
checksum = Bundler::Checksum.new(name, version, platform, [checksum]) name = $2
@checksums[checksum.full_name] = checksum version = $3
platform = $4
checksums = $6
return unless checksums
version = Gem::Version.new(version)
platform = platform ? Gem::Platform.new(platform) : Gem::Platform::RUBY
source = "#{@lockfile_path}:#{@pos} in the CHECKSUMS lockfile section"
checksums = checksums.split(",").map do |c|
algo, digest = c.split("-", 2)
Checksum::Single.new(algo, digest, source)
end end
full_name = GemHelpers.spec_full_name(name, version, platform)
# Don't raise exception if there's a checksum for a gem that's not in the lockfile,
# we prefer to heal invalid lockfiles
return unless spec = @specs[full_name]
spec.source.checksum_store.register_full_name(full_name, checksums)
end end
def parse_spec(line) def parse_spec(line)

View File

@ -39,7 +39,7 @@ module Bundler
# is present to be compatible with `Definition` and is used by # is present to be compatible with `Definition` and is used by
# rubygems source. # rubygems source.
module Source module Source
attr_reader :uri, :options, :name attr_reader :uri, :options, :name, :checksum_store
attr_accessor :dependency_names attr_accessor :dependency_names
def initialize(opts) def initialize(opts)
@ -48,6 +48,7 @@ module Bundler
@uri = opts["uri"] @uri = opts["uri"]
@type = opts["type"] @type = opts["type"]
@name = opts["name"] || "#{@type} at #{@uri}" @name = opts["name"] || "#{@type} at #{@uri}"
@checksum_store = Checksum::Store.new
end end
# This is used by the default `spec` method to constructs the # This is used by the default `spec` method to constructs the

View File

@ -93,56 +93,12 @@ module Bundler
" #{source.revision[0..6]}" " #{source.revision[0..6]}"
end end
# we don't get the checksum from a server like we could with EndpointSpecs
# calculating the checksum from the file on disk still provides some measure of security
# if it changes from install to install, that is cause for concern
def to_checksum
@checksum ||= begin
gem_path = fetch_gem
require "rubygems/package"
package = Gem::Package.new(gem_path)
digest = Bundler::Checksum.digest_from_file_source(package.gem)
digest.hexdigest!
end
digest = "sha256-#{@checksum}" if @checksum
Bundler::Checksum.new(name, version, platform, [digest])
end
private private
def to_ary def to_ary
nil nil
end end
def fetch_gem
fetch_platform
cache_path = download_cache_path || default_cache_path_for_rubygems_dir
gem_path = "#{cache_path}/#{file_name}"
return gem_path if File.exist?(gem_path)
SharedHelpers.filesystem_access(cache_path) do |p|
FileUtils.mkdir_p(p)
end
Bundler.rubygems.download_gem(self, remote.uri, cache_path)
gem_path
end
def download_cache_path
return unless Bundler.feature_flag.global_gem_cache?
return unless remote
return unless remote.cache_slug
Bundler.user_cache.join("gems", remote.cache_slug)
end
def default_cache_path_for_rubygems_dir
"#{Bundler.bundle_path}/cache"
end
def _remote_specification def _remote_specification
@_remote_specification ||= @spec_fetcher.fetch_spec([@name, @version, @original_platform]) @_remote_specification ||= @spec_fetcher.fetch_spec([@name, @version, @original_platform])
@_remote_specification || raise(GemspecError, "Gemspec data for #{full_name} was" \ @_remote_specification || raise(GemspecError, "Gemspec data for #{full_name} was" \

View File

@ -61,7 +61,7 @@ module Bundler
end end
def pre_install_checks def pre_install_checks
super && validate_bundler_checksum(options[:bundler_expected_checksum]) super && validate_bundler_checksum(options[:bundler_checksum_store])
end end
def build_extensions def build_extensions
@ -115,55 +115,56 @@ 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) def validate_bundler_checksum(checksum_store)
return true if Bundler.settings[:disable_checksum_validation] return true if Bundler.settings[:disable_checksum_validation]
return true unless checksum
return true unless source = @package.instance_variable_get(:@gem) return true unless source = @package.instance_variable_get(:@gem)
return true unless source.respond_to?(:with_read_io) return true unless source.respond_to?(:with_read_io)
digest = Bundler::Checksum.digest_from_file_source(source) digests = Bundler::Checksum.digests_from_file_source(source).transform_values(&:hexdigest!)
calculated_checksum = send(checksum_type(checksum), digest)
unless calculated_checksum == checksum checksum = checksum_store[spec]
raise SecurityError, <<-MESSAGE unless checksum.match_digests?(digests)
expected = checksum_store.send(:store)[spec.full_name]
raise SecurityError, <<~MESSAGE
Bundler cannot continue installing #{spec.name} (#{spec.version}). Bundler cannot continue installing #{spec.name} (#{spec.version}).
The checksum for the downloaded `#{spec.full_name}.gem` does not match \ The checksum for the downloaded `#{spec.full_name}.gem` does not match \
the checksum given by the server. This means the contents of the downloaded \ the known checksum for the gem.
gem is different from what was uploaded to the server, and could be a potential security issue. 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: To resolve this issue:
1. delete the downloaded gem located at: `#{spec.gem_dir}/#{spec.full_name}.gem` 1. delete the downloaded gem located at: `#{source.path}`
2. run `bundle install` 2. run `bundle install`
If you are sure that the new checksum is correct, you can \
remove the `#{GemHelpers.lock_name spec.name, spec.version, spec.platform}` 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 \ 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: security issue despite the mismatching checksum, do the following:
1. run `bundle config set --local disable_checksum_validation true` to turn off checksum verification 1. run `bundle config set --local disable_checksum_validation true` to turn off checksum verification
2. run `bundle install` 2. run `bundle install`
(More info: The expected SHA256 checksum was #{checksum.inspect}, but the \ #{expected.map do |algo, multi|
checksum for the downloaded gem was #{calculated_checksum.inspect}.) next unless actual = digests[algo]
next if actual == multi
"(More info: The expected #{algo.upcase} checksum was #{multi.digest.inspect}, but the " \
"checksum for the downloaded gem was #{actual.inspect}. The expected checksum came from: #{multi.sources.join(", ")})"
end.compact.join("\n")}
MESSAGE MESSAGE
end end
register_digests(digests, checksum_store, source)
true true
end end
def checksum_type(checksum) def register_digests(digests, checksum_store, source)
case checksum.length checksum_store.register(
when 64 then :hexdigest! spec,
when 44 then :base64digest! digests.map {|algo, digest| Checksum::Single.new(algo, digest, "downloaded gem @ `#{source.path}`") }
else raise InstallError, "The given checksum for #{spec.full_name} (#{checksum.inspect}) is not a valid SHA256 hexdigest nor base64digest" )
end
end
def hexdigest!(digest)
digest.hexdigest!
end
def base64digest!(digest)
if digest.respond_to?(:base64digest!)
digest.base64digest!
else
[digest.digest!].pack("m0")
end
end end
end end
end end

View File

@ -11,6 +11,8 @@ module Bundler
attr_accessor :dependency_names attr_accessor :dependency_names
attr_reader :checksum_store
def unmet_deps def unmet_deps
specs.unmet_dependency_names specs.unmet_dependency_names
end end

View File

@ -14,6 +14,7 @@ module Bundler
DEFAULT_GLOB = "{,*,*/*}.gemspec" DEFAULT_GLOB = "{,*,*/*}.gemspec"
def initialize(options) def initialize(options)
@checksum_store = Checksum::Store.new
@options = options.dup @options = options.dup
@glob = options["glob"] || DEFAULT_GLOB @glob = options["glob"] || DEFAULT_GLOB

View File

@ -19,6 +19,7 @@ module Bundler
@allow_remote = false @allow_remote = false
@allow_cached = false @allow_cached = false
@allow_local = options["allow_local"] || false @allow_local = options["allow_local"] || false
@checksum_store = Checksum::Store.new
Array(options["remotes"]).reverse_each {|r| add_remote(r) } Array(options["remotes"]).reverse_each {|r| add_remote(r) }
end end
@ -177,7 +178,7 @@ module Bundler
:wrappers => true, :wrappers => true,
:env_shebang => true, :env_shebang => true,
:build_args => options[:build_args], :build_args => options[:build_args],
:bundler_expected_checksum => spec.respond_to?(:checksum) && spec.checksum, :bundler_checksum_store => spec.source.checksum_store,
:bundler_extension_cache_path => extension_cache_path(spec) :bundler_extension_cache_path => extension_cache_path(spec)
) )

View File

@ -93,16 +93,6 @@ module Bundler
stub.raw_require_paths stub.raw_require_paths
end end
def add_checksum(checksum)
@checksum ||= checksum
end
def to_checksum
return Bundler::Checksum.new(name, version, platform, ["sha256-#{checksum}"]) if checksum
_remote_specification&.to_checksum
end
private private
def _remote_specification def _remote_specification

View File

@ -761,8 +761,6 @@ class Gem::Specification < Gem::BasicSpecification
attr_accessor :specification_version attr_accessor :specification_version
attr_reader :checksum
def self._all # :nodoc: def self._all # :nodoc:
@@all ||= Gem.loaded_specs.values | stubs.map(&:to_spec) @@all ||= Gem.loaded_specs.values | stubs.map(&:to_spec)
end end
@ -2740,22 +2738,4 @@ class Gem::Specification < Gem::BasicSpecification
def raw_require_paths # :nodoc: def raw_require_paths # :nodoc:
@require_paths @require_paths
end end
def add_checksum(checksum)
@checksum ||= checksum
end
# if we don't get the checksum from the server
# calculating the checksum from the file on disk still provides some measure of security
# if it changes from install to install, that is cause for concern
def to_checksum
return Bundler::Checksum.new(name, version, platform, ["sha256-#{checksum}"]) if checksum
return Bundler::Checksum.new(name, version, platform) unless File.exist?(cache_file)
require "rubygems/package"
package = Gem::Package.new(cache_file)
digest = Bundler::Checksum.digest_from_file_source(package.gem)
calculated_checksum = digest.hexdigest!
Bundler::Checksum.new(name, version, platform, ["sha256-#{calculated_checksum}"]) if calculated_checksum
end
end end

View File

@ -168,7 +168,7 @@ RSpec.describe Bundler::Definition do
only_java only_java
CHECKSUMS CHECKSUMS
#{checksum_for_repo_gem gem_repo1, "only_java", "1.1", "java"} only_java (1.1-java)
BUNDLED WITH BUNDLED WITH
#{Bundler::VERSION} #{Bundler::VERSION}

View File

@ -283,6 +283,7 @@ RSpec.describe "bundle cache" do
:rubygems_version => "1.3.2" :rubygems_version => "1.3.2"
simulate_new_machine simulate_new_machine
pending "Causes checksum mismatch exception"
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

@ -426,8 +426,8 @@ RSpec.describe "bundle check" do
depends_on_rack! depends_on_rack!
CHECKSUMS CHECKSUMS
#{checksum_for_repo_gem gem_repo4, "depends_on_rack", "1.0"} depends_on_rack (1.0)
#{checksum_for_repo_gem gem_repo4, "rack", "1.0"} rack (1.0)
BUNDLED WITH BUNDLED WITH
#{Bundler::VERSION} #{Bundler::VERSION}

View File

@ -905,7 +905,7 @@ RSpec.describe "bundle clean" do
bundle :lock bundle :lock
bundle "config set without development" bundle "config set without development"
bundle "config set path vendor/bundle" bundle "config set path vendor/bundle"
bundle "install" bundle "install", :verbose => true
bundle :clean bundle :clean
very_simple_binary_extensions_dir = very_simple_binary_extensions_dir =

View File

@ -65,7 +65,9 @@ RSpec.describe "bundle lock" do
it "prints a lockfile when there is no existing lockfile with --print" do it "prints a lockfile when there is no existing lockfile with --print" do
bundle "lock --print" bundle "lock --print"
expect(out).to eq(@lockfile.strip) # No checksums because no way to get them from a file uri source
# + no existing lockfile that has them
expect(out).to eq(@lockfile.strip.gsub(/ sha256-[a-f0-9]+$/, ""))
end end
it "prints a lockfile when there is an existing lockfile with --print" do it "prints a lockfile when there is an existing lockfile with --print" do
@ -79,7 +81,9 @@ RSpec.describe "bundle lock" do
it "writes a lockfile when there is no existing lockfile" do it "writes a lockfile when there is no existing lockfile" do
bundle "lock" bundle "lock"
expect(read_lockfile).to eq(@lockfile) # No checksums because no way to get them from a file uri source
# + no existing lockfile that has them
expect(read_lockfile).to eq(@lockfile.gsub(/ sha256-[a-f0-9]+$/, ""))
end end
it "writes a lockfile when there is an outdated lockfile using --update" do it "writes a lockfile when there is an outdated lockfile using --update" do
@ -93,7 +97,8 @@ RSpec.describe "bundle lock" do
bundle "lock --update", :env => { "BUNDLE_FROZEN" => "true" } bundle "lock --update", :env => { "BUNDLE_FROZEN" => "true" }
expect(read_lockfile).to eq(@lockfile) # No checksums for the updated gems
expect(read_lockfile).to eq(@lockfile.gsub(/( \(2\.3\.2\)) sha256-[a-f0-9]+$/, "\\1"))
end end
it "does not fetch remote specs when using the --local option" do it "does not fetch remote specs when using the --local option" do
@ -120,7 +125,7 @@ RSpec.describe "bundle lock" do
foo foo
CHECKSUMS CHECKSUMS
#{checksum_for_repo_gem repo, "foo", "1.0"} #{checksum_for_repo_gem repo, "foo", "1.0", :empty => true}
BUNDLED WITH BUNDLED WITH
#{Bundler::VERSION} #{Bundler::VERSION}
@ -136,7 +141,7 @@ RSpec.describe "bundle lock" do
bundle "lock --lockfile=lock" bundle "lock --lockfile=lock"
expect(out).to match(/Writing lockfile to.+lock/) expect(out).to match(/Writing lockfile to.+lock/)
expect(read_lockfile("lock")).to eq(@lockfile) expect(read_lockfile("lock")).to eq(@lockfile.gsub(/ sha256-[a-f0-9]+$/, ""))
expect { read_lockfile }.to raise_error(Errno::ENOENT) expect { read_lockfile }.to raise_error(Errno::ENOENT)
end end
@ -156,7 +161,7 @@ RSpec.describe "bundle lock" do
c.repo_gem repo, "weakling", "0.0.3" c.repo_gem repo, "weakling", "0.0.3"
end end
lockfile = strip_lockfile(<<-L) lockfile = <<~L
GEM GEM
remote: #{file_uri_for(repo)}/ remote: #{file_uri_for(repo)}/
specs: specs:
@ -203,7 +208,17 @@ RSpec.describe "bundle lock" do
bundle "lock --update rails rake" bundle "lock --update rails rake"
expect(read_lockfile).to eq(@lockfile) expect(read_lockfile).to eq(@lockfile.gsub(/( \((?:2\.3\.2|13\.0\.1)\)) sha256-[a-f0-9]+$/, "\\1"))
end
it "preserves unknown checksum algorithms" do
lockfile @lockfile.gsub(/(sha256-[a-f0-9]+)$/, "constant-true,\\1,xyz-123")
previous_lockfile = read_lockfile
bundle "lock"
expect(read_lockfile).to eq(previous_lockfile)
end end
it "does not unlock git sources when only uri shape changes" do it "does not unlock git sources when only uri shape changes" do
@ -280,7 +295,7 @@ RSpec.describe "bundle lock" do
G G
bundle "config set without test" bundle "config set without test"
bundle "config set path vendor/bundle" bundle "config set path vendor/bundle"
bundle "lock" bundle "lock", :verbose => true
expect(bundled_app("vendor/bundle")).not_to exist expect(bundled_app("vendor/bundle")).not_to exist
end end
@ -611,10 +626,10 @@ RSpec.describe "bundle lock" do
mixlib-shellout mixlib-shellout
CHECKSUMS CHECKSUMS
#{checksum_for_repo_gem gem_repo4, "ffi", "1.9.14", "x86-mingw32"} #{checksum_for_repo_gem gem_repo4, "ffi", "1.9.14", "x86-mingw32", :empty => true}
#{checksum_for_repo_gem gem_repo4, "gssapi", "1.2.0"} #{checksum_for_repo_gem gem_repo4, "gssapi", "1.2.0", :empty => true}
#{checksum_for_repo_gem gem_repo4, "mixlib-shellout", "2.2.6", "universal-mingw32"} #{checksum_for_repo_gem gem_repo4, "mixlib-shellout", "2.2.6", "universal-mingw32", :empty => true}
#{checksum_for_repo_gem gem_repo4, "win32-process", "0.8.3"} #{checksum_for_repo_gem gem_repo4, "win32-process", "0.8.3", :empty => true}
BUNDLED WITH BUNDLED WITH
#{Bundler::VERSION} #{Bundler::VERSION}
@ -646,12 +661,12 @@ RSpec.describe "bundle lock" do
mixlib-shellout mixlib-shellout
CHECKSUMS CHECKSUMS
#{checksum_for_repo_gem gem_repo4, "ffi", "1.9.14"} #{checksum_for_repo_gem gem_repo4, "ffi", "1.9.14", :empty => true}
#{checksum_for_repo_gem gem_repo4, "ffi", "1.9.14", "x86-mingw32"} #{checksum_for_repo_gem gem_repo4, "ffi", "1.9.14", "x86-mingw32", :empty => true}
#{checksum_for_repo_gem gem_repo4, "gssapi", "1.2.0"} #{checksum_for_repo_gem gem_repo4, "gssapi", "1.2.0", :empty => true}
#{checksum_for_repo_gem gem_repo4, "mixlib-shellout", "2.2.6"} #{checksum_for_repo_gem gem_repo4, "mixlib-shellout", "2.2.6", :empty => true}
#{checksum_for_repo_gem gem_repo4, "mixlib-shellout", "2.2.6", "universal-mingw32"} #{checksum_for_repo_gem gem_repo4, "mixlib-shellout", "2.2.6", "universal-mingw32", :empty => true}
#{checksum_for_repo_gem gem_repo4, "win32-process", "0.8.3"} #{checksum_for_repo_gem gem_repo4, "win32-process", "0.8.3", :empty => true}
BUNDLED WITH BUNDLED WITH
#{Bundler::VERSION} #{Bundler::VERSION}
@ -732,8 +747,8 @@ RSpec.describe "bundle lock" do
libv8 libv8
CHECKSUMS CHECKSUMS
#{checksum_for_repo_gem gem_repo4, "libv8", "8.4.255.0", "x86_64-darwin-19"} #{checksum_for_repo_gem gem_repo4, "libv8", "8.4.255.0", "x86_64-darwin-19", :empty => true}
#{checksum_for_repo_gem gem_repo4, "libv8", "8.4.255.0", "x86_64-darwin-20"} #{checksum_for_repo_gem gem_repo4, "libv8", "8.4.255.0", "x86_64-darwin-20", :empty => true}
BUNDLED WITH BUNDLED WITH
#{Bundler::VERSION} #{Bundler::VERSION}
@ -928,13 +943,15 @@ RSpec.describe "bundle lock" do
end end
context "when an update is available" do context "when an update is available" do
let(:repo) { gem_repo2 } let(:repo) do
before do
lockfile(@lockfile)
build_repo2 do build_repo2 do
build_gem "foo", "2.0" build_gem "foo", "2.0"
end end
gem_repo2
end
before do
lockfile(@lockfile)
end end
it "does not implicitly update" do it "does not implicitly update" do
@ -952,7 +969,7 @@ RSpec.describe "bundle lock" do
c.repo_gem repo, "weakling", "0.0.3" c.repo_gem repo, "weakling", "0.0.3"
end end
expected_lockfile = strip_lockfile(<<-L) expected_lockfile = <<~L
GEM GEM
remote: #{file_uri_for(repo)}/ remote: #{file_uri_for(repo)}/
specs: specs:
@ -1003,13 +1020,15 @@ RSpec.describe "bundle lock" do
c.repo_gem repo, "activerecord", "2.3.2" c.repo_gem repo, "activerecord", "2.3.2"
c.repo_gem repo, "activeresource", "2.3.2" c.repo_gem repo, "activeresource", "2.3.2"
c.repo_gem repo, "activesupport", "2.3.2" c.repo_gem repo, "activesupport", "2.3.2"
c.repo_gem repo, "foo", "2.0" # We don't have a checksum for foo 2,
# since it is not downloaded by bundle lock, therefore we don't include it
# c.repo_gem repo, "foo", "2.0"
c.repo_gem repo, "rails", "2.3.2" c.repo_gem repo, "rails", "2.3.2"
c.repo_gem repo, "rake", "13.0.1" c.repo_gem repo, "rake", "13.0.1"
c.repo_gem repo, "weakling", "0.0.3" c.repo_gem repo, "weakling", "0.0.3"
end end
expected_lockfile = strip_lockfile(<<-L) expected_lockfile = <<~L
GEM GEM
remote: #{file_uri_for(repo)}/ remote: #{file_uri_for(repo)}/
specs: specs:
@ -1041,7 +1060,7 @@ RSpec.describe "bundle lock" do
weakling weakling
CHECKSUMS CHECKSUMS
#{expected_checksums} #{expected_checksums.prepend(" ").lines(:chomp => true).append(" foo (2.0)").sort.join("\n")}
BUNDLED WITH BUNDLED WITH
#{Bundler::VERSION} #{Bundler::VERSION}
@ -1118,8 +1137,8 @@ RSpec.describe "bundle lock" do
debug debug
CHECKSUMS CHECKSUMS
#{checksum_for_repo_gem gem_repo4, "debug", "1.6.3"} #{checksum_for_repo_gem gem_repo4, "debug", "1.6.3", :empty => true}
#{checksum_for_repo_gem gem_repo4, "irb", "1.5.0"} #{checksum_for_repo_gem gem_repo4, "irb", "1.5.0", :empty => true}
BUNDLED WITH BUNDLED WITH
#{Bundler::VERSION} #{Bundler::VERSION}
@ -1424,6 +1443,10 @@ RSpec.describe "bundle lock" do
DEPENDENCIES DEPENDENCIES
foo! foo!
CHECKSUMS
#{checksum_for_repo_gem(gem_repo4, "foo", "1.0", :empty => true)}
#{checksum_for_repo_gem(gem_repo4, "nokogiri", "1.14.2", :empty => true)}
BUNDLED WITH BUNDLED WITH
#{Bundler::VERSION} #{Bundler::VERSION}
L L
@ -1507,6 +1530,12 @@ RSpec.describe "bundle lock" do
activesupport (= 7.0.4.3) activesupport (= 7.0.4.3)
govuk_app_config govuk_app_config
CHECKSUMS
#{checksum_for_repo_gem gem_repo4, "actionpack", "7.0.4.3", :empty => true}
#{checksum_for_repo_gem gem_repo4, "activesupport", "7.0.4.3", :empty => true}
#{checksum_for_repo_gem gem_repo4, "govuk_app_config", "4.13.0", :empty => true}
#{checksum_for_repo_gem gem_repo4, "railties", "7.0.4.3", :empty => true}
BUNDLED WITH BUNDLED WITH
#{Bundler::VERSION} #{Bundler::VERSION}
L L

View File

@ -300,7 +300,7 @@ RSpec.describe "bundle update" do
previous_lockfile = lockfile previous_lockfile = lockfile
bundle "lock --update" bundle "lock --update", :env => { "DEBUG" => "1" }, :verbose => true
expect(lockfile).to eq(previous_lockfile) expect(lockfile).to eq(previous_lockfile)
end end
@ -539,6 +539,10 @@ RSpec.describe "bundle update" do
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)
# needed because regressing to versions already present on the system
# won't add a checksum
expected_lockfile = expected_lockfile.gsub(/ sha256-[a-f0-9]+$/, "")
lockfile original_lockfile lockfile original_lockfile
bundle "update" bundle "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")
@ -547,26 +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 <<~L expect(lockfile).to eq expected_lockfile
GEM
remote: #{file_uri_for(gem_repo4)}/
specs:
activesupport (6.0.4.1)
tzinfo (~> 1.1)
tzinfo (1.2.9)
PLATFORMS
#{lockfile_platforms}
DEPENDENCIES
activesupport (~> 6.0.0)
CHECKSUMS
#{expected_checksums}
BUNDLED WITH
#{Bundler::VERSION}
L
end end
end end
@ -1283,11 +1268,26 @@ RSpec.describe "bundle update --bundler" do
source "#{file_uri_for(gem_repo4)}" source "#{file_uri_for(gem_repo4)}"
gem "rack" gem "rack"
G G
lockfile lockfile.sub(/(^\s*)#{Bundler::VERSION}($)/, '\11.0.0\2')
expected_checksum = checksum_for_repo_gem(gem_repo4, "rack", "1.0") expected_checksum = checksum_for_repo_gem(gem_repo4, "rack", "1.0")
expect(lockfile).to eq <<~L
GEM
remote: #{file_uri_for(gem_repo4)}/
specs:
rack (1.0)
FileUtils.rm_r gem_repo4 PLATFORMS
#{lockfile_platforms}
DEPENDENCIES
rack
CHECKSUMS
#{expected_checksum}
BUNDLED WITH
#{Bundler::VERSION}
L
lockfile lockfile.sub(/(^\s*)#{Bundler::VERSION}($)/, '\11.0.0\2')
bundle :update, :bundler => true, :artifice => "compact_index", :verbose => true bundle :update, :bundler => true, :artifice => "compact_index", :verbose => true
expect(out).to include("Using bundler #{Bundler::VERSION}") expect(out).to include("Using bundler #{Bundler::VERSION}")
@ -1717,14 +1717,6 @@ RSpec.describe "bundle update conservative" do
it "should only change direct dependencies when updating the lockfile with --conservative" do it "should only change direct dependencies when updating the lockfile with --conservative" do
bundle "lock --update --conservative" bundle "lock --update --conservative"
expected_checksums = construct_checksum_section do |c|
c.repo_gem gem_repo4, "isolated_dep", "2.0.1"
c.repo_gem gem_repo4, "isolated_owner", "1.0.2"
c.repo_gem gem_repo4, "shared_dep", "5.0.1"
c.repo_gem gem_repo4, "shared_owner_a", "3.0.2"
c.repo_gem gem_repo4, "shared_owner_b", "4.0.2"
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)}/
@ -1747,7 +1739,11 @@ RSpec.describe "bundle update conservative" do
shared_owner_b shared_owner_b
CHECKSUMS CHECKSUMS
#{expected_checksums} isolated_dep (2.0.1)
isolated_owner (1.0.2)
shared_dep (5.0.1)
shared_owner_a (3.0.2)
shared_owner_b (4.0.2)
BUNDLED WITH BUNDLED WITH
#{Bundler::VERSION} #{Bundler::VERSION}

View File

@ -721,7 +721,7 @@ RSpec.describe "bundle install from an existing gemspec" do
CHECKSUMS CHECKSUMS
activeadmin (2.9.0) activeadmin (2.9.0)
#{checksum_for_repo_gem gem_repo4, "jruby-openssl", "0.10.7", "java"} jruby-openssl (0.10.7-java)
#{checksum_for_repo_gem gem_repo4, "railties", "6.1.4"} #{checksum_for_repo_gem gem_repo4, "railties", "6.1.4"}
BUNDLED WITH BUNDLED WITH

View File

@ -39,9 +39,9 @@ RSpec.describe "bundle install with install_if conditionals" do
CHECKSUMS CHECKSUMS
#{checksum_for_repo_gem gem_repo1, "activesupport", "2.3.5"} #{checksum_for_repo_gem gem_repo1, "activesupport", "2.3.5"}
#{checksum_for_repo_gem gem_repo1, "foo", "1.0"} #{checksum_for_repo_gem gem_repo1, "foo", "1.0", :empty => true}
#{checksum_for_repo_gem gem_repo1, "rack", "1.0.0"} #{checksum_for_repo_gem gem_repo1, "rack", "1.0.0"}
#{checksum_for_repo_gem gem_repo1, "thin", "1.0"} #{checksum_for_repo_gem gem_repo1, "thin", "1.0", :empty => true}
BUNDLED WITH BUNDLED WITH
#{Bundler::VERSION} #{Bundler::VERSION}

View File

@ -849,6 +849,10 @@ RSpec.describe "bundle install with explicit source paths" do
DEPENDENCIES DEPENDENCIES
foo! foo!
CHECKSUMS
foo (1.0)
rack (0.9.1)
BUNDLED WITH BUNDLED WITH
#{Bundler::VERSION} #{Bundler::VERSION}
G G

View File

@ -226,6 +226,12 @@ RSpec.describe "bundle install across platforms" do
pry pry
CHECKSUMS CHECKSUMS
#{checksum_for_repo_gem gem_repo4, "coderay", "1.1.2"}
#{checksum_for_repo_gem gem_repo4, "empyrean", "0.1.0"}
#{checksum_for_repo_gem gem_repo4, "ffi", "1.9.23", "java"}
#{checksum_for_repo_gem gem_repo4, "method_source", "0.9.0"}
#{checksum_for_repo_gem gem_repo4, "pry", "0.11.3", "java"}
#{checksum_for_repo_gem gem_repo4, "spoon", "0.0.6"}
BUNDLED WITH BUNDLED WITH
#{Bundler::VERSION} #{Bundler::VERSION}
@ -260,6 +266,13 @@ RSpec.describe "bundle install across platforms" do
pry pry
CHECKSUMS CHECKSUMS
#{checksum_for_repo_gem gem_repo4, "coderay", "1.1.2"}
#{checksum_for_repo_gem gem_repo4, "empyrean", "0.1.0"}
#{checksum_for_repo_gem gem_repo4, "ffi", "1.9.23", "java"}
#{checksum_for_repo_gem gem_repo4, "method_source", "0.9.0"}
pry (0.11.3)
#{checksum_for_repo_gem gem_repo4, "pry", "0.11.3", "java"}
#{checksum_for_repo_gem gem_repo4, "spoon", "0.0.6"}
BUNDLED WITH BUNDLED WITH
#{Bundler::VERSION} #{Bundler::VERSION}
@ -295,6 +308,12 @@ RSpec.describe "bundle install across platforms" do
pry pry
CHECKSUMS CHECKSUMS
#{checksum_for_repo_gem gem_repo4, "coderay", "1.1.2"}
#{checksum_for_repo_gem gem_repo4, "empyrean", "0.1.0"}
#{checksum_for_repo_gem gem_repo4, "ffi", "1.9.23", "java"}
#{checksum_for_repo_gem gem_repo4, "method_source", "0.9.0"}
#{checksum_for_repo_gem gem_repo4, "pry", "0.11.3", "java"}
#{checksum_for_repo_gem gem_repo4, "spoon", "0.0.6"}
BUNDLED WITH BUNDLED WITH
1.16.1 1.16.1
@ -407,7 +426,7 @@ RSpec.describe "bundle install across platforms" do
CHECKSUMS CHECKSUMS
#{checksum_for_repo_gem(gem_repo1, "platform_specific", "1.0")} #{checksum_for_repo_gem(gem_repo1, "platform_specific", "1.0")}
#{checksum_for_repo_gem(gem_repo1, "platform_specific", "1.0", "java")} #{checksum_for_repo_gem(gem_repo1, "platform_specific", "1.0", "java", :empty => true)}
BUNDLED WITH BUNDLED WITH
#{Bundler::VERSION} #{Bundler::VERSION}

View File

@ -79,6 +79,9 @@ RSpec.describe "bundle install with specific platforms" do
DEPENDENCIES DEPENDENCIES
google-protobuf google-protobuf
CHECKSUMS
google-protobuf (3.0.0.alpha.4.0)
BUNDLED WITH BUNDLED WITH
2.1.4 2.1.4
L L
@ -102,6 +105,7 @@ RSpec.describe "bundle install with specific platforms" do
google-protobuf google-protobuf
CHECKSUMS CHECKSUMS
google-protobuf (3.0.0.alpha.5.0.5.1)
BUNDLED WITH BUNDLED WITH
#{Bundler::VERSION} #{Bundler::VERSION}
@ -622,8 +626,8 @@ RSpec.describe "bundle install with specific platforms" do
sorbet-static sorbet-static
CHECKSUMS CHECKSUMS
#{checksum_for_repo_gem gem_repo4, "nokogiri", "1.13.0", "x86_64-darwin"} #{checksum_for_repo_gem gem_repo4, "nokogiri", "1.13.0", "x86_64-darwin", :empty => true}
#{checksum_for_repo_gem gem_repo4, "sorbet-static", "0.5.10601", "x86_64-darwin"} #{checksum_for_repo_gem gem_repo4, "sorbet-static", "0.5.10601", "x86_64-darwin", :empty => true}
BUNDLED WITH BUNDLED WITH
#{Bundler::VERSION} #{Bundler::VERSION}
@ -807,6 +811,10 @@ RSpec.describe "bundle install with specific platforms" do
DEPENDENCIES DEPENDENCIES
sorbet-static (= 0.5.10549) sorbet-static (= 0.5.10549)
CHECKSUMS
#{checksum_for_repo_gem gem_repo4, "sorbet-static", "0.5.10549", "universal-darwin-20"}
#{checksum_for_repo_gem gem_repo4, "sorbet-static", "0.5.10549", "universal-darwin-21"}
BUNDLED WITH BUNDLED WITH
#{Bundler::VERSION} #{Bundler::VERSION}
L L
@ -828,7 +836,7 @@ RSpec.describe "bundle install with specific platforms" do
CHECKSUMS CHECKSUMS
#{checksum_for_repo_gem gem_repo4, "sorbet-static", "0.5.10549", "universal-darwin-20"} #{checksum_for_repo_gem gem_repo4, "sorbet-static", "0.5.10549", "universal-darwin-20"}
#{checksum_for_repo_gem gem_repo4, "sorbet-static", "0.5.10549", "universal-darwin-21"} #{checksum_for_repo_gem gem_repo4, "sorbet-static", "0.5.10549", "universal-darwin-21", :empty => true}
BUNDLED WITH BUNDLED WITH
#{Bundler::VERSION} #{Bundler::VERSION}
@ -884,15 +892,15 @@ RSpec.describe "bundle install with specific platforms" do
nokogiri (1.13.8-#{Gem::Platform.local}) nokogiri (1.13.8-#{Gem::Platform.local})
PLATFORMS PLATFORMS
#{lockfile_platforms_for([specific_local_platform, "ruby"])} #{lockfile_platforms("ruby")}
DEPENDENCIES DEPENDENCIES
nokogiri nokogiri
tzinfo (~> 1.2) tzinfo (~> 1.2)
CHECKSUMS CHECKSUMS
#{checksum_for_repo_gem gem_repo4, "nokogiri", "1.13.8"} #{checksum_for_repo_gem gem_repo4, "nokogiri", "1.13.8", :empty => true}
#{checksum_for_repo_gem gem_repo4, "nokogiri", "1.13.8", "arm64-darwin-22"} #{checksum_for_repo_gem gem_repo4, "nokogiri", "1.13.8", Gem::Platform.local, :empty => true}
BUNDLED WITH BUNDLED WITH
#{Bundler::VERSION} #{Bundler::VERSION}
@ -946,6 +954,10 @@ RSpec.describe "bundle install with specific platforms" do
concurrent-ruby concurrent-ruby
rack rack
CHECKSUMS
#{checksum_for_repo_gem gem_repo4, "concurrent-ruby", "1.2.2", :empty => true}
#{checksum_for_repo_gem gem_repo4, "rack", "3.0.7", :empty => true}
BUNDLED WITH BUNDLED WITH
#{Bundler::VERSION} #{Bundler::VERSION}
L L

View File

@ -882,18 +882,33 @@ The checksum of /versions does not match the checksum provided by the server! So
gem "rack" gem "rack"
G G
api_checksum = Spec::Checksums::ChecksumsBuilder.new.repo_gem(gem_repo1, "rack", "1.0.0").first.checksums.fetch("sha256")
gem_path = if Bundler.feature_flag.global_gem_cache?
default_cache_path.dirname.join("cache", "gems", "localgemserver.test.80.dd34752a738ee965a2a4298dc16db6c5", "rack-1.0.0.gem")
else
default_cache_path.dirname.join("rack-1.0.0.gem")
end
expect(exitstatus).to eq(19) expect(exitstatus).to eq(19)
expect(err). expect(err).
to include("Bundler cannot continue installing rack (1.0.0)."). to eq <<~E.strip
and include("The checksum for the downloaded `rack-1.0.0.gem` does not match the checksum given by the server."). Bundler cannot continue installing rack (1.0.0).
and include("This means the contents of the downloaded gem is different from what was uploaded to the server, and could be a potential security issue."). The checksum for the downloaded `rack-1.0.0.gem` does not match the known checksum for the gem.
and include("To resolve this issue:"). 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.
and include("1. delete the downloaded gem located at: `#{default_bundle_path}/gems/rack-1.0.0/rack-1.0.0.gem`").
and include("2. run `bundle install`"). To resolve this issue:
and include("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. delete the downloaded gem located at: `#{gem_path}`
and include("1. run `bundle config set --local disable_checksum_validation true` to turn off checksum verification"). 2. run `bundle install`
and include("2. run `bundle install`").
and match(/\(More info: The expected SHA256 checksum was "#{"ab" * 22}", but the checksum for the downloaded gem was ".+?"\.\)/) 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`.
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
end end
it "raises when the checksum is the wrong length" do it "raises when the checksum is the wrong length" do
@ -901,8 +916,8 @@ The checksum of /versions does not match the checksum provided by the server! So
source "#{source_uri}" source "#{source_uri}"
gem "rack" gem "rack"
G G
expect(exitstatus).to eq(5) expect(exitstatus).to eq(14)
expect(err).to include("The given checksum for rack-1.0.0 (\"checksum!\") is not a valid SHA256 hexdigest nor base64digest") expect(err).to include("The given 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

@ -161,7 +161,8 @@ RSpec.context "when resolving a bundle that includes yanked gems, but unlocking
foo foo
CHECKSUMS CHECKSUMS
#{checksum_for_repo_gem(gem_repo4, "bar", "2.0.0")} #{checksum_for_repo_gem(gem_repo4, "bar", "2.0.0", :empty => true)}
#{checksum_for_repo_gem(gem_repo4, "foo", "9.0.0", :empty => true)}
BUNDLED WITH BUNDLED WITH
#{Bundler::VERSION} #{Bundler::VERSION}

View File

@ -146,6 +146,9 @@ RSpec.describe "the lockfile format" do
DEPENDENCIES DEPENDENCIES
rack rack
CHECKSUMS
#{checksum_for_repo_gem(gem_repo2, "rack", "1.0.0")}
BUNDLED WITH BUNDLED WITH
#{version} #{version}
L L
@ -171,6 +174,9 @@ RSpec.describe "the lockfile format" do
DEPENDENCIES DEPENDENCIES
rack rack
CHECKSUMS
#{checksum_for_repo_gem(gem_repo2, "rack", "1.0.0")}
BUNDLED WITH BUNDLED WITH
#{version} #{version}
G G
@ -677,6 +683,10 @@ RSpec.describe "the lockfile format" do
DEPENDENCIES DEPENDENCIES
ckeditor! ckeditor!
CHECKSUMS
#{checksum_for_repo_gem(gem_repo4, "ckeditor", "4.0.8", :empty => true)}
#{checksum_for_repo_gem(gem_repo4, "orm_adapter", "0.4.1", :empty => true)}
BUNDLED WITH BUNDLED WITH
#{Bundler::VERSION} #{Bundler::VERSION}
L L
@ -1516,6 +1526,10 @@ RSpec.describe "the lockfile format" do
DEPENDENCIES DEPENDENCIES
direct_dependency direct_dependency
CHECKSUMS
#{checksum_for_repo_gem(gem_repo4, "direct_dependency", "4.5.6")}
#{checksum_for_repo_gem(gem_repo4, "indirect_dependency", "1.2.3")}
BUNDLED WITH BUNDLED WITH
#{Bundler::VERSION} #{Bundler::VERSION}
G G
@ -1570,6 +1584,10 @@ RSpec.describe "the lockfile format" do
DEPENDENCIES DEPENDENCIES
minitest-bisect minitest-bisect
CHECKSUMS
#{checksum_for_repo_gem(gem_repo4, "minitest-bisect", "1.6.0")}
#{checksum_for_repo_gem(gem_repo4, "path_expander", "1.1.1")}
BUNDLED WITH BUNDLED WITH
#{Bundler::VERSION} #{Bundler::VERSION}
L L

View File

@ -48,6 +48,9 @@ RSpec.configure do |config|
config.silence_filter_announcements = !ENV["TEST_ENV_NUMBER"].nil? config.silence_filter_announcements = !ENV["TEST_ENV_NUMBER"].nil?
config.backtrace_exclusion_patterns <<
%r{./spec/(spec_helper\.rb|support/.+)}
config.disable_monkey_patching! config.disable_monkey_patching!
# Since failures cause us to keep a bunch of long strings in memory, stop # Since failures cause us to keep a bunch of long strings in memory, stop

View File

@ -7,19 +7,19 @@ module Spec
@checksums = [] @checksums = []
end end
def repo_gem(gem_repo, gem_name, gem_version, platform = nil) def repo_gem(gem_repo, gem_name, gem_version, platform = nil, empty: false)
gem_file = if platform gem_file = if platform
"#{gem_repo}/gems/#{gem_name}-#{gem_version}-#{platform}.gem" "#{gem_repo}/gems/#{gem_name}-#{gem_version}-#{platform}.gem"
else else
"#{gem_repo}/gems/#{gem_name}-#{gem_version}.gem" "#{gem_repo}/gems/#{gem_name}-#{gem_version}.gem"
end end
checksum = sha256_checksum(gem_file) checksum = { "sha256" => sha256_checksum(gem_file) } unless empty
@checksums << Bundler::Checksum.new(gem_name, gem_version, platform, [checksum]) @checksums << Bundler::Checksum.new(gem_name, gem_version, platform, checksum)
end end
def to_lock def to_lock
@checksums.map(&:to_lock).join.strip @checksums.map(&:to_lock).sort.join.strip
end end
private private
@ -29,7 +29,7 @@ module Spec
digest = Bundler::SharedHelpers.digest(:SHA256).new digest = Bundler::SharedHelpers.digest(:SHA256).new
digest << f.read(16_384) until f.eof? digest << f.read(16_384) until f.eof?
"sha256-#{digest.hexdigest!}" digest.hexdigest!
end end
end end
end end
@ -42,9 +42,9 @@ module Spec
checksums.to_lock checksums.to_lock
end end
def checksum_for_repo_gem(gem_repo, gem_name, gem_version, platform = nil) def checksum_for_repo_gem(*args, **kwargs)
construct_checksum_section do |c| construct_checksum_section do |c|
c.repo_gem(gem_repo, gem_name, gem_version, platform) c.repo_gem(*args, **kwargs)
end end
end end
end end