From e163c92bb93190aafc3ec1c21fe5c69460f97610 Mon Sep 17 00:00:00 2001 From: SanthanRaj Date: Wed, 7 Mar 2018 13:42:47 -0800 Subject: [PATCH 1/2] Adding CAA check for domains in certificate --- bin/cablint | 3 ++ bin/cablint-ct | 3 +- bin/certlint | 1 + lib/certlint.rb | 1 + lib/certlint/caauth.rb | 108 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 lib/certlint/caauth.rb diff --git a/bin/cablint b/bin/cablint index 0f3a733..a41855f 100755 --- a/bin/cablint +++ b/bin/cablint @@ -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) @@ -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}" diff --git a/bin/cablint-ct b/bin/cablint-ct index 45562c4..8fd7d87 100755 --- a/bin/cablint-ct +++ b/bin/cablint-ct @@ -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 diff --git a/bin/certlint b/bin/certlint index 0508186..8c279ff 100755 --- a/bin/certlint +++ b/bin/certlint @@ -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}" diff --git a/lib/certlint.rb b/lib/certlint.rb index bc2601c..07af3f4 100644 --- a/lib/certlint.rb +++ b/lib/certlint.rb @@ -4,3 +4,4 @@ require 'certlint/pemlint' require 'certlint/namelint' require 'certlint/generalnames' +require 'certlint/caauth' diff --git a/lib/certlint/caauth.rb b/lib/certlint/caauth.rb new file mode 100644 index 0000000..a211f57 --- /dev/null +++ b/lib/certlint/caauth.rb @@ -0,0 +1,108 @@ +#!/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 + + 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 + nil + 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 = "<> #{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 + + 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, '(Hierarchy)') + 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 + + def self.CheckCAA(raw) + caa_result = [] + begin + cert = OpenSSL::X509::Certificate.new raw + rescue OpenSSL::X509::CertificateError + puts "CAA ERROR: 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| + caa_result << "CAA: #{s.strip} has CAA record at #{r[:location]}. CAA #{r[:flag]} #{r[:tag]} #{r[:value]}" + end + else + caa_result << "CAA: CAA not found for #{s.strip}" + end + end + end + return caa_result + end +end From ead1c9a97b0b0f4ac596f778b4cf3b71df6fa06f Mon Sep 17 00:00:00 2001 From: SanthanRaj Date: Wed, 7 Mar 2018 14:28:32 -0800 Subject: [PATCH 2/2] timeout error handling. Updated readme. --- README.md | 5 +++++ lib/certlint/caauth.rb | 27 +++++++++++++++++++++++---- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index e7f6550..4fc18ed 100644 --- a/README.md +++ b/README.md @@ -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 -CAA` or `ruby -I lib:ext bin/cablint -CAA` + ## Required gems * `public_suffix` @@ -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 diff --git a/lib/certlint/caauth.rb b/lib/certlint/caauth.rb index a211f57..9a433b3 100644 --- a/lib/certlint/caauth.rb +++ b/lib/certlint/caauth.rb @@ -17,13 +17,21 @@ 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 - nil + 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') @@ -49,6 +57,11 @@ def self.DnsRR(domain, loc) 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 @@ -60,7 +73,7 @@ def self.CAA(domain) domain.to_s.split('.').length domain = domain.to_s.split('.')[1..-1].join('.') if DnsRR(domain, '').length > 0 - caa = DnsRR(domain, '(Hierarchy)') + caa = DnsRR(domain, '') elsif CNAME(domain) && DnsRR(CNAME(domain), '').length > 0 caa = DnsRR(CNAME(domain), '(Hierarchy->CNAME)') end @@ -82,12 +95,14 @@ def self.CNAME(domain) 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 - puts "CAA ERROR: Error parsing Certificate" + caa_result << "CAA: Error parsing Certificate" else san = cert.extensions.find {|e| e.oid == "subjectAltName"} san_list = san.to_a[1].split(',') @@ -96,7 +111,11 @@ def self.CheckCAA(raw) result = CAA(s.strip) if result.length > 0 result.each do |r| - caa_result << "CAA: #{s.strip} has CAA record at #{r[:location]}. CAA #{r[:flag]} #{r[:tag]} #{r[:value]}" + 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}"