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/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..9a433b3 --- /dev/null +++ b/lib/certlint/caauth.rb @@ -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 = "<> #{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