lib/r509/subject.rb in r509-0.8.1 vs lib/r509/subject.rb in r509-0.9

- old
+ new

@@ -1,133 +1,226 @@ require "openssl" module R509 - #subject class. Used for building OpenSSL::X509::Name objects in a sane fashion - class Subject - # @param [Array, OpenSSL::X509::Name, R509::Subject] arg - def initialize(arg=nil) - case arg - when Array - @array = arg - when OpenSSL::X509::Name - sanitizer = R509::NameSanitizer.new - @array = sanitizer.sanitize(arg) - when R509::Subject - @array = arg.to_a - else - @array = [] - end - - # see if X509 thinks this is okay - name + # subject class. Used for building OpenSSL::X509::Name objects in a sane fashion + # @example + # subject = R509::Subject.new + # subject.CN= "test.test" + # subject.organization= "r509 LLC" + # @example + # subject = R509::Subject.new([['CN','test.test'],['O','r509 LLC']]) + # @example + # # you can also use the friendly getter/setters with custom OIDs + # R509::OIDMapper.register("1.2.3.4.5.6.7.8","COI","customOID") + # subject = R509::Subject.new + # subject.COI="test" + # # or + # subject.customOID="test" + # # or + # subject.custom_oid="test" + class Subject + # @param [Array, OpenSSL::X509::Name, R509::Subject, DER, nil] arg + def initialize(arg=nil) + if arg.kind_of?(Array) + @array = arg + elsif arg.kind_of?(OpenSSL::X509::Name) + sanitizer = R509::NameSanitizer.new + @array = sanitizer.sanitize(arg) + elsif arg.kind_of?(R509::Subject) + @array = arg.to_a + else + @array = [] + if not (begin OpenSSL::ASN1.decode(arg) rescue nil end).nil? + parse_asn1(arg) end + end - # @return [OpenSSL::X509::Name] - def name - OpenSSL::X509::Name.new(@array) - end + # see if X509 thinks this is okay + name + end - # @return [Boolean] - def empty? - @array.empty? + # @return [OpenSSL::X509::Name] + def name + OpenSSL::X509::Name.new(@array) + end + + # @return [Boolean] + def empty? + @array.empty? + end + + # get value for key + def [](key) + @array.each do |item| + if key == item[0] + return item[1] end + end + return nil + end - # get value for key - def [](key) - @array.each do |item| - if key == item[0] - return item[1] - end - end - return nil + # set key and value + def []=(key, value) + added = false + @array = @array.map{ |item| + if key == item[0] + added = true + [key, value] + else + item end + } - # set key and value - def []=(key, value) - added = false - @array = @array.map{ |item| - if key == item[0] - added = true - [key, value] - else - item - end - } + if not added + @array << [key, value] + end - if not added - @array << [key, value] - end + # see if X509 thinks this is okay + name - # see if X509 thinks this is okay - name + @array + end - @array - end + # @param [String] key item you want deleted + def delete(key) + @array = @array.select do |item| + item[0] != key + end + end - # @param [String] key item you want deleted - def delete(key) - @array = @array.select do |item| - item[0] != key - end - end + # @return [String] string of form /CN=something.com/O=whatever/L=Locality + def to_s + name.to_s + end - # @return [String] string of form /CN=something.com/O=whatever/L=Locality - def to_s - name.to_s + # @return [Array] Array of form [['CN','langui.sh']] + def to_a + @array + end + + # @private + def respond_to?(method_sym, include_private = false) + method_sym.to_s =~ /([^=]*)/ + oid = oid_check($1) + if not oid.nil? + true + else + super(method_sym, include_private) + end + end + + private + + # Try to build methods for getting/setting various subject attributes + # dynamically. this will also cache methods that get built via instance_eval. + # This code will also allow you to set subject items for custom oids + # defined via R509::OIDMapper + # + def method_missing(method_sym, *args, &block) + if method_sym.to_s =~ /(.*)=$/ + sn = oid_check($1) + if not sn.nil? + define_dynamic_setter(method_sym,sn) + send(method_sym, args.first) + else + return super(method_sym, *args, &block) end + else + sn = oid_check(method_sym) + if not sn.nil? + define_dynamic_getter(method_sym,sn) + send(method_sym) + else + return super(method_sym, *args, &block) + end + end + end - # @return [Array] Array of form [['CN','langui.sh'],['O','Org']] - def to_a - @array + def define_dynamic_setter(name,sn) + instance_eval <<-RUBY + def #{name.to_s}(value) + self["#{sn}"]= value end + RUBY end - # Sanitize an X509::Name. The #to_a method replaces unknown OIDs with "UNDEF", but the #to_s - # method doesn't. What we want to do is build the array that would have been produced by #to_a - # if it didn't throw away the OID. - class NameSanitizer - # @option name [OpenSSL::X509::Name] - # @return [Array] array of the form [["OID", "VALUE], ["OID", "VALUE"]] with "UNDEF" replaced by the actual OID - def sanitize(name) - line = name.to_s - array = name.to_a.dup - used_oids = [] - undefined_components(array).each do |component| - begin - # get the OID from the subject line that has this value - oids = line.scan(/\/([\d\.]+)=#{component[:value]}/).flatten - if oids.size == 1 - oid = oids.first - else - oid = oids.select{ |match| not used_oids.include?(match) }.first - end - # replace the "UNDEF" OID name in the array at the index the UNDEF was found - array[component[:index]][0] = oid - # remove the first occurrence of this in the subject line (so we can handle the same oid/value pair multiple times) - line = line.sub("/#{oid}=#{component[:value]}", "") - # we record which OIDs we've used in case two different unknown OIDs have the same value - used_oids << oid - rescue - # I don't expect this to happen, but if it does we'll just not replace UNDEF and continue - end - end - array + def define_dynamic_getter(name,sn) + instance_eval <<-RUBY + def #{name.to_s} + self["#{sn}"] end + RUBY + end - private + def oid_check(name) + oid = OpenSSL::ASN1::ObjectId.new(camelize(name)) + oid.short_name + end - # get the components from #to_a that are UNDEF - # @option array [Array<OpenSSL::X509::Name>] - # @return [Hash] - # @example - # Return value looks like - # { :index => the index in the original array where we found an UNDEF, :value => the subject component value } - def undefined_components(array) - components = [] - array.each_index do |index| - components << { :index => index, :value => array[index][1] } if array[index][0] == "UNDEF" - end - components + def camelize(sym) + sym.to_s.split('_').inject([]){ |buffer,e| buffer.push(buffer.empty? ? e : e.capitalize) }.join + end + + def parse_asn1(asn) + asn = OpenSSL::ASN1.decode asn + # parsing a subject DN + # We have to iterate a sequence, which holds sets. Each set has one value: a sequence, which has 2 values + # So it's effectively an array of arrays which each have only one element, which is an array of 2 values. + asn.value.each do |set| + sn = set.value.first.value.first.value + val = set.value.first.value.last.value + self[sn] = val + end + end + end + + # Sanitize an X509::Name. The #to_a method replaces unknown OIDs with "UNDEF", but the #to_s + # method doesn't. What we want to do is build the array that would have been produced by #to_a + # if it didn't throw away the OID. + # This method is not required as of ruby-1.9.3p125 and up. + class NameSanitizer + # @option name [OpenSSL::X509::Name] + # @return [Array] array of the form [["OID", "VALUE], ["OID", "VALUE"]] with "UNDEF" replaced by the actual OID + def sanitize(name) + line = name.to_s + array = name.to_a.dup + used_oids = [] + undefined_components(array).each do |component| + begin + # get the OID from the subject line that has this value + oids = line.scan(/\/([\d\.]+)=#{component[:value]}/).flatten + if oids.size == 1 + oid = oids.first + else + oid = oids.select{ |match| not used_oids.include?(match) }.first + end + # replace the "UNDEF" OID name in the array at the index the UNDEF was found + array[component[:index]][0] = oid + # remove the first occurrence of this in the subject line (so we can handle the same oid/value pair multiple times) + line = line.sub("/#{oid}=#{component[:value]}", "") + # we record which OIDs we've used in case two different unknown OIDs have the same value + used_oids << oid + rescue + # I don't expect this to happen, but if it does we'll just not replace UNDEF and continue end + end + array end + + private + + # get the components from #to_a that are UNDEF + # @option array [Array<OpenSSL::X509::Name>] + # @return [Hash] + # @example + # Return value looks like + # { :index => the index in the original array where we found an UNDEF, :value => the subject component value } + def undefined_components(array) + components = [] + array.each_index do |index| + components << { :index => index, :value => array[index][1] } if array[index][0] == "UNDEF" + end + components + end + end end