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