File: verifier.rb

package info (click to toggle)
puppet-agent 7.23.0-1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm
  • size: 19,092 kB
  • sloc: ruby: 245,074; sh: 456; makefile: 38; xml: 33
file content (142 lines) | stat: -rw-r--r-- 5,940 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
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
74
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
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
require_relative '../../puppet/ssl'

# Verify an SSL connection.
#
# @api private
class Puppet::SSL::Verifier

  FIVE_MINUTES_AS_SECONDS = 5 * 60

  attr_reader :ssl_context

  # Create a verifier using an `ssl_context`.
  #
  # @param hostname [String] FQDN of the server we're attempting to connect to
  # @param ssl_context [Puppet::SSL::SSLContext] ssl_context containing CA certs,
  #   CRLs, etc needed to verify the server's certificate chain
  # @api private
  def initialize(hostname, ssl_context)
    @hostname = hostname
    @ssl_context = ssl_context
  end

  # Return true if `self` is reusable with `verifier` meaning they
  # are using the same `ssl_context`, so there's no loss of security
  # when using a cached connection.
  #
  # @param verifier [Puppet::SSL::Verifier] the verifier to compare against
  # @return [Boolean] return true if a cached connection can be used, false otherwise
  # @api private
  def reusable?(verifier)
    verifier.instance_of?(self.class) &&
      verifier.ssl_context.object_id == @ssl_context.object_id
  end

  # Configure the `http` connection based on the current `ssl_context`.
  #
  # @param http [Net::HTTP] connection
  # @api private
  def setup_connection(http)
    http.cert_store = @ssl_context[:store]
    http.cert = @ssl_context[:client_cert]
    http.key = @ssl_context[:private_key]
    # default to VERIFY_PEER
    http.verify_mode = if !@ssl_context[:verify_peer]
                         OpenSSL::SSL::VERIFY_NONE
                       else
                         OpenSSL::SSL::VERIFY_PEER
                       end
    http.verify_callback = self
  end

  # This method is called if `Net::HTTP#start` raises an exception, which
  # could be a result of an openssl error during cert verification, due
  # to ruby's `Socket#post_connection_check`, or general SSL connection
  # error.
  #
  # @param http [Net::HTTP] connection
  # @param error [OpenSSL::SSL::SSLError] connection error
  # @raise [Puppet::SSL::CertVerifyError] SSL connection failed due to a
  #   verification error with the server's certificate or chain
  # @raise [Puppet::Error] server hostname does not match certificate
  # @raise [OpenSSL::SSL::SSLError] low-level SSL connection failure
  # @api private
  def handle_connection_error(http, error)
    raise @last_error if @last_error

    # ruby can pass SSL validation but fail post_connection_check
    peer_cert = http.peer_cert
    if peer_cert && !OpenSSL::SSL.verify_certificate_identity(peer_cert, @hostname)
      raise Puppet::SSL::CertMismatchError.new(peer_cert, @hostname)
    else
      raise error
    end
  end

  # OpenSSL will call this method with the verification result for each cert in
  # the server's chain, working from the root CA to the server's cert. If
  # preverify_ok is `true`, then that cert passed verification. If it's `false`
  # then the current verification error is contained in `store_context.error`.
  # and the current cert is in `store_context.current_cert`.
  #
  # If this method returns `false`, then verification stops and ruby will raise
  # an `OpenSSL::SSL::Error` with "certificate verification failed". If this
  # method returns `true`, then verification continues.
  #
  # If this method ignores a verification error, such as the cert's CRL will be
  # valid within the next 5 minutes, then this method may be called with a
  # different verification error for the same cert.
  #
  # WARNING: If `store_context.error` returns `OpenSSL::X509::V_OK`, don't
  # assume verification passed. Ruby 2.4+ implements certificate hostname
  # checking by default, and if the cert doesn't match the hostname, then the
  # error will be V_OK. Always use `preverify_ok` to determine if verification
  # succeeded or not.
  #
  # @param preverify_ok [Boolean] if `true` the current certificate in `store_context`
  #   was verified. Otherwise, check for the current error in `store_context.error`
  # @param store_context [OpenSSL::X509::StoreContext] The context holding the
  #   verification result for one certificate
  # @return [Boolean] If `true`, continue verifying the chain, even if that means
  #   ignoring the current verification error. If `false`, abort the connection.
  #
  # @api private
  def call(preverify_ok, store_context)
    return true if preverify_ok

    peer_cert = store_context.current_cert

    case store_context.error
    when OpenSSL::X509::V_OK
      # chain is from leaf to root, opposite of the order that `call` is invoked
      chain_cert = store_context.chain.first

      # ruby 2.4 doesn't compare certs based on value, so force to DER byte array
      if peer_cert && chain_cert && peer_cert.to_der == chain_cert.to_der && !OpenSSL::SSL.verify_certificate_identity(peer_cert, @hostname)
        @last_error = Puppet::SSL::CertMismatchError.new(peer_cert, @hostname)
        return false
      end

    # ruby-openssl#74ef8c0cc56b840b772240f2ee2b0fc0aafa2743 now sets the
    # store_context error when the cert is mismatched
    when OpenSSL::X509::V_ERR_HOSTNAME_MISMATCH
      @last_error = Puppet::SSL::CertMismatchError.new(peer_cert, @hostname)
      return false

    when OpenSSL::X509::V_ERR_CRL_NOT_YET_VALID
      crl = store_context.current_crl
      if crl && crl.last_update && crl.last_update < Time.now + FIVE_MINUTES_AS_SECONDS
        Puppet.debug("Ignoring CRL not yet valid, current time #{Time.now.utc}, CRL last updated #{crl.last_update.utc}")
        return true
      end
    end

    # TRANSLATORS: `error` is an untranslated message from openssl describing why a certificate in the server's chain is invalid, and `subject` is the identity/name of the failed certificate
    @last_error = Puppet::SSL::CertVerifyError.new(
      _("certificate verify failed [%{error} for %{subject}]") %
      { error: store_context.error_string, subject: peer_cert.subject.to_utf8 },
      store_context.error, peer_cert
    )
    false
  end
end