1#!/usr/bin/env ruby -wKU 2require 'FileUtils' 3require 'singleton' 4 5# ============================================================================= 6# Class: Utilities 7# 8# Description: This class provides utility functions for the rest of the 9# script. 10# 11# This is a singleton class meaning only one instance. 12# All of the methods are Class methods and are called by 13# Utilities.method_name 14# ============================================================================= 15class Utilities 16 include Singleton 17 18 # Provide a way to fail and die upon an error 19 def self.bail(reason = nil) 20 puts "reason" if !reason.nil? 21 exit(-1) 22 end 23 24 # Check to see if a path is valid and possibly a directory 25 def self.check_path(path, is_dir = true) 26 Utilities.bail(path + " does not exist") if !FileTest.exists? path 27 if is_dir 28 Utilities.bail(path + " is not a directory") if !FileTest.directory? path 29 end 30 true 31 end 32 33 # Add quotes to a string. This is useful for outputing file paths 34 def self.quote_str(str) 35 result = "'" + str + "'" 36 result 37 end 38 39 # convert a hex string to binary 40 def self.hex_to_bin(s) 41 s.scan(/../).map { |x| x.hex.chr }.join 42 end 43 44 # convert a binary string to hex 45 def self.bin_to_hex(s) 46 s.each_byte.map { |b| b.to_s(16) }.join 47 end 48 49end 50 51# ============================================================================= 52# Class: CertTools 53# 54# Description: This class provides functions for getting required file paths 55# needed for this script. It also provides support for saving 56# and restoring the keychain list and creating keychains. 57# 58# This is a singleton class meaning only one instance. 59# All of the methods are Class methods and are called by 60# Utilities.method_name 61# ============================================================================== 62class CertTools 63 include Singleton 64 65 attr_reader :build_dir 66 attr_reader :project_dir 67 attr_reader :certificate_dir 68 attr_reader :distrusted_certs_dir 69 attr_reader :revoked_certs_dir 70 attr_reader :root_certs_dir 71 attr_reader :intermediate_certs_dir 72 attr_reader :security_tool_path 73 attr_reader :output_keychain_path 74 attr_writer :saved_kc_list 75 76 # Initialize the single instance with the path strings needed by this script 77 def initialize() 78 79 @saved_kc_list = nil; 80 @build_dir = ENV["BUILT_PRODUCTS_DIR"] 81 @project_dir = ENV["PROJECT_DIR"] 82 @certificate_dir = File.join(@project_dir, "..") 83 84 @distrusted_certs_dir = File.join(certificate_dir, "distrusted") 85 @revoked_certs_dir = File.join(certificate_dir, "revoked") 86 @root_certs_dir = File.join(certificate_dir, "roots") 87 @intermediate_certs_dir = File.join(certificate_dir, "certs") 88 89 Utilities.check_path(@distrusted_certs_dir) 90 Utilities.check_path(@revoked_certs_dir) 91 Utilities.check_path(@root_certs_dir) 92 Utilities.check_path(@intermediate_certs_dir) 93 94 @security_tool_path = '/usr/bin/security' 95 Utilities.check_path(@security_tool_path, false) 96 97 @output_keychain_path = File.join(@build_dir , "BuiltKeychains") 98 FileUtils.mkdir_p(@output_keychain_path) if !FileTest.exists? @output_keychain_path 99 100 output_variables = false 101 if output_variables 102 puts "=================================================" 103 puts "CertTools variables" 104 puts " " 105 puts "@build_dir = #{@build_dir}" 106 puts "@project_dir = #{@project_dir}" 107 puts "@certificate_dir = #{@certificate_dir}" 108 puts "@distrusted_certs_dir = #{@distrusted_certs_dir}" 109 puts "@revoked_certs_dir = #{@revoked_certs_dir}" 110 puts "@root_certs_dir = #{@root_certs_dir}" 111 puts "@intermediate_certs_dir = #{@intermediate_certs_dir}" 112 puts "@security_tool_path = #{@security_tool_path}" 113 puts "@output_keychain_path = #{@output_keychain_path}" 114 puts "=================================================" 115 puts " " 116 end 117 end 118 119 # Get the Build (output) directory path 120 def self.build_dir 121 CertTools.instance.build_dir 122 end 123 124 # Get the directory path to the top level cedrtificates submodule 125 def self.certificate_dir 126 CertTools.instance.certificate_dir 127 end 128 129 # Get the directory path to the certs directory 130 def self.distrusted_certs_dir 131 CertTools.instance.distrusted_certs_dir 132 end 133 134 # Get the directory path to the revoked directory 135 def self.revoked_certs_dir 136 CertTools.instance.revoked_certs_dir 137 end 138 139 # Get the directory path to the roots directory 140 def self.root_certs_dir 141 CertTools.instance.root_certs_dir 142 end 143 144 # Get the directory path to the certs directory 145 def self.intermediate_certs_dir 146 CertTools.instance.intermediate_certs_dir 147 end 148 149 # Get the path to the security tool 150 def self.security_tool_path 151 CertTools.instance.security_tool_path 152 end 153 154 # Get the directory path to the output directory for the generated keychains 155 def self.output_keychain_path 156 CertTools.instance.output_keychain_path 157 end 158 159 # Save the current keychain list 160 def self.saveKeychainList() 161 cmd_str = CertTools.instance.security_tool_path + " list -d user" 162 temp = `#{cmd_str}` 163 CertTools.instance.saved_kc_list = temp 164 $? 165 end 166 167 # Restore the keychain list from a previous call to saveKeychainList 168 def self.restoreKeychainList() 169 return if CertTools.instance.saved_kc_list.nil? 170 st = CertTools.instance.security_tool_path 171 cmd_str = "echo -n " + Utilities.quote_str(CertTools.instance.saved_kc_list) + " | xargs " + st + " list -d user -s" 172 `#{cmd_str}` 173 $? 174 end 175 176 # Create a new Keychain file 177 def self.createKeychain(path, name) 178 FileUtils.rm_rf(path) if FileTest.exists? path 179 cmd_str = CertTools.security_tool_path + " create-keychain -p " + Utilities.quote_str(name) + " " + Utilities.quote_str(path) 180 `#{cmd_str}` 181 $? 182 end 183 184 185end 186 187# ============================================================================= 188# Class: BuildRootKeychains 189# 190# Description: This class provides the necessary functionality to create the 191# SystemRootCertificates.keychain and the 192# SystemTrustSettings.plist output files. 193# ============================================================================= 194class BuildRootKeychains 195 196 attr_reader :root_cert_file_name 197 attr_reader :root_cert_kc_path 198 attr_reader :settings_file_name 199 attr_reader :setting_file_path 200 attr_reader :temp_kc_name 201 attr_reader :temp_kc_path 202 203 204 attr :verbose 205 206 # Initialize this instance with the paths to the output files 207 def initialize(verbose = true) 208 @verbose = verbose 209 210 @root_cert_file_name = "SystemRootCertificates.keychain" 211 @root_cert_kc_path = File.join(CertTools.output_keychain_path, @root_cert_file_name) 212 213 @settings_file_name = "SystemTrustSettings.plist" 214 @setting_file_path = File.join(CertTools.output_keychain_path, @settings_file_name) 215 216 @temp_kc_name = "SystemTempCertificates.keychain" 217 @temp_kc_path = File.join(CertTools.build_dir, @temp_kc_name) 218 219 end 220 221 # Create the SystemRootCertificates.keychain 222 def create_root_keychain() 223 puts "Creating empty System Root certificates keychain at #{@root_cert_kc_path}" if @verbose 224 CertTools.createKeychain(@root_cert_kc_path, @root_cert_file_name) 225 end 226 227 # Create the SystemTrustSettings.plist file 228 def create_setting_file() 229 puts "Creating empty Setting file at #{@setting_file_path}" if @verbose 230 FileUtils.rm_rf(@setting_file_path) if FileTest.exists? @setting_file_path 231 cmd_str = CertTools.security_tool_path + " add-trusted-cert -o " + Utilities.quote_str(@setting_file_path) 232 `#{cmd_str}` 233 $? 234 end 235 236 # Add all of the root certificates in the root directory to the SystemRootCertificates.keychain 237 def add_roots() 238 puts "Adding root certs to #{@root_cert_file_name}" if @verbose 239 num_root_certs = 0 240 Dir.foreach(CertTools.root_certs_dir) do |f| 241 next if f[0].chr == "." 242 #puts "Processing root #{f}" if @verbose 243 full_root_path = File.join(CertTools.root_certs_dir, f) 244 if f == "AppleDEVID.cer" 245 puts " sipping intermediate #{f} for trust" if @verbose 246 cmd_str = CertTools.security_tool_path + " -q add-certificates -k " + Utilities.quote_str(@root_cert_kc_path) + " " + 247 Utilities.quote_str(full_root_path) 248 249 `#{cmd_str}` 250 Utilities.bail("Security tool add-certificates returned an error for #{full_root_path}") if $? != 0 251 else 252 cmd_str = CertTools.security_tool_path 253 cmd_str += " -q add-trusted-cert -i " 254 cmd_str += Utilities.quote_str(@setting_file_path) 255 cmd_str += " -o " 256 cmd_str += Utilities.quote_str(@setting_file_path) 257 cmd_str += " -k " 258 cmd_str += Utilities.quote_str(@root_cert_kc_path) 259 cmd_str += " " 260 cmd_str += Utilities.quote_str(full_root_path) 261 cmd_result = `#{cmd_str}` 262 Utilities.bail("Security tool add-trusted-cer returned an error for #{full_root_path}") if $? != 0 263 new_num_certs = get_num_root_certs 264 if new_num_certs <= num_root_certs then 265 puts "Root #{f} was not added! result = #{cmd_result.to_s}" 266 puts cmd_str 267 end 268 num_root_certs = new_num_certs 269 end 270 end 271 true 272 end 273 274 # Create a temp keychain needed by this script 275 def create_temp_keychain() 276 puts "Creating empty temp keychain at #{@temp_kc_path}" if @verbose 277 CertTools.createKeychain(@temp_kc_path, @temp_kc_name) 278 end 279 280 # Delete the temp keychain 281 def delete_temp_keychain() 282 FileUtils.rm_rf(@temp_kc_path) if FileTest.exists? @temp_kc_path 283 end 284 285 # Process a directory of certificates that are not to be trusted. 286 def process_certs(message, dir) 287 puts message if @verbose 288 Dir.foreach(dir) do |f| 289 next if f[0].chr == "." 290 full_path = File.join(dir, f) 291 #puts "Processing #{f}" if @verbose 292 cmd_str = CertTools.security_tool_path 293 #cmd_str += " -q add-trusted-cert -i " 294 cmd_str += " add-trusted-cert -i " 295 cmd_str += Utilities.quote_str(@setting_file_path) 296 cmd_str += " -o " 297 cmd_str += Utilities.quote_str(@setting_file_path) 298 cmd_str += " -k " 299 cmd_str += Utilities.quote_str(@temp_kc_path) 300 cmd_str += " -r deny " 301 cmd_str += Utilities.quote_str(full_path) 302 `#{cmd_str}` 303 Utilities.bail("Security add-trusted-cert returned an error for #{full_path}") if $? != 0 304 end 305 end 306 307 # Process the distrusted certificates 308 def distrust_certs() 309 process_certs("Explicitly distrusting certs", CertTools.distrusted_certs_dir) 310 end 311 312 # Process the revoked certificates 313 def revoked_certs() 314 process_certs("Explicitly distrusting certs", CertTools.revoked_certs_dir) 315 end 316 317 def get_num_root_certs() 318 cmd_str = CertTools.security_tool_path + " find-certificate -a " + Utilities.quote_str(@root_cert_kc_path) 319 cert_str = `#{cmd_str}` 320 Utilities.bail(" find-certificate failed") if $? != 0 321 cert_list = cert_str.split 322 labl_list = cert_list.grep(/issu/) 323 labl_list.length 324 end 325 326 # Ensure that all of the certs in the directory were added to the SystemRootCertificates.keychain file 327 def check_all_roots_added() 328 329 #cmd_str = CertTools.security_tool_path + " find-certificate -a " + Utilities.quote_str(@root_cert_kc_path) 330 #cert_str = `#{cmd_str}` 331 #Utilities.bail(" find-certificate failed") if $? != 0 332 #cert_list = cert_str.split 333 #labl_list = cert_list.grep(/labl/) 334 #num_items_in_kc = labl_list.length 335 num_items_in_kc = get_num_root_certs 336 337 file_system_entries = Dir.entries(CertTools.root_certs_dir) 338 num_file_system_entries = file_system_entries.length 339 file_system_entries.each do |f| 340 if f[0].chr == "." 341 num_file_system_entries = num_file_system_entries - 1 342 end 343 end 344 345 puts "num_items_in_kc = #{num_items_in_kc}" if @verbose 346 puts "num_file_system_entries = #{num_file_system_entries}" if @verbose 347 num_items_in_kc == num_file_system_entries 348 end 349 350 # Set the file access for the SystemRootCertificates.keychain and 351 # SystemTrustSettings.plist files 352 def set_file_priv() 353 FileUtils.chmod 0644, @setting_file_path 354 FileUtils.chmod 0644, @root_cert_kc_path 355 end 356 357 # Do all of the processing to create the SystemRootCertificates.keychain and 358 # SystemTrustSettings.plist files 359 def do_processing() 360 result = create_root_keychain 361 Utilities.bail("create_root_keychain failed") if result != 0 362 Utilities.bail("create_setting_file failed") if create_setting_file != 0 363 add_roots() 364 Utilities.bail("create_temp_keychain failed") if create_temp_keychain != 0 365 distrust_certs() 366 revoked_certs() 367 delete_temp_keychain() 368 Utilities.bail("check_all_roots_added failes") if !check_all_roots_added 369 set_file_priv() 370 end 371end 372 373# ============================================================================= 374# Class: BuildCAKeychain 375# 376# Description: This class provides the necessary functionality to create the 377# SystemCACertificates.keychain output file. 378# ============================================================================= 379class BuildCAKeychain 380 381 attr_reader :cert_kc_name 382 attr_reader :cert_kc_path 383 384 attr :verbose 385 386 # Initialize the output path for this instance 387 def initialize(verbose = true) 388 @verbose = verbose 389 390 @cert_kc_name = "SystemCACertificates.keychain" 391 @cert_kc_path = File.join(CertTools.output_keychain_path, @cert_kc_name) 392 end 393 394 395 # Add all of the certificates in the certs directory to the 396 # SystemCACertificates.keychain file 397 def do_processing() 398 CertTools.createKeychain(@cert_kc_path, @cert_kc_name) 399 cert_path = CertTools.intermediate_certs_dir 400 401 puts "Adding intermediate cderts to #{@cert_kc_path}" if @verbose 402 puts "Intermediates #{cert_path}" if @verbose 403 404 Dir.foreach(cert_path) do |f| 405 next if f[0].chr == "." 406 full_path = File.join(cert_path, f) 407 puts "Processing #{f}" if @verbose 408 cmd_str = CertTools.security_tool_path 409 cmd_str += " -q add-certificates " 410 cmd_str += " -k " 411 cmd_str += Utilities.quote_str(@cert_kc_path) 412 cmd_str += " " 413 cmd_str += Utilities.quote_str(full_path) 414 `#{cmd_str}` 415 Utilities.bail("Security add-certificates returned an error for #{full_path}") if $? != 0 416 end 417 418 FileUtils.chmod 0644, @cert_kc_path 419 end 420end 421 422 423# ============================================================================= 424# Class: BuildEVRoots 425# 426# Description: This class provides the necessary functionality to create the 427# EVRoots.plist output file. 428# ============================================================================= 429class BuildEVRoots 430 attr_reader :open_ssl_tool_path 431 attr_reader :plistbuddy_tool_path 432 attr_reader :evroots_kc_name 433 attr_reader :evroots_kc_path 434 attr_reader :evroots_plist_name 435 attr_reader :evroots_plist_path 436 attr_reader :evroots_config_path 437 438 attr :verbose 439 attr :evroots_config_data 440 441 # Initilaize this instance with the paths to the openssl and PlistBuddy tools 442 # along with the output paths for the EVRoots.keychain and EVRoots.plist files 443 # 444 # The use of the openssl and PListBuddy tools should be removed. These were 445 # kept to ensure that the outputs between this new script and the original 446 # shell scripts remain the same 447 def initialize(verbose = true) 448 449 @verbose = verbose 450 451 @open_ssl_tool_path = "/usr/bin/openssl" 452 @plistbuddy_tool_path = "/usr/libexec/PlistBuddy" 453 @evroots_config_path = File.join(CertTools.certificate_dir, "CertificateTool/BuildOSXRootKeychain/evroot.config") 454 @evroots_config_data = nil 455 456 Utilities.check_path(@evroots_config_path, false) 457 458 @evroots_kc_name = "EVRoots.keychain" 459 @evroots_kc_path = File.join(CertTools.build_dir, @evroots_kc_name) 460 461 @evroots_plist_name = "EVRoots.plist" 462 @evroots_plist_path = File.join(CertTools.output_keychain_path, @evroots_plist_name) 463 464 end 465 466 # Get and cache the data in the evroot.config file. 467 def get_config_data() 468 return @evroots_config_data if !@evroots_config_data.nil? 469 470 @evroots_config_data = "" 471 File.open(@evroots_config_path, "r") do |file| 472 file.each do |line| 473 line.gsub!(/^#.*\n/, '') 474 next if line.empty? 475 line.gsub!(/^\s*\n/, '') 476 next if line.empty? 477 @evroots_config_data += line 478 end 479 end 480 @evroots_config_data 481 end 482 483 # Break the string from the get_config_data method into an array of lines. 484 def get_cert_lines() 485 lines_str = get_config_data 486 lines = lines_str.split("\n") 487 lines 488 end 489 490 # The processing for the EVRoots.plist requires two passes. This first pass 491 # adds the certs in the evroot.config file to the EVRoots.keychain 492 def pass_one() 493 lines = get_cert_lines 494 lines.each do |line| 495 items = line.split('"') 496 items.shift 497 items.each do |cert_file| 498 next if cert_file.empty? || cert_file == " " 499 cert_file.gsub!(/\"/, '') 500 puts "Adding cert from file #{cert_file}" if @verbose 501 cert_to_add = File.join(CertTools.root_certs_dir, cert_file) 502 Utilities.bail("#{cert_to_add} does not exist") if !FileTest.exists?(cert_to_add) 503 504 quoted_cert_to_add = Utilities.quote_str(cert_to_add) 505 cmd_str = CertTools.security_tool_path + " -q add-certificates -k " + @evroots_kc_path + " " + quoted_cert_to_add 506 `#{cmd_str}` 507 Utilities.bail("#{cmd_str} failed") if $? != 0 && $? != 256 508 end # items.each do |cert_file| 509 end # lines.each do |line| 510 end 511 512 # The second pass does the work to create the EVRoots.plist 513 def pass_two() 514 lines = get_cert_lines 515 lines.sort! 516 lines.each do |line| 517 # Split the line using a doulbe quote. This is needed to ensure that file names with spaces work 518 items = line.split('"') 519 520 # Get the oid string which is the first item in the array. 521 oid_str = items.shift 522 oid_str.gsub!(/\s/, '') 523 524 # For each line in the evroot.config there may be multiple certs for a single oid string. 525 # This is supported by adding an array in the EVRoots.plist 526 index = 0 527 cmd_str = @plistbuddy_tool_path + " -c " + '"' + "add :#{oid_str} array" + '"' + " " + @evroots_plist_path 528 `#{cmd_str}` 529 Utilities.bail("#{cmd_str} failed") if $? != 0 530 531 # Loop through all of the cert file names in the line. 532 items.each do |cert_file| 533 # Get the full path to the cert file. 534 next if cert_file.empty? || cert_file == " " 535 cert_file.gsub!(/\"/, '') 536 cert_to_hash = File.join(CertTools.root_certs_dir, cert_file) 537 Utilities.bail("#{cert_to_hash} does not exist") if !FileTest.exists?(cert_to_hash) 538 539 # Use the openssl command line tool (yuck!) to get the fingerprint of the certificate 540 cmd_str = @open_ssl_tool_path + " x509 -inform DER -in " + Utilities.quote_str(cert_to_hash) + " -fingerprint -noout" 541 finger_print = `#{cmd_str}` 542 Utilities.bail("#{cmd_str} failed") if $? != 0 543 544 # Post process the data from the openssl tool to get just the hex hash fingerprint. 545 finger_print.gsub!(/SHA1 Fingerprint=/, '') 546 finger_print.gsub!(/:/,'').chomp! 547 puts "Certificate fingerprint for #{cert_file} SHA1: #{finger_print}" if @verbose 548 549 # Convert the hex hash string to binary data and write that data out to a temp file 550 binary_finger_print = Utilities.hex_to_bin(finger_print) 551 FileUtils.rm_f "/tmp/certsha1hashtmp" 552 File.open("/tmp/certsha1hashtmp", "w") { |f| f.write binary_finger_print } 553 554 # Use the PlistBuddy tool to add the binary data to the EVRoots.plist array for the oid 555 cmd_str = @plistbuddy_tool_path + " -c " + '"' + "add :#{oid_str}:#{index} data" + '"' + " -c " + '"' + 556 "import :#{oid_str}:#{index} " + "/tmp/certsha1hashtmp" + '"' + " " + @evroots_plist_path 557 `#{cmd_str}` 558 Utilities.bail("#{cmd_str} failed") if $? != 0 559 560 # Verify the hash value by using the PListbuddy tool to read back in the binary hash data 561 cmd_str = @plistbuddy_tool_path + " -c " + '"' + "print :#{oid_str}:#{index} data" + '" ' + @evroots_plist_path 562 file_binary_finger_print = `#{cmd_str}` 563 Utilities.bail("#{cmd_str} failed") if $? != 0 564 file_binary_finger_print.chomp! 565 566 # Convert the binary data into hex data to make comparision easier 567 hex_finger_print = Utilities.bin_to_hex(binary_finger_print) 568 hex_file_finger_print = Utilities.bin_to_hex(file_binary_finger_print) 569 570 # Compare the two hex strings to ensure the all is well 571 if hex_finger_print != hex_file_finger_print 572 puts "### BUILD FAILED: data verification error" 573 puts "You likely need to install a newer version of #{@plistbuddy_tool_path} see <rdar://6208924> for details" 574 CertTools.restoreKeychainList 575 FileUtils.rm_f @evroots_plist_path 576 exit 1 577 end 578 579 # All is well prepare for the next item to add to the array 580 index += 1 581 582 end # items.each do |cert_file| 583 end # lines.each do |line| 584 end # def pass_two() 585 586 # Do all of the necessary work for this class 587 def do_processing() 588 CertTools.saveKeychainList 589 CertTools.createKeychain(@evroots_kc_path, @evroots_kc_name) 590 pass_one 591 puts "Removing #{@evroots_plist_path}" if @verbose 592 FileUtils.rm_f @evroots_plist_path 593 pass_two 594 FileUtils.chmod 0644, @evroots_plist_path 595 puts "Built #{@evroots_plist_path} successfully" if @verbose 596 end 597 598end 599 600# Make the SystemRootCertificates.keychain and SystemTrustSettings.plist files 601 602# To get verbose logging set this true 603verbose = false; 604 605brkc = BuildRootKeychains.new(verbose) 606brkc.do_processing 607 608# Make the SystemCACertificates.keychain file 609bcakc = BuildCAKeychain.new(verbose) 610bcakc.do_processing 611 612# Make the EVRoots.plist file 613bevr = BuildEVRoots.new(verbose) 614bevr.do_processing 615 616# M I C R O S O F T H A C K ! 617# It turns out that the Mac Office (2008) rolled there own solution to roots. 618# The X509Anchors file used to hold the roots in old version of OSX. This was 619# an implementation detail and was NOT part of the API set. Unfortunately, 620# Microsoft used the keychain directly instead of using the supplied APIs. When 621# the X509Anchors file was removed it broke Mac Office. So this file is now 622# supplied to keep Office from breaking. It is NEVER updated and there is no 623# code to update this file. We REALLY should see if this is still necessary 624x509_anchors_path = File.join(CertTools.certificate_dir, "CertificateTool/BuildOSXRootKeychain/X509Anchors") 625output_dir = File.join(CertTools.output_keychain_path, "X509Anchors") 626FileUtils.cp x509_anchors_path, output_dir 627 628puts "That's all folks!" 629