Skip to content
This repository has been archived by the owner on Jun 19, 2020. It is now read-only.

CAA information for domains in certificate #61

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ For now, execute by running:

`ruby -I lib:ext bin/certlint` or `ruby -I lib:ext bin/cablint`

Add '-CAA' flag to get CAA information. Note: -CAA flag MUST be at the very end.

`ruby -I lib:ext bin/certlint <certfile> -CAA` or `ruby -I lib:ext bin/cablint <certfile1> <certfile2> -CAA`

## Required gems

* `public_suffix`
Expand All @@ -31,6 +35,7 @@ capital letter, a colon, and a space. The letters indicate the type of message:
* W: Warning. These are issues where a standard recommends differently but the standard uses terms such as "SHOULD" or "MAY".
* E: Error. These are issues where the certificate is not compliant with the standard.
* F: Fatal Error. These errors are fatal to the checks and prevent most further checks from being executed. These are extremely bad errors.
* CAA: Real-time CAA information for a domain (not CAA info when the cert was issued). It also specifies whether the CAA RR was encountered in the primary domain, CNAME, or hierarchy.

## Thanks

Expand Down
3 changes: 3 additions & 0 deletions bin/cablint
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@
# permissions and limitations under the License.
require 'certlint'

caa_flag = true if ARGV.last == "-CAA"
ARGV.each do |file|
next if file == "-CAA"
fn = File.basename(file)
raw = File.read(file)

Expand All @@ -26,6 +28,7 @@ ARGV.each do |file|
end

m += CertLint::CABLint.lint(der)
m += CAAuth.CheckCAA(der) if caa_flag
m.each do |msg|
begin
puts "#{msg}\t#{fn}"
Expand Down
3 changes: 2 additions & 1 deletion bin/cablint-ct
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,10 @@ ct.get_entries(entry, entry).each do |e|
if e['leaf_input'].entry_type == 0
der = e['leaf_input'].raw_certificate
else
der = e['extra_data'].pre_certificate.raw_certificate
der = e['extra_data'].pre_certificate.raw_certificate
end
m = CertLint::CABLint.lint(der.to_s)
m += CAAuth.CheckCAA(der) if ARGV[2] == "-CAA"
m.each do |msg|
puts "#{msg}\tCT:#{entry}"
end
Expand Down
1 change: 1 addition & 0 deletions bin/certlint
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ end
der = raw

m = CertLint.lint(der)
m += CAAuth.CheckCAA(der) if ARGV[1] == "-CAA"
m.each do |msg|
begin
puts "#{msg}\t#{fn}"
Expand Down
1 change: 1 addition & 0 deletions lib/certlint.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
require 'certlint/pemlint'
require 'certlint/namelint'
require 'certlint/generalnames'
require 'certlint/caauth'
127 changes: 127 additions & 0 deletions lib/certlint/caauth.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
#!/usr/bin/ruby -Eutf-8:utf-8
# encoding: UTF-8
# Copyright 2018 Santhan Raj. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"). You may not
# use this file except in compliance with the License. A copy of the License
# is located at
#
# http://aws.amazon.com/apache2.0/
#
# or in the "license" file accompanying this file. This file is distributed on
# an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
# express or implied. See the License for the specific language governing
# permissions and limitations under the License.
require 'resolv'
require 'openssl'

module CAAuth

#Performs a CAA request for the given domain. The second parameter "loc" is a placeholder to store whether this is the
#primary domain in question or whether this domain is a result of either CNAME or Tree climbing look up of the primary
#domain.

# returns an array of hashs with CAA information for the particular domain (:flag, :tag, :value).

def self.DnsRR(domain, loc)
caa_rr = []
Resolv::DNS.open do |dns|
begin
all_records = dns.getresources(domain, Resolv::DNS::Resource::IN::ANY)
rescue Resolv::ResolvError
caa_rr << {:error => true, :error_value => "Error retrieving"}
rescue Resolv::ResolvTimeout
caa_rr << {:error => true, :error_value => "Request timed-out trying"}
else
all_records.each do |rr|
if (rr.is_a? Resolv::DNS::Resource::Generic) && (rr.class.name.split('::').last == 'Type257_Class1')
data = rr.data.bytes
flag = data[0].to_s
if data[2..10].pack('c*').eql? "issuewild"
tag = data[2..10].pack('c*')
value = data[11..-1].pack('c*')
elsif ["issue", "iodef"].include? data[2..6].pack('c*')
tag = data[2..6].pack('c*')
value = data[7..-1].pack('c*')
else
tag = "<<Unknown property-name-value ->> #{data[2..-1].pack('c*')}"
value = ''
end
caa_rr << {:location => "#{domain}#{loc}", :flag => flag, :tag => tag, :value => value}
end
end
return caa_rr
ensure
dns.close()
end
end
end


#Performs CAA check as per RFC 6844 Section 4 (Errata 5065, 5097). The
#array from the DnsRR method is not manipulated/changed here. It is simply
#passed on to the calling function. I kept getting an Ruby interpretor error
#when I tried to return directly. Hence the need for an array to hold and return
def self.CAA(domain)
caa = []
if DnsRR(domain, '').length > 0
return DnsRR(domain, '(Primary Domain)')
elsif CNAME(domain) && DnsRR(CNAME(domain), '').length > 0
return DnsRR(CNAME(domain, '(CNAME)'))
else
while domain.to_s.split('.').length > 1
domain.to_s.split('.').length
domain = domain.to_s.split('.')[1..-1].join('.')
if DnsRR(domain, '').length > 0
caa = DnsRR(domain, '')
elsif CNAME(domain) && DnsRR(CNAME(domain), '').length > 0
caa = DnsRR(CNAME(domain), '(Hierarchy->CNAME)')
end
break if caa.length > 0
end
return caa
end
end

def self.CNAME(domain)
Resolv::DNS.open do |dns|
begin
return dns.getresources(domain, Resolv::DNS::Resource::IN::CNAME)[0].name.to_s rescue nil
rescue Resolv::ResolvError
nil
ensure
dns.close()
end
end
end

#Takes a der/pem cert as input and runs each domain in SAN through the CAA check.
#It doesn't check the CN since the CN should be a part of SAN
def self.CheckCAA(raw)
caa_result = []
begin
cert = OpenSSL::X509::Certificate.new raw
rescue OpenSSL::X509::CertificateError
caa_result << "CAA: Error parsing Certificate"
else
san = cert.extensions.find {|e| e.oid == "subjectAltName"}
san_list = san.to_a[1].split(',')
san_list.each do |s|
s.slice! "DNS:"
result = CAA(s.strip)
if result.length > 0
result.each do |r|
if r[:error]
caa_result << "CAA: #{r[:error_value]} CAA information for #{s.strip}"
else
caa_result << "CAA: #{s.strip} has CAA record at #{r[:location]}. CAA #{r[:flag]} #{r[:tag]} #{r[:value]}"
end
end
else
caa_result << "CAA: CAA not found for #{s.strip}"
end
end
end
return caa_result
end
end