Skip to content

Commit

Permalink
Merge pull request #196 from 18F/stages/rc-2021
Browse files Browse the repository at this point in the history
Deploy RC 14 to Prod
  • Loading branch information
solipet committed Jan 14, 2021
2 parents f4c113a + 17c2b22 commit 823d88e
Show file tree
Hide file tree
Showing 23 changed files with 604 additions and 222 deletions.
4 changes: 4 additions & 0 deletions .codeclimate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,7 @@ plugins:
channel: rubocop-0-78
config:
file: .rubocop.yml

checks:
method-complexity:
enabled: false
30 changes: 0 additions & 30 deletions .reek.yml

This file was deleted.

2 changes: 1 addition & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ group :development, :test do
gem 'bullet', '>= 6.0.2'
gem 'pry-byebug'
gem 'rspec-rails', '>= 3.8.3'
gem 'thin', '>= 1.7.2'
gem 'thin', '>= 1.8.0'
end

group :development do
Expand Down
2 changes: 1 addition & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1149,7 +1149,7 @@ DEPENDENCIES
rubocop-rails (>= 2.4.1)
shoulda-matchers (~> 3.1, >= 3.1.3)
simplecov (>= 0.13.0)
thin (>= 1.7.2)
thin (>= 1.8.0)
timecop
tzinfo-data
webmock
Expand Down
8 changes: 6 additions & 2 deletions app/controllers/health/certs_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ class CertsController < ApplicationController
newrelic_ignore_apdex

def index
deadline = params[:deadline].present? ? Time.zone.parse(params[:deadline]) : 30.days.from_now

result = health_checker.check_certs(deadline: deadline)

render json: result.as_json,
Expand All @@ -16,5 +14,11 @@ def index
def health_checker
@health_checker ||= HealthChecker.new
end

def deadline
DurationParser.new(params[:deadline]).parse&.from_now ||
Time.zone.parse(params[:deadline].to_s) ||
30.days.from_now
end
end
end
45 changes: 31 additions & 14 deletions app/models/certificate.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,43 +52,56 @@ def self_signed?
signing_key_id == key_id
end

def validate_cert
def validate_cert(is_leaf: false)
if expired?
'expired'
elsif trusted_root?
# The other checks are all irrelevant if we trust the root.
raise "trusted root missing from store #{key_id}" if CertificateStore.instance[key_id].blank?
'valid'
else
validate_untrusted_root
validate_untrusted_root(is_leaf: is_leaf)
end
end

def validate_untrusted_root
def validate_untrusted_root(is_leaf:)
if self_signed?
'self-signed cert'
elsif !signature_verified?
'unverified'
elsif revoked?
'revoked'
elsif is_leaf && !signing_key_in_store? && !valid_policies?
'bad policy'
else
'valid'
end
end

def valid?
validate_cert == 'valid'
def valid?(is_leaf: false)
validate_cert(is_leaf: is_leaf) == 'valid'
end

def pem_filename
"#{subject.to_s(OpenSSL::X509::Name::COMPAT)}.pem"
end

def to_pem
"Subject: #{subject}\nIssuer: #{issuer}\n#{@x509_cert.to_pem}"
end

def signature_verified?
signing_cert = CertificateStore.instance[signing_key_id]
# Use HTTP stuff to download PKCS7 bundles
signing_cert = CertificateStore.instance[signing_key_id] ||
IssuingCaService.fetch_signing_key_for_cert(self)
UnrecognizedCertificateAuthority.find_or_create_for_certificate(self) unless signing_cert
signing_cert && verify(signing_cert.public_key) && signing_cert.valid?
end

def signing_key_in_store?
CertificateStore.instance[signing_key_id].present?
end

def ca_capable?
basic_constraints = get_extension('basicConstraints')&.split(/\s*,\s*/)
key_usage = get_extension('keyUsage')&.split(/\s*,\s*/)
Expand Down Expand Up @@ -118,8 +131,7 @@ def aia
end

def token(extra)
maybe_log_certificate
if valid?
if valid?(is_leaf: true)
token_for_valid_certificate(extra)
else
token_for_invalid_certificate(extra)
Expand Down Expand Up @@ -151,13 +163,12 @@ def logging_content
to_text + "\n\n" + to_pem
end

private

def maybe_log_certificate
return if valid? && critical_policies_recognized? && allowed_by_policy?
CertificateLoggerService.log_certificate(self)
def valid_policies?
critical_policies_recognized? && allowed_by_policy?
end

private

def get_extension(oid)
@x509_cert.extensions.detect { |record| record.oid == oid }&.value
end
Expand All @@ -167,6 +178,10 @@ def extract_http_url(list)
end

def token_for_valid_certificate(extra)
# Log the certificate if it is valid, but we would reject it for policy
# failures without the intermediate certs in the store
CertificateLoggerService.log_certificate(self) if !valid_policies?

subject_s = subject.to_s(OpenSSL::X509::Name::RFC2253)
piv = PivCac.find_or_create_by(dn: subject_s)
Rails.logger.info('Returning a token for a valid certificate.')
Expand Down Expand Up @@ -201,8 +216,10 @@ def cert_store
end

def token_for_invalid_certificate(extra)
CertificateLoggerService.log_certificate(self)

# figure out the reason for being invalid
reason = validate_cert
reason = validate_cert(is_leaf: true)

Rails.logger.warn("Certificate invalid: #{reason}")
TokenService.box(extra.merge(error: "certificate.#{reason}", key_id: key_id))
Expand Down
43 changes: 30 additions & 13 deletions app/services/certificate_chain_service.rb
Original file line number Diff line number Diff line change
@@ -1,40 +1,57 @@
class CertificateChainService
# Gets the chain of Cerificates between this cert and the root
# @param [Certificate]
def call(cert)
# @return [Array<Certificate>]
def chain(cert)
process_unknown_certs(cert.signing_key_id, cert.ca_issuer_http_url)
end

# Gets the chain of Certificates and also prints them out
# @param [Certificate]
# @return [Array<Certificate>]
def debug(cert)
chain(cert).each_with_index { |cert, step| print_cert(cert, step) }
end

# Finds the missing certs in the chain and writes them to the config/certs repo
# @param [Certificate]
def missing(cert)
chain(cert).reject { |cert| CertificateStore.instance[cert.key_id] }
end

# @return [Array<Certificate>]
def process_unknown_certs(ca_id, ca_issuer_url, new_certs = [])
ca_id.upcase!
ca_cert = get_cert_from_issuer(ca_id, ca_issuer_url)
process_certificate_chain(ca_cert)
end

# @return [Array<Certificate>]
def process_certificate_chain(ca_cert, chain_array = [], step = 0)
start_processing(ca_cert, step)
chain_array << ca_cert
issuer_key_id = ca_cert.signing_key_id
issuer_ca_issuer_url = ca_cert.issuer_metadata[:ca_issuer_url]
issuer_ca_cert = get_cert_from_issuer(issuer_key_id, issuer_ca_issuer_url)
issuer_ca_cert = get_cert_from_issuer(ca_cert.signing_key_id, ca_cert.issuer_metadata[:ca_issuer_url])
step += 1
if step <= 6
if step <= 6 && !issuer_ca_cert.trusted_root?
process_certificate_chain(issuer_ca_cert, chain_array, step)
end
chain_array
end

def start_processing(x509_cert, step)
# @api private
# @param [Certificate] cert
def print_cert(cert, step)
puts "///////////////////////////////////////"
puts "///////////// [ CA Step: #{step} ] /////////////"
puts "///////////////////////////////////////"
puts "#{x509_cert.to_pem}"
puts "key_id: #{x509_cert.key_id}"
puts "signing_key_id: #{x509_cert.signing_key_id}"
puts "ca_issuer_dn: #{x509_cert.issuer_metadata[:dn]}"
puts "ca_issuer_url: #{x509_cert.issuer_metadata[:ca_issuer_url]}"
puts "#{cert.to_pem}"
puts "key_id: #{cert.key_id}"
puts "signing_key_id: #{cert.signing_key_id}"
puts "ca_issuer_dn: #{cert.issuer_metadata[:dn]}"
puts "ca_issuer_url: #{cert.issuer_metadata[:ca_issuer_url]}"
end

def get_cert_from_issuer(ca_id, ca_issuer_url)
puts "fetching: #{ca_issuer_url}"
STDERR.puts "fetching: #{ca_issuer_url}"
response = get_response(ca_issuer_url)
p7c = OpenSSL::PKCS7.new(response.body)
p7c.certificates.each do |issuing_x509_certificate|
Expand Down
15 changes: 15 additions & 0 deletions app/services/certificate_store.rb
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ def add_certificate(cert)
end

def x509_certificate_chain(cert)
alert_on_expired_cert(cert)
trusted_ca_root_identifiers.each do |cert_root_id|
sequence = x509_certificate_chain_to_root(cert, cert_root_id)
return sequence if sequence&.any? && sequence&.all?
Expand Down Expand Up @@ -144,4 +145,18 @@ def extract_certs(raw)
Certificate.new(OpenSSL::X509::Certificate.new(pem + END_CERTIFICATE)) if pem.strip.present?
end.compact.select(&:ca_capable?)
end

def alert_on_expired_cert(cert)
return unless Certificate.new(cert).expired?

NewRelic::Agent.notice_error(
<<-STR.squish
Certificate Expired:
Expiration: #{cert.not_after},
Subject: #{cert.subject},
Issuer: #{cert.issuer},
Key ID: #{cert.key_id}
STR
)
end
end
39 changes: 39 additions & 0 deletions app/services/duration_parser.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Parses duration strings ("1d", 2w", "3m", "4y" into ActiveSupport::Durations)
class DurationParser
attr_reader :value

# @param value [String, nil]
def initialize(value)
@value = value
end

# @return [ActiveSupport::Duration, nil]
def parse
return if value.blank?

match = value.match(/^(?<number>\d+)(?<duration>\D)$/)
return nil unless match

parse_duration(Integer(match[:number], 10), match[:duration])
rescue ArgumentError
nil
end

def valid?
value.blank? || !parse.nil?
end

# @api private
def parse_duration(number, duration)
case duration
when 'd' # days
number.days
when 'w' # weeks
(7 * number).days
when 'm' # months
(30 * number).days
when 'y' # years
(365 * number).days
end
end
end
Loading

0 comments on commit 823d88e

Please sign in to comment.