Class: Gem::Guardian::RubygemsClient

Inherits:
Object
  • Object
show all
Defined in:
lib/gem/guardian/rubygems_client.rb

Overview

Resolves gem sources, reads registry metadata, and downloads gem artifacts.

The client deliberately separates source discovery, checksum-provider lookup, provenance lookup, and artifact download. This lets gem-guardian support RubyGems.org, RubyGems-compatible private registries, and publisher-provided checksum URLs without coupling verification to one registry API. rubocop:disable Metrics/ClassLength

Defined Under Namespace

Classes: TrustedPublishingProvenance

Constant Summary collapse

SOURCE_COMMIT_PATTERN =

Matches the Source Commit field on the RubyGems provenance page.

%r{Source Commit\s+([A-Za-z0-9._/-]+@[A-Za-z0-9._-]+)}i
BUILD_FILE_PATTERN =

Matches the Build File field on the RubyGems provenance page.

/Build File\s+([^\s]+)/i
LOG_ENTRY_PATTERN =

Matches the transparency log URL shown on the RubyGems provenance page.

%r{transparency log entry\s*(https?://[^\s]+)}i
SHA256_PATTERN =

Matches the SHA256 checksum shown on the RubyGems provenance page.

/SHA 256 checksum\s*([a-f0-9]{64})/i
WORKFLOW_PATTERN =

Matches the provenance workflow label shown on the RubyGems provenance page.

/
  Built and signed on\s+
  ([A-Za-z0-9 ._-]+?)
  (?:\s+Build summary|\s+Source Commit|\z)
/ix
DEFAULT_HOST =

Default RubyGems.org endpoint used by the client.

"https://rubygems.org"
MAX_REDIRECTS =

Maximum number of HTTP redirects followed for API and artifact requests.

5
OPEN_TIMEOUT =

Connection timeout, in seconds, for direct HTTP requests.

10
READ_TIMEOUT =

Read timeout, in seconds, for direct HTTP requests.

30

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(host: DEFAULT_HOST, http: Net::HTTP, credentials: Bundler.settings, spec_fetcher: Gem::SpecFetcher.fetcher, sources: Gem.sources, checksum_providers: nil) ⇒ RubygemsClient

Returns a new instance of RubygemsClient.

Parameters:

  • host (String) (defaults to: DEFAULT_HOST)

    default RubyGems host used for API requests when a dependency has no source

  • http (#get_response) (defaults to: Net::HTTP)

    HTTP client used for metadata and artifact requests

  • credentials (Object) (defaults to: Bundler.settings)

    Bundler settings-like object used to resolve source credentials

  • spec_fetcher (Gem::SpecFetcher) (defaults to: Gem::SpecFetcher.fetcher)

    RubyGems spec fetcher used for source discovery

  • sources (Gem::SourceList, Array<Gem::Source>) (defaults to: Gem.sources)

    configured RubyGems sources

  • checksum_providers (Array<#checksum_for>, nil) (defaults to: nil)

    ordered checksum providers. Defaults to RubyGems API and compact index providers.



68
69
70
71
72
73
74
75
76
77
# File 'lib/gem/guardian/rubygems_client.rb', line 68

def initialize(host: DEFAULT_HOST, http: Net::HTTP, credentials: Bundler.settings,
               spec_fetcher: Gem::SpecFetcher.fetcher, sources: Gem.sources,
               checksum_providers: nil)
  @host = host.delete_suffix("/")
  @http = http
  @credentials = credentials
  @spec_fetcher = spec_fetcher
  @sources = sources
  @checksum_providers = checksum_providers || default_checksum_providers
end

Class Method Details

.default_checksum_providersArray<#checksum_for>

Built-in checksum providers used when no project configuration overrides provider order.

Returns:

  • (Array<#checksum_for>)

    RubyGems.org API and compact-index providers



57
58
59
# File 'lib/gem/guardian/rubygems_client.rb', line 57

def self.default_checksum_providers
  [ChecksumProvider::RubyGemsApi.new, ChecksumProvider::CompactIndex.new]
end

Instance Method Details

#compact_index_registry_checksum(dependency) ⇒ ChecksumProvider::Result?

Returns checksum metadata from the RubyGems/Bundler compact index.

Parameters:

  • dependency (Dependency)

    dependency to look up

Returns:



167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
# File 'lib/gem/guardian/rubygems_client.rb', line 167

def compact_index_registry_checksum(dependency)
  host = host_for(dependency)
  info_path = "/info/#{dependency.name}"
  info = get(info_path, host:, progress: false)
  sha = compact_index_checksum_for(info, dependency)
  return if blank?(sha)

  ChecksumProvider::Result.new(
    sha256: sha.downcase,
    source: :registry,
    provider: "compact-index",
    verification_uri: "#{host.delete_suffix("/")}#{info_path}"
  )
rescue StandardError
  nil
end

#download_gem(dependency, destination) ⇒ String

Downloads the .gem file for +dependency+ into +destination+.

RubyGems is used for source/spec resolution, but gem-guardian performs the artifact download itself. This keeps verification deterministic, applies explicit HTTP timeouts, avoids RubyGems installer-side behavior, and prevents Gem::Source#download from emitting progress output or hanging in internal fetch paths.

Parameters:

  • dependency (Dependency)

    dependency to resolve and download

  • destination (String)

    path where the downloaded artifact should be written

Returns:

  • (String)

    destination path

Raises:



209
210
211
212
213
214
# File 'lib/gem/guardian/rubygems_client.rb', line 209

def download_gem(dependency, destination)
  spec, source = resolve_spec_and_source(dependency)
  download_gem_uri(gem_uri(source, spec), destination)
rescue StandardError => e
  raise ArtifactFetchError, "Could not fetch #{dependency.gem_filename}: #{e.message}"
end

#expected_sha256(dependency) ⇒ String

Returns the expected SHA256 checksum for +dependency+.

This compatibility method returns only the digest. Prefer #registry_checksum when callers need provider metadata such as the verification URI or provider name.

Parameters:

  • dependency (Dependency)

    dependency to look up

Returns:

  • (String)

    SHA256 digest from the first checksum provider that can answer

Raises:



108
109
110
111
112
113
114
# File 'lib/gem/guardian/rubygems_client.rb', line 108

def expected_sha256(dependency)
  checksum = registry_checksum(dependency)
  return checksum.sha256 if checksum

  raise ChecksumNotFound,
        "No SHA256 found for #{dependency.name} #{dependency.version} #{dependency.platform}"
end

#registry_checksum(dependency) ⇒ ChecksumProvider::Result?

Returns registry or publisher supplied checksum metadata for +dependency+.

Providers are tried in order. The first provider that returns a checksum becomes the independent checksum source. This allows RubyGems.org, compact index registries, and publisher-controlled checksum URLs to participate in the same verification flow.

Parameters:

  • dependency (Dependency)

    dependency to look up

Returns:



125
126
127
128
129
130
131
132
133
134
# File 'lib/gem/guardian/rubygems_client.rb', line 125

def registry_checksum(dependency)
  @checksum_providers.each do |provider|
    checksum = provider.checksum_for(dependency, client: self)
    return checksum if checksum
  rescue StandardError
    next
  end

  nil
end

#resolve_dependency(dependency) ⇒ Dependency

Returns +dependency+ with its source populated from the configured RubyGems sources.

Explicit verification starts with a source-less dependency, unlike Bundler lockfile verification where Bundler has already recorded the remote. Resolving through RubyGems keeps gem-guardian aligned with gem install behavior for private registries such as GitHub Packages, Gemfury, CodeArtifact, or self-hosted RubyGems-compatible servers.

Parameters:

  • dependency (Dependency)

    dependency that may not include a source URI

Returns:

  • (Dependency)

    dependency with a sanitized source URI when resolution succeeds



89
90
91
92
93
94
95
96
97
# File 'lib/gem/guardian/rubygems_client.rb', line 89

def resolve_dependency(dependency)
  return dependency unless blank?(dependency.source)

  _spec, source = resolve_spec_and_source(dependency)
  Dependency.new(name: dependency.name, version: dependency.version, platform: dependency.platform,
                 source: sanitized_source_uri(source))
rescue StandardError
  dependency
end

#rubygems_api_checksum(dependency) ⇒ ChecksumProvider::Result?

Returns checksum metadata from the RubyGems.org-style versions API.

Parameters:

  • dependency (Dependency)

    dependency to look up

Returns:



148
149
150
151
152
153
154
155
156
157
158
159
160
161
# File 'lib/gem/guardian/rubygems_client.rb', line 148

def rubygems_api_checksum(dependency)
  version = matching_version(dependency)
  sha = version && version_checksum(version)
  return if blank?(sha)

  ChecksumProvider::Result.new(
    sha256: sha.downcase,
    source: :registry,
    provider: "rubygems-api",
    verification_uri: "#{host_for(dependency).delete_suffix("/")}/api/v1/versions/#{dependency.name}.json"
  )
rescue StandardError
  nil
end

#sanitize_uri(uri) ⇒ String

Returns a sanitized URI string suitable for reports.

Parameters:

  • uri (URI, String)

    URI that may contain credentials

Returns:

  • (String)

    URI with password/token material redacted



140
141
142
# File 'lib/gem/guardian/rubygems_client.rb', line 140

def sanitize_uri(uri)
  sanitized_uri(uri)
end

#trusted_publishing_provenance(dependency) ⇒ TrustedPublishingProvenance?

Returns trusted publishing provenance data for +dependency+ when RubyGems exposes it.

Parameters:

  • dependency (Dependency)

    dependency to inspect

Returns:



188
189
190
191
192
193
194
195
# File 'lib/gem/guardian/rubygems_client.rb', line 188

def trusted_publishing_provenance(dependency)
  return nil unless ruby_gems_org_source?(dependency)

  version = matching_version(dependency)
  version && provenance_for(version) ||
    attestation_api_provenance(dependency) ||
    version_page_provenance(dependency)
end