]> git.saurik.com Git - apple/security.git/blob - certificates/CertificateTool/BuildOSXRootKeychain/buildRootKeychain.rb
Security-57031.1.35.tar.gz
[apple/security.git] / certificates / CertificateTool / BuildOSXRootKeychain / buildRootKeychain.rb
1 #!/usr/bin/env ruby -wKU
2 require 'FileUtils'
3 require '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 # =============================================================================
15 class 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
49 end
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 # ==============================================================================
62 class 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
185 end
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 # =============================================================================
194 class 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
371 end
372
373 # =============================================================================
374 # Class: BuildCAKeychain
375 #
376 # Description: This class provides the necessary functionality to create the
377 # SystemCACertificates.keychain output file.
378 # =============================================================================
379 class 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
420 end
421
422
423 # =============================================================================
424 # Class: BuildEVRoots
425 #
426 # Description: This class provides the necessary functionality to create the
427 # EVRoots.plist output file.
428 # =============================================================================
429 class 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
598 end
599
600 # Make the SystemRootCertificates.keychain and SystemTrustSettings.plist files
601
602 # To get verbose logging set this true
603 verbose = false;
604
605 brkc = BuildRootKeychains.new(verbose)
606 brkc.do_processing
607
608 # Make the SystemCACertificates.keychain file
609 bcakc = BuildCAKeychain.new(verbose)
610 bcakc.do_processing
611
612 # Make the EVRoots.plist file
613 bevr = BuildEVRoots.new(verbose)
614 bevr.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
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
627
628 puts "That's all folks!"