Chris@0: # $Id: ber.rb 142 2006-07-26 12:20:33Z blackhedd $ Chris@0: # Chris@0: # NET::BER Chris@0: # Mixes ASN.1/BER convenience methods into several standard classes. Chris@0: # Also provides BER parsing functionality. Chris@0: # Chris@0: #---------------------------------------------------------------------------- Chris@0: # Chris@0: # Copyright (C) 2006 by Francis Cianfrocca. All Rights Reserved. Chris@0: # Chris@0: # Gmail: garbagecat10 Chris@0: # Chris@0: # This program is free software; you can redistribute it and/or modify Chris@0: # it under the terms of the GNU General Public License as published by Chris@0: # the Free Software Foundation; either version 2 of the License, or Chris@0: # (at your option) any later version. Chris@0: # Chris@0: # This program is distributed in the hope that it will be useful, Chris@0: # but WITHOUT ANY WARRANTY; without even the implied warranty of Chris@0: # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the Chris@0: # GNU General Public License for more details. Chris@0: # Chris@0: # You should have received a copy of the GNU General Public License Chris@0: # along with this program; if not, write to the Free Software Chris@0: # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA Chris@0: # Chris@0: #--------------------------------------------------------------------------- Chris@0: # Chris@0: # Chris@0: Chris@0: Chris@0: Chris@0: Chris@0: module Net Chris@0: Chris@0: module BER Chris@0: Chris@0: class BerError < Exception; end Chris@0: Chris@0: Chris@0: # This module is for mixing into IO and IO-like objects. Chris@0: module BERParser Chris@0: Chris@0: # The order of these follows the class-codes in BER. Chris@0: # Maybe this should have been a hash. Chris@0: TagClasses = [:universal, :application, :context_specific, :private] Chris@0: Chris@0: BuiltinSyntax = { Chris@0: :universal => { Chris@0: :primitive => { Chris@0: 1 => :boolean, Chris@0: 2 => :integer, Chris@0: 4 => :string, Chris@0: 10 => :integer, Chris@0: }, Chris@0: :constructed => { Chris@0: 16 => :array, Chris@0: 17 => :array Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: # Chris@0: # read_ber Chris@0: # TODO: clean this up so it works properly with partial Chris@0: # packets coming from streams that don't block when Chris@0: # we ask for more data (like StringIOs). At it is, Chris@0: # this can throw TypeErrors and other nasties. Chris@0: # Chris@0: def read_ber syntax=nil Chris@0: return nil if (StringIO == self.class) and eof? Chris@0: Chris@0: id = getc # don't trash this value, we'll use it later Chris@0: tag = id & 31 Chris@0: tag < 31 or raise BerError.new( "unsupported tag encoding: #{id}" ) Chris@0: tagclass = TagClasses[ id >> 6 ] Chris@0: encoding = (id & 0x20 != 0) ? :constructed : :primitive Chris@0: Chris@0: n = getc Chris@0: lengthlength,contentlength = if n <= 127 Chris@0: [1,n] Chris@0: else Chris@0: j = (0...(n & 127)).inject(0) {|mem,x| mem = (mem << 8) + getc} Chris@0: [1 + (n & 127), j] Chris@0: end Chris@0: Chris@0: newobj = read contentlength Chris@0: Chris@0: objtype = nil Chris@0: [syntax, BuiltinSyntax].each {|syn| Chris@0: if syn && (ot = syn[tagclass]) && (ot = ot[encoding]) && ot[tag] Chris@0: objtype = ot[tag] Chris@0: break Chris@0: end Chris@0: } Chris@0: Chris@0: obj = case objtype Chris@0: when :boolean Chris@0: newobj != "\000" Chris@0: when :string Chris@0: (newobj || "").dup Chris@0: when :integer Chris@0: j = 0 Chris@0: newobj.each_byte {|b| j = (j << 8) + b} Chris@0: j Chris@0: when :array Chris@0: seq = [] Chris@0: sio = StringIO.new( newobj || "" ) Chris@0: # Interpret the subobject, but note how the loop Chris@0: # is built: nil ends the loop, but false (a valid Chris@0: # BER value) does not! Chris@0: while (e = sio.read_ber(syntax)) != nil Chris@0: seq << e Chris@0: end Chris@0: seq Chris@0: else Chris@0: raise BerError.new( "unsupported object type: class=#{tagclass}, encoding=#{encoding}, tag=#{tag}" ) Chris@0: end Chris@0: Chris@0: # Add the identifier bits into the object if it's a String or an Array. Chris@0: # We can't add extra stuff to Fixnums and booleans, not that it makes much sense anyway. Chris@0: obj and ([String,Array].include? obj.class) and obj.instance_eval "def ber_identifier; #{id}; end" Chris@0: obj Chris@0: Chris@0: end Chris@0: Chris@0: end # module BERParser Chris@0: end # module BER Chris@0: Chris@0: end # module Net Chris@0: Chris@0: Chris@0: class IO Chris@0: include Net::BER::BERParser Chris@0: end Chris@0: Chris@0: require "stringio" Chris@0: class StringIO Chris@0: include Net::BER::BERParser Chris@0: end Chris@0: Chris@0: begin Chris@0: require 'openssl' Chris@0: class OpenSSL::SSL::SSLSocket Chris@0: include Net::BER::BERParser Chris@0: end Chris@0: rescue LoadError Chris@0: # Ignore LoadError. Chris@0: # DON'T ignore NameError, which means the SSLSocket class Chris@0: # is somehow unavailable on this implementation of Ruby's openssl. Chris@0: # This may be WRONG, however, because we don't yet know how Ruby's Chris@0: # openssl behaves on machines with no OpenSSL library. I suppose Chris@0: # it's possible they do not fail to require 'openssl' but do not Chris@0: # create the classes. So this code is provisional. Chris@0: # Also, you might think that OpenSSL::SSL::SSLSocket inherits from Chris@0: # IO so we'd pick it up above. But you'd be wrong. Chris@0: end Chris@0: Chris@0: class String Chris@0: def read_ber syntax=nil Chris@0: StringIO.new(self).read_ber(syntax) Chris@0: end Chris@0: end Chris@0: Chris@0: Chris@0: Chris@0: #---------------------------------------------- Chris@0: Chris@0: Chris@0: class FalseClass Chris@0: # Chris@0: # to_ber Chris@0: # Chris@0: def to_ber Chris@0: "\001\001\000" Chris@0: end Chris@0: end Chris@0: Chris@0: Chris@0: class TrueClass Chris@0: # Chris@0: # to_ber Chris@0: # Chris@0: def to_ber Chris@0: "\001\001\001" Chris@0: end Chris@0: end Chris@0: Chris@0: Chris@0: Chris@0: class Fixnum Chris@0: # Chris@0: # to_ber Chris@0: # Chris@0: def to_ber Chris@0: i = [self].pack('w') Chris@0: [2, i.length].pack("CC") + i Chris@0: end Chris@0: Chris@0: # Chris@0: # to_ber_enumerated Chris@0: # Chris@0: def to_ber_enumerated Chris@0: i = [self].pack('w') Chris@0: [10, i.length].pack("CC") + i Chris@0: end Chris@0: Chris@0: # Chris@0: # to_ber_length_encoding Chris@0: # Chris@0: def to_ber_length_encoding Chris@0: if self <= 127 Chris@0: [self].pack('C') Chris@0: else Chris@0: i = [self].pack('N').sub(/^[\0]+/,"") Chris@0: [0x80 + i.length].pack('C') + i Chris@0: end Chris@0: end Chris@0: Chris@0: end # class Fixnum Chris@0: Chris@0: Chris@0: class Bignum Chris@0: Chris@0: def to_ber Chris@0: i = [self].pack('w') Chris@0: i.length > 126 and raise Net::BER::BerError.new( "range error in bignum" ) Chris@0: [2, i.length].pack("CC") + i Chris@0: end Chris@0: Chris@0: end Chris@0: Chris@0: Chris@0: Chris@0: class String Chris@0: # Chris@0: # to_ber Chris@0: # A universal octet-string is tag number 4, Chris@0: # but others are possible depending on the context, so we Chris@0: # let the caller give us one. Chris@0: # The preferred way to do this in user code is via to_ber_application_sring Chris@0: # and to_ber_contextspecific. Chris@0: # Chris@0: def to_ber code = 4 Chris@0: [code].pack('C') + length.to_ber_length_encoding + self Chris@0: end Chris@0: Chris@0: # Chris@0: # to_ber_application_string Chris@0: # Chris@0: def to_ber_application_string code Chris@0: to_ber( 0x40 + code ) Chris@0: end Chris@0: Chris@0: # Chris@0: # to_ber_contextspecific Chris@0: # Chris@0: def to_ber_contextspecific code Chris@0: to_ber( 0x80 + code ) Chris@0: end Chris@0: Chris@0: end # class String Chris@0: Chris@0: Chris@0: Chris@0: class Array Chris@0: # Chris@0: # to_ber_appsequence Chris@0: # An application-specific sequence usually gets assigned Chris@0: # a tag that is meaningful to the particular protocol being used. Chris@0: # This is different from the universal sequence, which usually Chris@0: # gets a tag value of 16. Chris@0: # Now here's an interesting thing: We're adding the X.690 Chris@0: # "application constructed" code at the top of the tag byte (0x60), Chris@0: # but some clients, notably ldapsearch, send "context-specific Chris@0: # constructed" (0xA0). The latter would appear to violate RFC-1777, Chris@0: # but what do I know? We may need to change this. Chris@0: # Chris@0: Chris@0: def to_ber id = 0; to_ber_seq_internal( 0x30 + id ); end Chris@0: def to_ber_set id = 0; to_ber_seq_internal( 0x31 + id ); end Chris@0: def to_ber_sequence id = 0; to_ber_seq_internal( 0x30 + id ); end Chris@0: def to_ber_appsequence id = 0; to_ber_seq_internal( 0x60 + id ); end Chris@0: def to_ber_contextspecific id = 0; to_ber_seq_internal( 0xA0 + id ); end Chris@0: Chris@0: private Chris@0: def to_ber_seq_internal code Chris@0: s = self.to_s Chris@0: [code].pack('C') + s.length.to_ber_length_encoding + s Chris@0: end Chris@0: Chris@0: end # class Array Chris@0: Chris@0: