1 #!/usr/bin/env ruby -wKU
5 # =============================================================================
8 # Description: This class provides utility functions for the rest of the
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 # =============================================================================
18 # Provide a way to fail and die upon an error
19 def self.bail(reason
= nil)
20 puts
"reason" if !reason
.nil?
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
28 Utilities
.bail(path +
" is not a directory") if !FileTest
.directory
? path
33 # Add quotes to a string. This is useful for outputing file paths
34 def self.quote_str(str
)
35 result
= "'" + str +
"'"
39 # convert a hex string to binary
40 def self.hex_to_bin(s
)
41 s
.scan(/../).map
{ |x
| x
.hex
.chr
}.join
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
51 # =============================================================================
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.
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 # ==============================================================================
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
76 # Initialize the single instance with the path strings needed by this script
80 @build_dir = ENV["BUILT_PRODUCTS_DIR"]
81 @project_dir = ENV["PROJECT_DIR"]
82 @certificate_dir = File
.join(@project_dir, "..")
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")
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)
94 @security_tool_path = '/usr/bin/security'
95 Utilities
.check_path(@security_tool_path, false)
97 @output_keychain_path = File
.join(@build_dir , "BuiltKeychains")
98 FileUtils
.mkdir_p(@output_keychain_path) if !FileTest
.exists
? @output_keychain_path
100 output_variables
= false
102 puts
"================================================="
103 puts
"CertTools variables"
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
"================================================="
119 # Get the Build (output) directory path
121 CertTools
.instance
.build_dir
124 # Get the directory path to the top level cedrtificates submodule
125 def self.certificate_dir
126 CertTools
.instance
.certificate_dir
129 # Get the directory path to the certs directory
130 def self.distrusted_certs_dir
131 CertTools
.instance
.distrusted_certs_dir
134 # Get the directory path to the revoked directory
135 def self.revoked_certs_dir
136 CertTools
.instance
.revoked_certs_dir
139 # Get the directory path to the roots directory
140 def self.root_certs_dir
141 CertTools
.instance
.root_certs_dir
144 # Get the directory path to the certs directory
145 def self.intermediate_certs_dir
146 CertTools
.instance
.intermediate_certs_dir
149 # Get the path to the security tool
150 def self.security_tool_path
151 CertTools
.instance
.security_tool_path
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
159 # Save the current keychain list
160 def self.saveKeychainList()
161 cmd_str
= CertTools
.instance
.security_tool_path +
" list -d user"
163 CertTools
.instance
.saved_kc_list
= temp
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"
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
)
187 # =============================================================================
188 # Class: BuildRootKeychains
190 # Description: This class provides the necessary functionality to create the
191 # SystemRootCertificates.keychain and the
192 # SystemTrustSettings.plist output files.
193 # =============================================================================
194 class BuildRootKeychains
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
206 # Initialize this instance with the paths to the output files
207 def initialize(verbose
= true)
210 @root_cert_file_name = "SystemRootCertificates.keychain"
211 @root_cert_kc_path = File
.join(CertTools
.output_keychain_path
, @root_cert_file_name)
213 @settings_file_name = "SystemTrustSettings.plist"
214 @setting_file_path = File
.join(CertTools
.output_keychain_path
, @settings_file_name)
216 @temp_kc_name = "SystemTempCertificates.keychain"
217 @temp_kc_path = File
.join(CertTools
.build_dir
, @temp_kc_name)
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)
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)
236 # Add all of the root certificates in the root directory to the SystemRootCertificates.keychain
238 puts
"Adding root certs to #{@root_cert_file_name}" if @verbose
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
)
250 Utilities
.bail("Security tool add-certificates returned an error for #{full_root_path}") if $
? !
= 0
252 cmd_str
= CertTools
.security_tool_path
253 cmd_str +
= " -q add-trusted-cert -i "
254 cmd_str +
= Utilities
.quote_str(@setting_file_path)
256 cmd_str +
= Utilities
.quote_str(@setting_file_path)
258 cmd_str +
= Utilities
.quote_str(@root_cert_kc_path)
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}"
268 num_root_certs = new_num_certs
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)
280 # Delete the temp keychain
281 def delete_temp_keychain()
282 FileUtils.rm_rf(@temp_kc_path) if FileTest.exists? @temp_kc_path
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)
297 cmd_str +
= Utilities
.quote_str(@setting_file_path)
299 cmd_str +
= Utilities
.quote_str(@temp_kc_path)
300 cmd_str +
= " -r deny "
301 cmd_str +
= Utilities
.quote_str(full_path
)
303 Utilities
.bail("Security add-trusted-cert returned an error for #{full_path}") if $
? !
= 0
307 # Process the distrusted certificates
309 process_certs("Explicitly distrusting certs", CertTools
.distrusted_certs_dir
)
312 # Process the revoked certificates
314 process_certs("Explicitly distrusting certs", CertTools
.revoked_certs_dir
)
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/)
326 # Ensure that all of the certs in the directory were added to the SystemRootCertificates.keychain file
327 def check_all_roots_added()
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
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
|
341 num_file_system_entries
= num_file_system_entries
- 1
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
350 # Set the file access for the SystemRootCertificates.keychain and
351 # SystemTrustSettings.plist files
353 FileUtils
.chmod
0644, @setting_file_path
354 FileUtils
.chmod
0644, @root_cert_kc_path
357 # Do all of the processing to create the SystemRootCertificates.keychain and
358 # SystemTrustSettings.plist files
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
364 Utilities
.bail("create_temp_keychain failed") if create_temp_keychain !
= 0
367 delete_temp_keychain()
368 Utilities
.bail("check_all_roots_added failes") if !check_all_roots_added
373 # =============================================================================
374 # Class: BuildCAKeychain
376 # Description: This class provides the necessary functionality to create the
377 # SystemCACertificates.keychain output file.
378 # =============================================================================
379 class BuildCAKeychain
381 attr_reader
:cert_kc_name
382 attr_reader
:cert_kc_path
386 # Initialize the output path for this instance
387 def initialize(verbose
= true)
390 @cert_kc_name = "SystemCACertificates.keychain"
391 @cert_kc_path = File
.join(CertTools
.output_keychain_path
, @cert_kc_name)
395 # Add all of the certificates in the certs directory to the
396 # SystemCACertificates.keychain file
398 CertTools
.createKeychain(@cert_kc_path, @cert_kc_name)
399 cert_path
= CertTools
.intermediate_certs_dir
401 puts
"Adding intermediate cderts to #{@cert_kc_path}" if @verbose
402 puts
"Intermediates #{cert_path}" if @verbose
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 "
411 cmd_str +
= Utilities
.quote_str(@cert_kc_path)
413 cmd_str +
= Utilities
.quote_str(full_path
)
415 Utilities
.bail("Security add-certificates returned an error for #{full_path}") if $
? !
= 0
418 FileUtils
.chmod
0644, @cert_kc_path
423 # =============================================================================
424 # Class: BuildEVRoots
426 # Description: This class provides the necessary functionality to create the
427 # EVRoots.plist output file.
428 # =============================================================================
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
439 attr
:evroots_config_data
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
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)
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
456 Utilities
.check_path(@evroots_config_path, false)
458 @evroots_kc_name = "EVRoots.keychain"
459 @evroots_kc_path = File
.join(CertTools
.build_dir
, @evroots_kc_name)
461 @evroots_plist_name = "EVRoots.plist"
462 @evroots_plist_path = File
.join(CertTools
.output_keychain_path
, @evroots_plist_name)
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?
470 @evroots_config_data = ""
471 File
.open(@evroots_config_path, "r") do |file
|
473 line
.gsub!
(/^#.*\n/, '')
475 line
.gsub!
(/^\s*\n/, '')
477 @evroots_config_data +
= line
483 # Break the string from the get_config_data method into an array of lines.
485 lines_str
= get_config_data
486 lines
= lines_str
.split("\n")
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
493 lines
= get_cert_lines
495 items
= line
.split('"')
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
)
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
507 Utilities
.bail("#{cmd_str} failed") if $
? !
= 0 && $
? !
= 256
508 end # items.each do |cert_file|
509 end # lines.each do |line|
512 # The second pass does the work to create the EVRoots.plist
514 lines
= get_cert_lines
517 # Split the line using a doulbe quote. This is needed to ensure that file names with spaces work
518 items
= line
.split('"')
520 # Get the oid string which is the first item in the array.
521 oid_str
= items
.shift
522 oid_str
.gsub!
(/\s/, '')
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
527 cmd_str
= @plistbuddy_tool_path +
" -c " +
'"' +
"add :#{oid_str} array" +
'"' +
" " +
@evroots_plist_path
529 Utilities
.bail("#{cmd_str} failed") if $
? !
= 0
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
)
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
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
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
}
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
558 Utilities
.bail("#{cmd_str} failed") if $
? !
= 0
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!
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
)
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
579 # All is well prepare for the next item to add to the array
582 end # items.each do |cert_file|
583 end # lines.each do |line|
586 # Do all of the necessary work for this class
588 CertTools
.saveKeychainList
589 CertTools
.createKeychain(@evroots_kc_path, @evroots_kc_name)
591 puts
"Removing #{@evroots_plist_path}" if @verbose
592 FileUtils
.rm_f
@evroots_plist_path
594 FileUtils
.chmod
0644, @evroots_plist_path
595 puts
"Built #{@evroots_plist_path} successfully" if @verbose
600 # Make the SystemRootCertificates.keychain and SystemTrustSettings.plist files
602 # To get verbose logging set this true
605 brkc
= BuildRootKeychains
.new(verbose
)
608 # Make the SystemCACertificates.keychain file
609 bcakc
= BuildCAKeychain
.new(verbose
)
612 # Make the EVRoots.plist file
613 bevr
= BuildEVRoots
.new(verbose
)
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
624 x509_anchors_path
= File
.join(CertTools
.certificate_dir
, "CertificateTool/BuildOSXRootKeychain/X509Anchors")
625 output_dir
= File
.join(CertTools
.output_keychain_path
, "X509Anchors")
626 FileUtils
.cp x509_anchors_path
, output_dir
628 puts
"That's all folks!"