Class: AiRootShield::CertificatePinningHelper

Inherits:
Object
  • Object
show all
Defined in:
lib/ai_root_shield/certificate_pinning_helper.rb

Overview

Certificate pinning helper for TLS public key pinning integration

Constant Summary collapse

SUPPORTED_ALGORITHMS =

Supported hash algorithms for pinning

%w[sha256 sha1].freeze
COMMON_CA_PINS =

Common certificate authorities and their pins

{
  "letsencrypt" => [
    "sha256/YLh1dUR9y6Kja30RrAn7JKnbQG/uEtLMkBgFF2Fuihg=", # ISRG Root X1
    "sha256/sRHdihwgkaib1P1gxX8HFszlD+7/gTfNvuAybgLPNis="  # ISRG Root X2
  ],
  "digicert" => [
    "sha256/WoiWRyIOVNa9ihaBciRSC7XHjliYS9VwUGOIud4PB18=", # DigiCert Global Root G2
    "sha256/RRM1dGqnDFsCJXBTHky16vi1obOlCgFFn/yOhI/y+ho="  # DigiCert Global Root CA
  ],
  "google" => [
    "sha256/KwccWaCgrnaw6tsrrSO61FgLacNgG2MMLq8GE6+oP5I=", # GTS Root R1
    "sha256/FEzVOUp4dF3gI0ZVPRJhFbSD608T5Wx5Bp0+jBw/gQo="  # GTS Root R2
  ]
}.freeze

Instance Method Summary collapse

Constructor Details

#initialize(config = {}) ⇒ CertificatePinningHelper

Returns a new instance of CertificatePinningHelper.



29
30
31
32
33
34
35
36
37
38
39
40
# File 'lib/ai_root_shield/certificate_pinning_helper.rb', line 29

def initialize(config = {})
  @config = {
    algorithm: "sha256",
    backup_pins: [],
    pin_validation_enabled: true,
    allow_backup_pins: true,
    strict_mode: false
  }.merge(config)
  
  @pinned_hosts = {}
  @validation_cache = {}
end

Instance Method Details

#add_pin(host, pins, options = {}) ⇒ Object

Add certificate pin for a host

Parameters:

  • host (String) —

    Hostname to pin

  • pins (Array<String>) —

    Array of certificate pins

  • options (Hash) (defaults to: {}) —

    Additional options



46
47
48
49
50
51
52
53
54
55
56
# File 'lib/ai_root_shield/certificate_pinning_helper.rb', line 46

def add_pin(host, pins, options = {})
  normalized_host = normalize_host(host)
  
  @pinned_hosts[normalized_host] = {
    pins: Array(pins),
    algorithm: options[:algorithm] || @config[:algorithm],
    backup_pins: options[:backup_pins] || [],
    strict_mode: options[:strict_mode] || @config[:strict_mode],
    added_at: Time.now
  }
end

#clear_cache ⇒ Object

Clear validation cache



190
191
192
# File 'lib/ai_root_shield/certificate_pinning_helper.rb', line 190

def clear_cache
  @validation_cache.clear
end

#extract_pin(certificate, algorithm = "sha256") ⇒ String

Extract certificate pin from certificate

Parameters:

  • certificate (OpenSSL::X509::Certificate) —

    Certificate to extract pin from

  • algorithm (String) (defaults to: "sha256") —

    Hash algorithm to use

Returns:

  • (String) —

    Certificate pin



84
85
86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/ai_root_shield/certificate_pinning_helper.rb', line 84

def extract_pin(certificate, algorithm = "sha256")
  public_key_der = certificate.public_key.to_der
  
  case algorithm.downcase
  when "sha256"
    digest = OpenSSL::Digest::SHA256.digest(public_key_der)
    "sha256/#{[digest].pack('m0')}"
  when "sha1"
    digest = OpenSSL::Digest::SHA1.digest(public_key_der)
    "sha1/#{[digest].pack('m0')}"
  else
    raise ArgumentError, "Unsupported algorithm: #{algorithm}"
  end
end

#generate_pins_for_url(url, algorithm = "sha256") ⇒ Array<String>

Generate pins for a URL

Parameters:

  • url (String) —

    URL to generate pins for

  • algorithm (String) (defaults to: "sha256") —

    Hash algorithm to use

Returns:

  • (Array<String>) —

    Generated pins



133
134
135
136
137
138
# File 'lib/ai_root_shield/certificate_pinning_helper.rb', line 133

def generate_pins_for_url(url, algorithm = "sha256")
  cert_chain = get_certificate_chain(url)
  return [] if cert_chain.empty?
  
  cert_chain.map { |cert| extract_pin(cert, algorithm) }
end

#get_certificate_chain(url) ⇒ Array<OpenSSL::X509::Certificate>

Get certificate chain from URL

Parameters:

  • url (String) —

    URL to get certificate chain from

Returns:

  • (Array<OpenSSL::X509::Certificate>) —

    Certificate chain



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
# File 'lib/ai_root_shield/certificate_pinning_helper.rb', line 102

def get_certificate_chain(url)
  uri = URI.parse(url)
  return [] unless uri.scheme == "https"
  
  cert_chain = []
  
  begin
    tcp_socket = TCPSocket.new(uri.host, uri.port || 443)
    ssl_context = OpenSSL::SSL::SSLContext.new
    ssl_context.verify_mode = OpenSSL::SSL::VERIFY_NONE
    
    ssl_socket = OpenSSL::SSL::SSLSocket.new(tcp_socket, ssl_context)
    ssl_socket.hostname = uri.host
    ssl_socket.connect
    
    cert_chain = ssl_socket.peer_cert_chain || []
    
    ssl_socket.close
    tcp_socket.close
  rescue => e
    # Log error but don't raise to allow graceful handling
    warn "Certificate chain retrieval failed for #{url}: #{e.message}"
  end
  
  cert_chain
end

#load_ca_pins(ca_name, hosts) ⇒ Object

Load pins from common CA configurations

Parameters:

  • ca_name (String) —

    CA name (letsencrypt, digicert, google)

  • hosts (Array<String>) —

    Hosts to apply CA pins to

Raises:

  • (ArgumentError)


207
208
209
210
211
212
213
214
# File 'lib/ai_root_shield/certificate_pinning_helper.rb', line 207

def load_ca_pins(ca_name, hosts)
  ca_pins = COMMON_CA_PINS[ca_name.downcase]
  raise ArgumentError, "Unknown CA: #{ca_name}" unless ca_pins
  
  Array(hosts).each do |host|
    add_pin(host, ca_pins, backup_pins: ca_pins)
  end
end

#pinning_status ⇒ Hash

Get pinning status for all configured hosts

Returns:

  • (Hash) —

    Pinning status



179
180
181
182
183
184
185
186
187
# File 'lib/ai_root_shield/certificate_pinning_helper.rb', line 179

def pinning_status
  {
    enabled: @config[:pin_validation_enabled],
    total_hosts: @pinned_hosts.size,
    hosts: @pinned_hosts.keys,
    cache_size: @validation_cache.size,
    configuration: @config
  }
end

#remove_pin(host) ⇒ Object

Remove pin for host

Parameters:

  • host (String) —

    Host to remove pin for



196
197
198
199
200
201
202
# File 'lib/ai_root_shield/certificate_pinning_helper.rb', line 196

def remove_pin(host)
  normalized_host = normalize_host(host)
  @pinned_hosts.delete(normalized_host)
  
  # Clear related cache entries
  @validation_cache.delete_if { |key, _| key.include?(normalized_host) }
end

#validate_configuration ⇒ Hash

Validate current pinning configuration

Returns:

  • (Hash) —

    Validation report



142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
# File 'lib/ai_root_shield/certificate_pinning_helper.rb', line 142

def validate_configuration
  report = {
    total_pins: @pinned_hosts.size,
    valid_pins: 0,
    invalid_pins: 0,
    issues: []
  }
  
  @pinned_hosts.each do |host, config|
    begin
      # Test connectivity and pin validation
      test_url = "https://#{host}"
      cert_chain = get_certificate_chain(test_url)
      
      if cert_chain.empty?
        report[:issues] << "Cannot retrieve certificate chain for #{host}"
        report[:invalid_pins] += 1
      else
        validation_result = perform_pin_validation(config, cert_chain)
        if validation_result[:valid]
          report[:valid_pins] += 1
        else
          report[:invalid_pins] += 1
          report[:issues] << "Pin validation failed for #{host}: #{validation_result[:reason]}"
        end
      end
    rescue => e
      report[:invalid_pins] += 1
      report[:issues] << "Error validating #{host}: #{e.message}"
    end
  end
  
  report
end

#validate_pin(host, cert_chain) ⇒ Hash

Validate certificate chain against pinned certificates

Parameters:

  • host (String) —

    Hostname being validated

  • cert_chain (Array<OpenSSL::X509::Certificate>) —

    Certificate chain

Returns:

  • (Hash) —

    Validation result



62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
# File 'lib/ai_root_shield/certificate_pinning_helper.rb', line 62

def validate_pin(host, cert_chain)
  normalized_host = normalize_host(host)
  pin_config = @pinned_hosts[normalized_host]
  
  return { valid: true, reason: "no_pin_configured" } unless pin_config
  
  # Check cache first
  cache_key = generate_cache_key(host, cert_chain)
  return @validation_cache[cache_key] if @validation_cache[cache_key]
  
  result = perform_pin_validation(pin_config, cert_chain)
  
  # Cache result for performance
  @validation_cache[cache_key] = result
  
  result
end