Module: Gem::PqTlsPolicy

Defined in:
lib/rubygems_pq_tls_policy.rb,
lib/rubygems_pq_tls_policy/error.rb,
lib/rubygems_pq_tls_policy/patch.rb,
lib/rubygems_pq_tls_policy/config.rb,
lib/rubygems_pq_tls_policy/version.rb,
lib/rubygems_pq_tls_policy/diagnostic.rb,
lib/rubygems_pq_tls_policy/certificate_signature.rb

Defined Under Namespace

Modules: CertificateSignature, Diagnostic, PerConnectionCheck, RequestHTTPSPoolPatch Classes: Config, Error, InvalidConfiguration, UnsupportedRuntime, Violation

Constant Summary collapse

VERSION =
"1.2.0"

Class Method Summary collapse

Class Method Details

.check_net_http_connection!(http) ⇒ Object



49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
# File 'lib/rubygems_pq_tls_policy.rb', line 49

def check_net_http_connection!(http)
  config = self.config
  return true unless config.enabled?

  config.validate!

  socket = net_http_ssl_socket(http)
  group = socket&.respond_to?(:group) ? socket.group : nil
  hostname = http.respond_to?(:address) ? http.address : "unknown"

  if config.trace?
    warn "[rubygems:tls] host=#{hostname} version=#{safe_ssl_version(socket)} cipher=#{safe_cipher_name(socket)} group=#{group.inspect}"
  end

  if config.pq_required? && !config.allowed_groups.include?(group)
    raise Gem::PqTlsPolicy::Violation,
      "RubyGems gem-server TLS key exchange policy violation: " \
      "host=#{hostname.inspect} used group=#{group.inspect}; " \
      "allowed=#{config.allowed_groups.join(',')}"
  end

  check_certificate_signature_policy!(socket, hostname, config)

  true
end

.configObject



111
112
113
# File 'lib/rubygems_pq_tls_policy/config.rb', line 111

def self.config
  Config.new
end

.extend_net_http_connection!(http) ⇒ Object



44
45
46
47
# File 'lib/rubygems_pq_tls_policy.rb', line 44

def extend_net_http_connection!(http)
  http.extend(PerConnectionCheck) unless http.is_a?(PerConnectionCheck)
  http
end

.install!(config = self.config) ⇒ Object



27
28
29
30
31
32
33
34
# File 'lib/rubygems_pq_tls_policy.rb', line 27

def install!(config = self.config)
  return true if installed?

  validate_runtime!(config)
  Gem::Request::HTTPSPool.prepend(RequestHTTPSPoolPatch)
  @installed = true
  true
end

.install_if_enabledObject



13
14
15
16
17
18
19
# File 'lib/rubygems_pq_tls_policy.rb', line 13

def install_if_enabled
  config = self.config
  return false unless config.enabled?

  config.validate!
  install!(config)
end

.install_if_enabled_for_plugin!Object



21
22
23
24
25
# File 'lib/rubygems_pq_tls_policy.rb', line 21

def install_if_enabled_for_plugin!
  install_if_enabled
rescue Error => e
  abort "RubyGems PQ TLS policy failed to load: #{e.message} (#{e.class})"
end

.installed?Boolean

Returns:

  • (Boolean)


36
37
38
# File 'lib/rubygems_pq_tls_policy.rb', line 36

def installed?
  @installed == true
end

.openssl_runtime_version_at_least?(major, minor, patch) ⇒ Boolean

Returns:

  • (Boolean)


121
122
123
124
125
126
127
128
129
130
131
132
# File 'lib/rubygems_pq_tls_policy.rb', line 121

def openssl_runtime_version_at_least?(major, minor, patch)
  version = if defined?(OpenSSL::OPENSSL_LIBRARY_VERSION)
    OpenSSL::OPENSSL_LIBRARY_VERSION
  elsif defined?(OpenSSL::OPENSSL_VERSION)
    OpenSSL::OPENSSL_VERSION
  end

  match = version.to_s.match(/\AOpenSSL\s+(\d+)\.(\d+)\.(\d+)/)
  return false unless match

  ([match[1].to_i, match[2].to_i, match[3].to_i] <=> [major, minor, patch]) >= 0
end

.validate_runtime!(config = self.config) ⇒ Object

Raises:



75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/rubygems_pq_tls_policy.rb', line 75

def validate_runtime!(config = self.config)
  begin
    require "openssl"
  rescue LoadError => e
    raise UnsupportedRuntime, "OpenSSL is unavailable: #{e.message}"
  end
  require "rubygems/request"

  requirements = []
  socket = defined?(OpenSSL::SSL::SSLSocket) ? OpenSSL::SSL::SSLSocket : nil
  https_pool = defined?(Gem::Request::HTTPSPool) ? Gem::Request::HTTPSPool : nil
  http_pool = defined?(Gem::Request::HTTPPool) ? Gem::Request::HTTPPool : nil

  requirements << "OpenSSL::SSL::SSLSocket is unavailable" unless socket
  if config.key_exchange_enabled? && !socket&.method_defined?(:group)
    requirements << "OpenSSL::SSL::SSLSocket#group is unavailable"
  end
  if config.cert_signature_enabled? &&
      !socket&.method_defined?(:peer_cert) &&
      !socket&.method_defined?(:peer_cert_chain)
    requirements << "OpenSSL::SSL::SSLSocket certificate inspection is unavailable"
  end
  requirements << "Gem::Request::HTTPSPool is unavailable" unless https_pool
  requirements << "Gem::Request::HTTPPool is unavailable" unless http_pool
  unless https_pool&.private_method_defined?(:setup_connection)
    requirements << "Gem::Request::HTTPSPool#setup_connection is unavailable"
  end
  unless http_pool&.private_method_defined?(:setup_connection)
    requirements << "Gem::Request::HTTPPool#setup_connection is unavailable"
  end

  if config.key_exchange_enabled? && !openssl_runtime_version_at_least?(3, 5, 0)
    requirements << "OpenSSL runtime must be 3.5.0 or newer for the default PQ TLS group"
  end

  return true if requirements.empty?

  message = "RubyGems PQ TLS policy cannot be enabled on this Ruby/OpenSSL runtime. " \
    "#{requirements.join('; ')}."
  if config.key_exchange_enabled?
    message += " Use Ruby linked against OpenSSL 3.5 or newer, such as ruby:4.0.5-trixie."
  end

  raise UnsupportedRuntime, message
end

.warn_unavailable(message) ⇒ Object



40
41
42
# File 'lib/rubygems_pq_tls_policy.rb', line 40

def warn_unavailable(message)
  warn "[rubygems:tls] #{message}" if config.trace?
end