• Home
  • History
  • Annotate
  • Line#
  • Navigate
  • Raw
  • Download
  • only in /macosx-10.10/Security-57031.1.35/certificates/CertificateTool/BuildOSXRootKeychain/
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