This class checks the conf.xml and kasp.xml files to make sure that they syntactically valid, and also semantically valid. Any oddities in the configuration are reported to the user.
# File ../../auditor/lib/kasp_checker.rb, line 47 def check @ret_val = 999 conf_file = @conf_file if (!conf_file) KASPAuditor.exit("No configuration file specified", 1) end # Validate the conf.xml against the RNG validate_file(conf_file, CONF_FILE) # Now check the config file kasp_file = check_config_file(conf_file) if (@kasp_file) # Override the configured kasp.xml with the user-supplied value kasp_file = @kasp_file end if (kasp_file) # Validate the kasp.xml against the RNG validate_file(kasp_file, KASP_FILE) # Now check the kasp file check_kasp_file(kasp_file) else log(LOG_ERR, "KASP configuration file cannot be found") end @ret_val = 0 if (@ret_val >= LOG_WARNING) # Only return an error if LOG_ERR or above was raised if (@ret_val == 999) exit(0) else exit(@ret_val) end end
Load the specified config file and sanity check it. The file should have been validated against the RNG before this method is called. Sets the syslog facility if it is defined. Returns the configured location of the kasp.xml configuration file.
# File ../../auditor/lib/kasp_checker.rb, line 154 def check_config_file(conf_file) kasp_file = nil begin File.open((conf_file + "").untaint , 'r') {|file| begin doc = REXML::Document.new(file) rescue Exception => e log(LOG_CRIT, "Can't understand #{conf_file} - exiting") exit(1) end begin facility = doc.elements['Configuration/Common/Logging/Syslog/Facility'].text # Now turn the facility string into a Syslog::Constants format.... syslog_facility = eval "Syslog::LOG_" + (facility.upcase+"").untaint @syslog = syslog_facility rescue Exception => e print "Error reading syslog config : #{e}\n" # @syslog = Syslog::LOG_DAEMON end begin kasp_file = doc.elements['Configuration/Common/PolicyFile'].text rescue Exception log(LOG_ERR, "Can't read KASP policy location from conf.xml - exiting") end # Checks we need to run on conf.xml : # 1. If a user and/or group is defined in the conf.xml then check that it exists. # Do this for *all* privs instances (in Signer, Auditor and Enforcer as well as top-level) warned_users = [] doc.root.each_element('//Privileges/User') {|user| # Now check the user exists # Keep a list of the users/groups we have already warned for, and make sure we only warn for them once next if (warned_users.include?(user.text)) begin Etc.getpwnam((user.text+"").untaint).uid rescue ArgumentError warned_users.push(user.text) log(LOG_ERR, "User #{user.text} does not exist") end } warned_groups = [] doc.root.each_element('//Privileges/Group') {|group| # Now check the group exists # Keep a list of the users/groups we have already warned for, and make sure we only warn for them once next if (warned_groups.include?(group.text)) begin Etc.getgrnam((group.text+"").untaint).gid rescue ArgumentError warned_groups.push(group.text) log(LOG_ERR, "Group #{group.text} does not exist") end } check_db(doc) # The Directory code is commented out until we support chroot again # doc.root.each_element('//Privileges/Directory') {|dir| # print "Dir : #{dir}\n" # # Now check the directory # if (!File.exist?(dir)) # log(LOG_ERR, "Direcotry #{dir} cannot be found") # end # } # # 2. If there are multiple repositories of the same type # (i.e. Module is the same for them), then each must have a unique TokenLabel # So, for each Repository, get the Name, Module and TokenLabel. # Then make sure that there are no repositories which share both Module # and TokenLabel @repositories = {} doc.elements.each('Configuration/RepositoryList/Repository') {|repository| name = repository.attributes['name'] # Check if two repositories exist with the same name if (@repositories.keys.include?name) log(LOG_ERR, "Two repositories exist with the same name (#{name})") end mod = repository.elements['Module'].text # 5. Check that the shared library (Module) exists. if (!File.exist?((mod+"").untaint)) log(LOG_ERR, "Module #{mod} in Repository #{name} cannot be found") end tokenlabel = repository.elements['TokenLabel'].text # print "Checking Module #{mod} and TokenLabel #{tokenlabel} in Repository #{name}\n" # Now check if repositories already includes the [mod, tokenlabel] hash if (@repositories.values.include?([mod, tokenlabel])) log(LOG_ERR, "Multiple Repositories in #{conf_file} have the same Module (#{mod}) and TokenLabel (#{tokenlabel}), for Repository #{name}") end @repositories[name] = [mod, tokenlabel] # 3. If a repository specifies a capacity, the capacity must be greater than zero. # This check is performed when the XML is validated against the RNG (which specifies positiveInteger for Capacity) # # Also } # check durations for Interval and RolloverNotification (the only duration elements in conf.xml) ["Enforcer/Interval", "Enforcer/RolloverNotification"].each {|element| doc.root.each_element("//"+element) {|el| check_duration_element_proc(el, "conf.xml", element, conf_file)} } } return ((kasp_file+"").untaint) rescue Errno::ENOENT log(LOG_ERR, "Can't find config file : #{conf_file}") return nil end end
# File ../../auditor/lib/kasp_checker.rb, line 263 def check_db(doc) # Now check that the DB is writable by the user # //Enforcer/Datastore/Sqlite doc.root.each_element('/Configuration/Enforcer/Datastore/SQLite') {|sqlite| file = ((sqlite.text+"").untaint) if !File.exist?(file) log(LOG_ERR, "Can't find DB file : #{file}") return end stat = File::Stat.new(file) # Get the User and Group from the file - default to current user_name=nil group_name=nil begin user_name = doc.elements['Configuration/Enforcer/Privileges/User'].text rescue Exception end begin group_name = doc.elements['Configuration/Enforcer/Privileges/Group'].text rescue Exception end if (user_name || group_name) # Other user of group specified - will need to fire up another process, # passing in the UID and GID to change to, and then inspect return pid = fork { # Do all the changes and then check writable begin if (group_name) group = Etc.getgrnam((group_name+"").untaint).gid Process::Sys.setgid(group) end if (user_name) user = Etc.getpwnam((user_name+"").untaint).uid Process::Sys.setuid(user) end rescue Exception => e log(LOG_ERR, "Can't change to #{user_name}, #{group_name} to check DB write permissions") end if (stat.writable?) exit(0) else exit(-1) end } Process.wait(pid) ret_status = $? >> 8 if (ret_status != 0) log(LOG_ERR, "#{user_name} user can not write to DB file #{file}\n") end else # No user/group specified - now check that the file is writable by current user if !(stat.writable?) log(LOG_ERR, "Current user can not write to DB file #{file}\n") end end } doc.root.each_element('//Enforcer/Datastore/MySQL') {|mysql| # @TODO@ If //Enforcer/Datastore/MySQL is used, then we could try to connect to the database? # Complete once MySQL support is complete } end
# File ../../auditor/lib/kasp_checker.rb, line 327 def check_duration_element_proc(element, policy, name, filename) duration = element.text # print "Checking duration of #{name} : #{duration}, #{duration.length}\n" last_digit = duration[duration.length-1, 1].downcase if (last_digit == "m" && !(/T/=~duration)) log(LOG_WARNING, "In #{(policy == "conf.xml") ? 'Configuration' : 'policy ' + policy + ', '} M used in duration field for #{name} (#{duration})" + " in #{filename} - this will be interpreted as 31 days") end if (last_digit == "y") log(LOG_WARNING, "In #{(policy == "conf.xml") ? 'Configuration' : 'policy ' + policy + ', '} Y used in duration field for #{name} (#{duration})" + " in #{filename} - this will be interpreted as 365 days") end end
# File ../../auditor/lib/kasp_checker.rb, line 342 def check_kasp_file(kasp_file) begin File.open((kasp_file.to_s+"").untaint, 'r') {|file| begin doc = REXML::Document.new(file) rescue Exception => e log(LOG_CRIT, "Can't understand #{file} - exiting") exit(1) end # Run the following checks on kasp.xml : policy_names = [] doc.elements.each('KASP/Policy') {|policy| name = policy.attributes['name'] # Check if two policies exist with the same name if (policy_names.include?name) log(LOG_ERR, "Two policies exist with the same name (#{name})") end policy_names.push(name) # 2. For all policies, check that the "Re-sign" interval is less than the "Refresh" interval. resign_secs = get_duration(policy,'Signatures/Resign', kasp_file) refresh_secs = get_duration(policy, 'Signatures/Refresh', kasp_file) if (refresh_secs != 0 && refresh_secs <= resign_secs) log(LOG_ERR, "The Refresh interval (#{refresh_secs} seconds) for " + "#{name} Policy in #{kasp_file} is less than or equal to the Resign interval" + " (#{resign_secs} seconds)") end # 3. Ensure that the "Default" and "Denial" validity periods are greater than the "Refresh" interval. default_secs = get_duration(policy, 'Signatures/Validity/Default', kasp_file) denial_secs = get_duration(policy, 'Signatures/Validity/Denial', kasp_file) if (default_secs <= refresh_secs) log(LOG_ERR, "Validity/Default (#{default_secs} seconds) for #{name} " + "policy in #{kasp_file} is less than the Refresh interval " + "(#{refresh_secs} seconds)") end if (denial_secs <= refresh_secs) log(LOG_ERR, "Validity/Denial (#{denial_secs} seconds) for #{name} " + "policy in #{kasp_file} is less than or equal to the Refresh interval " + "(#{refresh_secs} seconds)") end # 5. Warn if "Jitter" is greater than 50% of the maximum of the "default" and "Denial" period. (This is a bit arbitrary. The point is to get the user to realise that there will be a large spread in the signature lifetimes.) jitter_secs = get_duration(policy, 'Signatures/Jitter', kasp_file) max_default_denial=[default_secs, denial_secs].max max_default_denial_type = max_default_denial == default_secs ? "Default" : "Denial" if (jitter_secs > (max_default_denial * 0.5)) log(LOG_WARNING, "Jitter time (#{jitter_secs} seconds) is large" + " compared to Validity/#{max_default_denial_type} " + "(#{max_default_denial} seconds) for #{name} policy in #{kasp_file}") end # 14. Error if jitter is greater than either Default or Denial Validity if (jitter_secs > default_secs) log(LOG_ERR, "Jitter time (#{jitter_secs}) is greater than the Default Validity (#{default_secs}) for #{name} policy in #{kasp_file}") end if (jitter_secs > denial_secs) log(LOG_ERR, "Jitter time (#{jitter_secs}) is greater than the Denial Validity (#{denial_secs}) for #{name} policy in #{kasp_file}") end # 6. Warn if the InceptionOffset is greater than one hour. (Again arbitrary - but do we really expect the times on two systems to differ by more than this?) inception_offset_secs = get_duration(policy, 'Signatures/InceptionOffset', kasp_file) if (inception_offset_secs > (60 * 60)) log(LOG_WARNING, "InceptionOffset is higher than expected " + "(#{inception_offset_secs} seconds) for #{name} policy in #{kasp_file}") end # 7. Warn if the "PublishSafety" and "RetireSafety" margins are less than 0.1 * TTL or more than 5 * TTL. publish_safety_secs = get_duration(policy, 'Keys/PublishSafety', kasp_file) retire_safety_secs = get_duration(policy, 'Keys/RetireSafety', kasp_file) ttl_secs = get_duration(policy, 'Keys/TTL', kasp_file) [{publish_safety_secs => "Keys/PublishSafety"}, {retire_safety_secs => "Keys/RetireSafety"}].each {|pair| pair.each {|time, label| if (time < (0.1 * ttl_secs)) log(LOG_WARNING, "#{label} (#{time} seconds) in #{name} policy" + " in #{kasp_file} is less than 0.1 * TTL (#{ttl_secs} seconds)") end if (time > (5 * ttl_secs)) log(LOG_WARNING, "#{label} (#{time} seconds) in #{name} policy" + " in #{kasp_file} is more than 5 * TTL (#{ttl_secs} seconds)") end } } # Get the denial type (NSEC or NSEC3) denial_type = nil if (policy.elements['Denial/NSEC']) denial_type = "NSEC" else denial_type = "NSEC3" # Now check that the algorithm is correct policy.each_element('Denial/NSEC3/Hash/') {|hash| alg = hash.elements["Algorithm"].text if (alg.to_i != 1) log(LOG_ERR, "NSEC3 Hash algorithm is #{alg} but should be 1"); end } end # For all keys (if any are configured)... max = 9999999999999999 ksk_lifetime = max zsk_lifetime = max policy.each_element('Keys/ZSK') {|zsk| check_key(zsk, "ZSK", name, kasp_file, denial_type) zskl = get_duration(zsk, 'Lifetime', kasp_file) zsk_lifetime = [zsk_lifetime, zskl].min } policy.each_element('Keys/KSK') {|ksk| check_key(ksk, "KSK", name, kasp_file, denial_type) kskl = get_duration(ksk, 'Lifetime', kasp_file) ksk_lifetime = [ksk_lifetime, kskl].min } # 12. Warn if for any zone, the KSK lifetime is less than the ZSK lifetime. if ((ksk_lifetime != max) && (zsk_lifetime != max) && (ksk_lifetime < zsk_lifetime)) log(LOG_WARNING, "KSK minimum lifetime (#{ksk_lifetime} seconds)" + " is less than ZSK minimum lifetime (#{zsk_lifetime} seconds)"+ " for #{name} Policy in #{kasp_file}") end # 15. Warn if resalt is less than resign interval. if (denial_type == "NSEC3") resign_secs = get_duration(policy,'Signatures/Resign', kasp_file) resalt_secs = get_duration(policy,'Denial/NSEC3/Resalt', kasp_file) if (resalt_secs) if (resalt_secs < resign_secs) log(LOG_WARNING, "NSEC3 resalt interval (#{resalt_secs}) is less than" + " signature resign interval (#{resign_secs})" + " for #{name} Policy in #{kasp_file}") end end end # 9. If datecounter is used for serial, then no more than 99 signings should be done per day (there are only two digits to play with in the version number). resigns_per_day = (60 * 60 * 24) / resign_secs if (resigns_per_day > 99) # Check if the datecounter is used - if so, warn policy.each_element('Zone/SOA/Serial') {|serial| if (serial.text.downcase == "datecounter") log(LOG_ERR, "In #{kasp_file}, policy #{name}, serial type datecounter used"+ " but #{resigns_per_day} re-signs requested."+ " No more than 99 re-signs per day should be used with datecounter"+ " as only 2 digits are allocated for the version number") # 13. Check that the value of the "Serial" tag is valid. elsif !(["unixtime", "datecounter", "keep", "counter"].include?serial.text.downcase) log(LOG_ERR, "In #{kasp_file}, policy #{name}, unknown Serial type encountered ('#{serial.text}')." + " Should be either 'unixtime', 'counter', 'datecounter' or 'keep'") end } end ["Signatures/Resign", "Signatures/Refresh", "Signatures/Validity/Default", "Signatures/Validity/Denial", "Signatures/Jitter", "Signatures/InceptionOffset", "Keys/RetireSafety", "Keys/PublishSafety", "Keys/Purge", "NSEC3/Resalt", "SOA/Minimum", "ZSK/Lifetime", "KSK/Lifetime", "TTL", "PropagationDelay"].each {|element| policy.each_element(element) {|el| check_duration_element_proc(el, name, element, kasp_file)} } } # 1. Warn if a policy named "default" does not exist. if (!policy_names.include?"default") log(LOG_WARNING, "No policy named 'default' in #{kasp_file}. This " + "means you will need to refer explicitly to the policy for each zone") end } rescue Errno::ENOENT log(LOG_ERR, "Can't find KASP config file : #{kasp_file}") end end
# File ../../auditor/lib/kasp_checker.rb, line 515 def check_key(key, type, policy, kasp_file, denial_type) # 7. The algorithm should be checked to ensure it is consistent with the NSEC/NSEC3 choice for the zone. alg = key.elements['Algorithm'].text if (denial_type == "NSEC3") # Check correct algorithm used for NSEC3 if (!(["6","7","8","10"].include?alg)) log(LOG_ERR, "In policy #{policy}, incompatible algorithm (#{alg}) used for #{type} NSEC3" + " in #{kasp_file} - should be 6,7,8 or 10") end end # 9. The key strength should be checked for sanity - warn if less than 1024 or more than 4096 begin key_length = key.elements['Algorithm'].attributes['length'].to_i if (key_length < 1024) log(LOG_WARNING, "Key length of #{key_length} used for #{type} in #{policy}"+ " policy in #{kasp_file}. Should probably be 1024 or more") elsif (key_length > 4096) log(LOG_WARNING, "Key length of #{key_length} used for #{type} in #{policy}"+ " policy in #{kasp_file}. Should probably be 4096 or less") end rescue Exception # Fine - this is an optional element end # 10. Check that repositories listed in the KSK and ZSK sections are defined in conf.xml. repository = key.elements['Repository'].text if (!@repositories.keys.include?repository) log(LOG_ERR, "Unknown repository (#{repository}) defined for #{type} in"+ " #{policy} policy in #{kasp_file}") end end
# File ../../auditor/lib/kasp_checker.rb, line 548 def get_duration(doc, element, kasp_file) begin text = doc.elements[element].text # Now get the numSeconds from the XSDDuration format duration = KASPAuditor::Config.xsd_duration_to_seconds(text) return duration rescue Exception log(LOG_ERR, "Can't find #{element} in #{doc.attributes['name']} in #{kasp_file}") return 0 end end
# File ../../auditor/lib/kasp_checker.rb, line 81 def log(level, msg) if (level.to_i < @ret_val) @ret_val = level.to_i end if (@syslog) Syslog.open("ods-kaspcheck", Syslog::LOG_PID | Syslog::LOG_CONS, @syslog) { |slog| slog.log(level, msg) } end # Convert the level into text, rather than a number? e.g. "WARNING" level_string = case level when LOG_ERR then "ERROR" when LOG_WARNING then "WARNING" when LOG_INFO then "INFO" when LOG_CRIT then "CRITICAL" end print "#{level_string}: #{msg}\n" end
# File ../../auditor/lib/kasp_checker.rb, line 101 def validate_file(file, type) # Actually call xmllint to do the validation if (file) rng_location = nil if (type == CONF_FILE) rng_location = @rng_path + "/conf.rng" else rng_location = @rng_path + "/kasp.rng" end rng_location = (rng_location.to_s + "").untaint file = (file.to_s + "").untaint r, w = IO.pipe pid = fork { r.close $stdout.reopen w ret = system("#{(@xmllint.to_s + "").untaint} --noout --relaxng #{rng_location} #{file}") w.close exit!(ret) } w.close ret_strings = [] r.each {|l| ret_strings.push(l)} Process.waitpid(pid) ret_val = $?.exitstatus # Now rewrite captured output from xmllint to log method ret_strings.each {|line| line.chomp! if line.index(" validates") # log(LOG_INFO, line + " OK") else log(LOG_ERR, line) end } if (!ret_val) log(LOG_ERR, "Errors found validating " + ((file== nil)? "unknown file" : file) + " against " + ((type == CONF_FILE) ? "conf" : "kasp") + ".rng") end else log(LOG_ERR, "Not validating : no file passed to validate against " + (((type == CONF_FILE) ? "conf" : "kasp") + ".rng")) end end
Generated with the Darkfish Rdoc Generator 2.