class RASN1::Types::Base

@abstract This is base class for all ASN.1 types.

Subclasses SHOULD define:
* an ID constant defining ASN.1 BER/DER identification number,
* a private method {#value_to_der} converting its {#value} to DER,
* a private method {#der_to_value} converting DER into {#value}.

Define an optional value

An optional value may be defined using :optional key from {#initialize}:

Integer.new(:int, optional: true)

An optional value implies:

Define a default value

A default value may be defined using :default key from {#initialize}:

Integer.new(:int, default: 0)

A default value implies:

Define a tagged value

ASN.1 permits to define tagged values. By example:

-- context specific tag
CType ::= [0] EXPLICIT INTEGER
-- application specific tag
AType ::= [APPLICATION 1] EXPLICIT INTEGER
-- private tag
PType ::= [PRIVATE 2] EXPLICIT INTEGER

These types may be defined as:

ctype = RASN1::Types::Integer.new(explicit: 0)                      # with explicit, default #asn1_class is :context
atype = RASN1::Types::Integer.new(explicit: 1, class: :application)
ptype = RASN1::Types::Integer.new(explicit: 2, class: :private)

Sometimes, an EXPLICIT type should be CONSTRUCTED. To do that, use :constructed option:

ptype = RASN1::Types::Integer.new(explicit: 2, class: :private, constructed: true)

Implicit tagged values may also be defined:

ctype_implicit = RASN1::Types::Integer.new(implicit: 0)

@author Sylvain Daubert

Constants

CLASSES

Allowed ASN.1 classes

CLASS_MASK

Binary mask to get class @private

INDEFINITE_LENGTH

Length value for indefinite length

MULTI_OCTETS_ID

@private first octet identifier for multi-octets identifier

Attributes

asn1_class[R]

@return [Symbol]

default[R]

@return [Object,nil] default value, if defined

name[R]

@return [String,nil]

value[W]

@return [Object]

Public Class Methods

encoded_type() click to toggle source

Get ASN.1 type used to encode this one @return [String]

# File lib/rasn1/types/base.rb, line 85
def self.encoded_type
  type
end
new(value_or_options={}, options={}) click to toggle source

@overload initialize(options={})

@param [Hash] options
@option options [Symbol] :class ASN.1 class. Default value is +:universal+.
 If +:explicit+ or +:implicit:+ is defined, default value is +:context+.
@option options [::Boolean] :optional define this tag as optional. Default
  is +false+
@option options [Object] :default default value (ASN.1 DEFAULT)
@option options [Object] :value value to set
@option options [::Integer] :implicit define an IMPLICIT tagged type
@option options [::Integer] :explicit define an EXPLICIT tagged type
@option options [::Boolean] :constructed if +true+, set type as constructed.
 May only be used when +:explicit+ is defined, else it is discarded.
@option options [::String] :name name for this node

@overload initialize(value, options={})

@param [Object] value value to set for this ASN.1 object
@param [Hash] options
@option options [Symbol] :class ASN.1 class. Default value is +:universal+.
 If +:explicit+ or +:implicit:+ is defined, default value is +:context+.
@option options [::Boolean] :optional define this value as optional. Default
  is +false+
@option options [Object] :default default value (ASN.1 DEFAULT)
@option options [::Integer] :implicit define an IMPLICIT tagged type
@option options [::Integer] :explicit define an EXPLICIT tagged type
@option options [::Boolean] :constructed if +true+, set type as constructed.
 May only be used when +:explicit+ is defined, else it is discarded.
@option options [::String] :name name for this node
# File lib/rasn1/types/base.rb, line 126
def initialize(value_or_options={}, options={})
  @constructed = nil
  if value_or_options.is_a? Hash
    set_options value_or_options
  else
    set_options options
    @value = value_or_options
  end
end
parse(der_or_ber, options={}) click to toggle source

Parse a DER or BER string @param [String] der_or_ber string to parse @param [Hash] options @option options [Boolean] :ber if true, parse a BER string, else a DER one @note More options are supported. See {Base#initialize}.

# File lib/rasn1/types/base.rb, line 94
def self.parse(der_or_ber, options={})
  obj = self.new(options)
  obj.parse!(der_or_ber, ber: options[:ber])
  obj
end
type() click to toggle source

Get ASN.1 type @return [String]

# File lib/rasn1/types/base.rb, line 77
def self.type
  return @type if defined? @type

  @type = self.to_s.gsub(/.*::/, '').gsub(/([a-z0-9])([A-Z])/, '\1 \2').upcase
end

Public Instance Methods

==(other) click to toggle source

Objects are equal if they have same class AND same DER @param [Base] other @return [Boolean]

# File lib/rasn1/types/base.rb, line 250
def ==(other)
  (other.class == self.class) && (other.to_der == self.to_der)
end
constructed?() click to toggle source

@return [::Boolean] true if this is a constructed type

# File lib/rasn1/types/base.rb, line 189
def constructed?
  (self.class < Constructed) || !!@constructed
end
explicit?() click to toggle source

Say if a tagged type is explicit @return [::Boolean,nil] return nil if not tagged, return true

if explicit, else +false+
# File lib/rasn1/types/base.rb, line 165
def explicit?
  !defined?(@tag) ? nil : @tag == :explicit
end
id() click to toggle source

Get identifier value @return [Integer]

# File lib/rasn1/types/base.rb, line 201
def id
  id_value
end
implicit?() click to toggle source

Say if a tagged type is implicit @return [::Boolean,nil] return nil if not tagged, return true

if implicit, else +false+
# File lib/rasn1/types/base.rb, line 172
def implicit?
  !defined?(@tag) ? nil : @tag == :implicit
end
initialize_copy(_other) click to toggle source

Used by #dup and #clone. Deep copy @value and @default.

# File lib/rasn1/types/base.rb, line 137
def initialize_copy(_other)
  @value = @value.dup
  @default = @default.dup
end
inspect(level=0) click to toggle source

@param [Integer] level @return [String]

# File lib/rasn1/types/base.rb, line 239
def inspect(level=0)
  str = common_inspect(level)
  str << " #{value.inspect}"
  str << ' OPTIONAL' if optional?
  str << " DEFAULT #{@default}" unless @default.nil?
  str
end
optional?() click to toggle source

@return [::Boolean]

# File lib/rasn1/types/base.rb, line 152
def optional?
  @optional
end
parse!(der, ber: false) click to toggle source

@abstract This method SHOULD be partly implemented by subclasses to parse

data. Subclasses SHOULD respond to +#der_to_value+.

Parse a DER string. This method updates object. @param [String] der DER string @param [Boolean] ber if true, accept BER encoding @return [Integer] total number of parsed bytes @raise [ASN1Error] error on parsing

# File lib/rasn1/types/base.rb, line 212
def parse!(der, ber: false)
  return 0 unless check_id(der)

  id_size = Types.decode_identifier_octets(der).last
  total_length, data = get_data(der[id_size..-1], ber)
  total_length += id_size
  if explicit?
    # Delegate to #explicit type to generate sub-value
    type = explicit_type
    type.value = @value
    type.parse!(data)
    @value = type.value
  else
    der_to_value(data, ber: ber)
  end

  total_length
end
primitive?() click to toggle source

@return [::Boolean] true if this is a primitive type

# File lib/rasn1/types/base.rb, line 184
def primitive?
  (self.class < Primitive) && !@constructed
end
tagged?() click to toggle source

Say if this type is tagged or not @return [::Boolean]

# File lib/rasn1/types/base.rb, line 158
def tagged?
  !@tag.nil?
end
to_der() click to toggle source

@abstract This method SHOULD be partly implemented by subclasses, which

SHOULD respond to +#value_to_der+.

@return [String] DER-formated string

# File lib/rasn1/types/base.rb, line 179
def to_der
  build
end
type() click to toggle source

Get ASN.1 type @return [String]

# File lib/rasn1/types/base.rb, line 195
def type
  self.class.type
end
value() click to toggle source

Get value or default value

# File lib/rasn1/types/base.rb, line 143
def value
  if @value.nil?
    @default
  else
    @value
  end
end
value_size() click to toggle source

Give size in octets of encoded value @return [Integer]

# File lib/rasn1/types/base.rb, line 233
def value_size
  value_to_der.size
end

Private Instance Methods

bin2hex(str) click to toggle source
# File lib/rasn1/types/base.rb, line 482
def bin2hex(str)
  str.unpack1('H*')
end
build() click to toggle source
# File lib/rasn1/types/base.rb, line 341
def build
  if can_build?
    if explicit?
      v = explicit_type
      v.value = @value
      encoded_value = v.to_der
    else
      encoded_value = value_to_der
    end
    encode_identifier_octets << encode_size(encoded_value.size) << encoded_value
  else
    ''
  end
end
can_build?() click to toggle source
# File lib/rasn1/types/base.rb, line 336
def can_build?
  !(!@default.nil? && (@value.nil? || (@value == @default))) &&
    !(optional? && @value.nil?)
end
check_id(der) click to toggle source
# File lib/rasn1/types/base.rb, line 403
def check_id(der)
  expected_id = encode_identifier_octets
  real_id = der[0, expected_id.size]
  if real_id != expected_id
    if optional?
      @value = nil
    elsif !@default.nil?
      @value = @default
    else
      raise_id_error(der)
    end
    false
  else
    true
  end
end
class_from_numeric_id(id) click to toggle source
# File lib/rasn1/types/base.rb, line 458
def class_from_numeric_id(id)
  CLASSES.key(id & CLASS_MASK)
end
common_inspect(level) click to toggle source
# File lib/rasn1/types/base.rb, line 266
def common_inspect(level)
  lvl = level >= 0 ? level : 0
  str = '  ' * lvl
  str << "#{@name} " unless @name.nil?
  str << "#{type}:"
end
der2name(der) click to toggle source
# File lib/rasn1/types/base.rb, line 471
def der2name(der)
  return 'no ID' if der.nil? || der.empty?

  asn1_class, pc, id, id_size = Types.decode_identifier_octets(der)
  name = +"#{asn1_class.to_s.upcase} #{pc.to_s.upcase}"
  type =  Types.constants.map { |c| Types.const_get(c) }
               .select { |klass| klass < Primitive || klass < Constructed }
               .find { |klass| klass::ID == id }
  name << " #{type.nil? ? '0x%X (0x%s)' % [id, bin2hex(der[0...id_size])] : type.encoded_type}"
end
der_to_value(der, ber: false) click to toggle source
# File lib/rasn1/types/base.rb, line 282
def der_to_value(der, ber: false)
  @value = der
end
encode_identifier_octets() click to toggle source
# File lib/rasn1/types/base.rb, line 362
def encode_identifier_octets
  id2octets.pack('C*')
end
encode_size(size) click to toggle source
# File lib/rasn1/types/base.rb, line 388
def encode_size(size)
  if size >= INDEFINITE_LENGTH
    bytes = []
    while size > 255
      bytes.unshift(size & 0xff)
      size >>= 8
    end
    bytes.unshift(size)
    bytes.unshift(INDEFINITE_LENGTH | bytes.size)
    bytes.pack('C*')
  else
    [size].pack('C')
  end
end
explicit_type() click to toggle source
# File lib/rasn1/types/base.rb, line 448
def explicit_type
  self.class.new
end
get_data(der, ber) click to toggle source
# File lib/rasn1/types/base.rb, line 420
def get_data(der, ber)
  length = der[0, 1].unpack1('C')
  length_length = 0

  if length == INDEFINITE_LENGTH
    if primitive?
      raise ASN1Error, "malformed #{type}: indefinite length " \
        'forbidden for primitive types'
    elsif ber
      raise NotImplementedError, 'indefinite length not supported'
    else
      raise ASN1Error, 'indefinite length forbidden in DER encoding'
    end
  elsif length < INDEFINITE_LENGTH
    data = der[1, length]
  else
    length_length = length & 0x7f
    length = der[1, length_length].unpack('C*')
                                  .reduce(0) { |len, b| (len << 8) | b }
    data = der[1 + length_length, length]
  end

  total_length = 1 + length
  total_length += length_length if length_length.positive?

  [total_length, data]
end
id2octets() click to toggle source
# File lib/rasn1/types/base.rb, line 366
def id2octets
  first_octet = CLASSES[asn1_class] | pc_bit
  if id < MULTI_OCTETS_ID
    [first_octet | id]
  else
    [first_octet | MULTI_OCTETS_ID] + unsigned_to_chained_octets(id)
  end
end
id_value() click to toggle source
# File lib/rasn1/types/base.rb, line 356
def id_value
  return @id_value if defined? @id_value

  self.class::ID
end
pc_bit() click to toggle source
# File lib/rasn1/types/base.rb, line 256
def pc_bit
  if @constructed.nil?
    self.class::ASN1_PC
  elsif @constructed # true
    Constructed::ASN1_PC
  else # false
    Primitive::ASN1_PC
  end
end
raise_id_error(der) click to toggle source
# File lib/rasn1/types/base.rb, line 452
def raise_id_error(der)
  msg = name.nil? ? +'' : +"#{name}: "
  msg << "Expected #{self2name} but get #{der2name(der)}"
  raise ASN1Error, msg
end
self2name() click to toggle source
# File lib/rasn1/types/base.rb, line 462
def self2name
  name = +"#{asn1_class.to_s.upcase} #{constructed? ? 'CONSTRUCTED' : 'PRIMITIVE'}"
  if implicit? || explicit?
    name << ' 0x%X (0x%s)' % [id, bin2hex(encode_identifier_octets)]
  else
    name << ' ' << self.class.type
  end
end
set_class(asn1_class) click to toggle source
# File lib/rasn1/types/base.rb, line 296
def set_class(asn1_class)
  case asn1_class
  when nil
    @asn1_class = :universal
  when Symbol
    raise ClassError unless CLASSES.key? asn1_class

    @asn1_class = asn1_class
  else
    raise ClassError
  end
end
set_default(default) click to toggle source
# File lib/rasn1/types/base.rb, line 313
def set_default(default)
  @default = default
end
set_optional(optional) click to toggle source
# File lib/rasn1/types/base.rb, line 309
def set_optional(optional)
  @optional = !!optional
end
set_options(options) click to toggle source
# File lib/rasn1/types/base.rb, line 286
def set_options(options)
  set_class options[:class]
  set_optional options[:optional]
  set_default options[:default]
  set_tag options
  @value = options[:value]
  @name = options[:name]
  @options = options
end
set_tag(options) click to toggle source

handle undocumented option :tag_value, used internally by {RASN1.parse} to parse non-universal class tags.

# File lib/rasn1/types/base.rb, line 319
def set_tag(options)
  if options[:explicit]
    @tag = :explicit
    @id_value = options[:explicit]
    @constructed = options[:constructed]
  elsif options[:implicit]
    @tag = :implicit
    @id_value = options[:implicit]
    @constructed = options[:constructed]
  elsif options[:tag_value]
    @id_value = options[:tag_value]
    @constructed = options[:constructed]
  end

  @asn1_class = :context if defined?(@tag) && (@asn1_class == :universal)
end
unsigned_to_chained_octets(value) click to toggle source

Encode an unsigned integer on multiple octets. Value is encoded on bit 6-0 of each octet, bit 7(MSB) indicates wether further octets follow.

# File lib/rasn1/types/base.rb, line 378
def unsigned_to_chained_octets(value)
  ary = []
  while value.positive?
    ary.unshift(value & 0x7f | 0x80)
    value >>= 7
  end
  ary[-1] &= 0x7f
  ary
end
value_to_der() click to toggle source
# File lib/rasn1/types/base.rb, line 273
def value_to_der
  case @value
  when Base
    @value.to_der
  else
    @value.to_s
  end
end