rasn2 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +22 -0
  3. data/README.md +4 -0
  4. data/lib/rasn1/errors.rb +55 -0
  5. data/lib/rasn1/helpers/colorize.rb +76 -0
  6. data/lib/rasn1/model.rb +831 -0
  7. data/lib/rasn1/schema_parser.rb +470 -0
  8. data/lib/rasn1/tracer.rb +200 -0
  9. data/lib/rasn1/types/any.rb +98 -0
  10. data/lib/rasn1/types/base.rb +675 -0
  11. data/lib/rasn1/types/bit_string.rb +100 -0
  12. data/lib/rasn1/types/bmp_string.rb +22 -0
  13. data/lib/rasn1/types/boolean.rb +57 -0
  14. data/lib/rasn1/types/choice.rb +158 -0
  15. data/lib/rasn1/types/constrained.rb +51 -0
  16. data/lib/rasn1/types/constructed.rb +51 -0
  17. data/lib/rasn1/types/enumerated.rb +44 -0
  18. data/lib/rasn1/types/generalized_time.rb +156 -0
  19. data/lib/rasn1/types/ia5_string.rb +21 -0
  20. data/lib/rasn1/types/integer.rb +166 -0
  21. data/lib/rasn1/types/null.rb +43 -0
  22. data/lib/rasn1/types/numeric_string.rb +41 -0
  23. data/lib/rasn1/types/object_id.rb +64 -0
  24. data/lib/rasn1/types/octet_string.rb +52 -0
  25. data/lib/rasn1/types/primitive.rb +13 -0
  26. data/lib/rasn1/types/printable_string.rb +42 -0
  27. data/lib/rasn1/types/sequence.rb +105 -0
  28. data/lib/rasn1/types/sequence_of.rb +199 -0
  29. data/lib/rasn1/types/set.rb +28 -0
  30. data/lib/rasn1/types/set_of.rb +18 -0
  31. data/lib/rasn1/types/tag.rb +189 -0
  32. data/lib/rasn1/types/universal_string.rb +22 -0
  33. data/lib/rasn1/types/utc_time.rb +67 -0
  34. data/lib/rasn1/types/utf8_string.rb +21 -0
  35. data/lib/rasn1/types/visible_string.rb +30 -0
  36. data/lib/rasn1/types.rb +149 -0
  37. data/lib/rasn1/value_notation.rb +256 -0
  38. data/lib/rasn1/version.rb +6 -0
  39. data/lib/rasn1/wrapper.rb +279 -0
  40. data/lib/rasn1.rb +51 -0
  41. metadata +123 -0
@@ -0,0 +1,199 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RASN1
4
+ module Types
5
+ # ASN.1 SEQUENCE OF
6
+ #
7
+ # A SEQUENCE OF is an array of one ASN.1 type.
8
+ #
9
+ # == Use with {Primitive} types
10
+ # To encode this ASN.1 example:
11
+ # Integers ::= SEQUENCE OF INTEGER
12
+ # do:
13
+ # # Create a SEQUENCE OF INTEGER
14
+ # seqof = RASN1::Types::SequenceOf.new(RASN1::Types::Integer)
15
+ # # Set integer values
16
+ # seqof.value = [1, 2, 3, 4].map { |i| RASN1::Types::Integer.new(value: i) }
17
+ # seqof << 5 # infer a RASN1::Types::Integer with given value
18
+ #
19
+ # == Use with {Constructed} types
20
+ # SEQUENCE OF may be used to create sequence of composed types. For example:
21
+ # composed_type = RASN1::Types::Sequence.new
22
+ # commposed_type.value = [RASN1::Types::Integer,
23
+ # RASN1::Types::OctetString]
24
+ # seqof = RASN1::Types::SequenceOf.new(composed_type)
25
+ # seqof << [0, 'data0']
26
+ # seqof << [1, 'data1']
27
+ #
28
+ # == Use with {Model}
29
+ # SEQUENCE OF may also be used with a Model type:
30
+ # class MyModel < RASN1::Model
31
+ # sequence :seq,
32
+ # content: [boolean(:bool), integer(:int)]
33
+ # end
34
+ #
35
+ # seqof = RASN1::Types::SequenceOf.new(:record, MyModel)
36
+ # # set values
37
+ # seqof << { bool: true, int: 12 }
38
+ # seqof << { bool: false, int: 65535 }
39
+ # # Generate DER string
40
+ # der = seqof.to_der # => String
41
+ # # parse
42
+ # seqof.parse! der
43
+ #
44
+ # After parsing, a SEQUENCE OF may be accessed as an Array:
45
+ # seqof[0] # => MyModel
46
+ # seqof[0][:bool].value # => true
47
+ # seqof[0][:int].value # => 12
48
+ # @author Sylvain Daubert
49
+ class SequenceOf < Constructed
50
+ # SequenceOf id value
51
+ ID = Sequence::ID
52
+
53
+ # @return [Class, Base]
54
+ attr_reader :of_type
55
+
56
+ # A SEQUENCE OF is encoded as a SEQUENCE.
57
+ # @return ['SEQUENCE']
58
+ def self.encoded_type
59
+ Sequence.encoded_type
60
+ end
61
+
62
+ # @param [Class, Base] of_type base type for sequence of
63
+ # @see Base#initialize
64
+ def initialize(of_type, options={})
65
+ super(options)
66
+ @of_type = of_type
67
+ @no_value = false
68
+ end
69
+
70
+ # Clone @#of_type and values
71
+ def initialize_copy(*)
72
+ super
73
+ @of_type = @of_type.dup
74
+ @value = @value.map(&:dup)
75
+ end
76
+
77
+ # @return [Array]
78
+ def void_value
79
+ []
80
+ end
81
+
82
+ # Add an item to SEQUENCE OF
83
+ # @param [Object] obj
84
+ # @note
85
+ # * if a SEQUENCE OF primitive type, +obj+ is an instance of primitive type or equivalent Ruby type
86
+ # * if a SEQUENCE OF composed type, +obj+ is an array of ruby type instances
87
+ # * if a SEQUENCE OF model, +obj+ is either a Model or a Hash
88
+ def <<(obj)
89
+ return push_primitive(obj) if of_type_class < Primitive
90
+ return push_composed_array(obj) if composed_of_type?
91
+
92
+ push_model(obj)
93
+ end
94
+
95
+ # Get element of index +idx+
96
+ # @param [Integer] idx
97
+ # @return [Base]
98
+ def [](idx)
99
+ @value[idx]
100
+ end
101
+
102
+ # Get length of SEQUENCE OF (ie number of elements)
103
+ # @return [Integer]
104
+ def length
105
+ @value.length
106
+ end
107
+
108
+ # @param [::Integer] level
109
+ # @return [String]
110
+ def inspect(level=0)
111
+ str = common_inspect(level)
112
+ str << "\n"
113
+ level = level.abs + 1
114
+ @value.each do |item|
115
+ case item
116
+ when Base, Model
117
+ next if item.optional? && item.value.nil?
118
+
119
+ str << item.inspect(level)
120
+ str << "\n" unless str.end_with?("\n")
121
+ else
122
+ str << (' ' * level) << "#{item.inspect}\n"
123
+ end
124
+ end
125
+ str
126
+ end
127
+
128
+ # Make sequence of value from +der+ string
129
+ # @param [String] der
130
+ # @param [::Boolean] ber
131
+ # @return [void]
132
+ def der_to_value(der, ber: false) # rubocop:disable Lint/UnusedMethodArgument
133
+ @value = []
134
+ nb_bytes = 0
135
+
136
+ while nb_bytes < der.length
137
+ type = of_type_class.new
138
+ nb_bytes += type.parse!(der[nb_bytes, der.length])
139
+ @value << type
140
+ end
141
+ end
142
+
143
+ private
144
+
145
+ def of_type_class
146
+ @of_type.is_a?(Class) ? @of_type : @of_type.class
147
+ end
148
+
149
+ def composed_of_type?
150
+ !@of_type.is_a?(Class) && [Sequence, Set].include?(of_type_class)
151
+ end
152
+
153
+ def value_to_der
154
+ @value.map(&:to_der).join
155
+ end
156
+
157
+ def explicit_type
158
+ self.class.new(self.of_type, name: name)
159
+ end
160
+
161
+ def push_primitive(obj)
162
+ @value <<
163
+ case obj
164
+ when Primitive
165
+ obj
166
+ else
167
+ of_type_class.new(value: obj)
168
+ end
169
+ end
170
+
171
+ def push_composed_array(obj)
172
+ raise ASN1Error, 'object to add should be an Array' unless obj.is_a?(Array)
173
+
174
+ new_value = of_type_class.new
175
+ @of_type.value.each_with_index do |type, i|
176
+ type2 = type.dup
177
+ type2.value = obj[i]
178
+ new_value.value << type2
179
+ end
180
+ @value << new_value
181
+ end
182
+
183
+ def push_model(obj)
184
+ case obj
185
+ when Hash
186
+ @value << of_type_class.new(obj)
187
+ when of_type_class
188
+ @value << obj
189
+ else
190
+ raise ASN1Error, "object to add should be a #{of_type_class} or a Hash"
191
+ end
192
+ end
193
+
194
+ def trace_data
195
+ ''
196
+ end
197
+ end
198
+ end
199
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RASN1
4
+ module Types
5
+ # ASN.1 set
6
+ #
7
+ # A set is a collection of another ASN.1 types.
8
+ #
9
+ # To encode this ASN.1 example:
10
+ # Record ::= SET {
11
+ # id INTEGER,
12
+ # room [0] INTEGER OPTIONAL,
13
+ # house [1] IMPLICIT INTEGER DEFAULT 0
14
+ # }
15
+ # do:
16
+ # set = RASN1::Types::Set.new
17
+ # set.value = [
18
+ # RASN1::Types::Integer
19
+ # RASN1::Types::Integer(explicit: 0, optional: true),
20
+ # RASN1::Types::Integer(implicit: 1, default: 0)
21
+ # ]
22
+ # @author Sylvain Daubert
23
+ class Set < Sequence
24
+ # Set id value
25
+ ID = 0x11
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RASN1
4
+ module Types
5
+ # ASN.1 SET OF
6
+ # @author Sylvain Daubert
7
+ class SetOf < SequenceOf
8
+ # SetOf id value
9
+ ID = Set::ID
10
+
11
+ # A SET OF is encoded as a SET.
12
+ # @return ['SET']
13
+ def self.encoded_type
14
+ Set.encoded_type
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,189 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RASN1
4
+ module Types
5
+ # ASN.1 Tag constructed type.
6
+ #
7
+ # A Tag type is a constructed element with schematized content
8
+ # (child elements, like a {Sequence}) that can be bound to flexible tag
9
+ # criteria instead of a fixed universal tag:
10
+ # * a single tag number,
11
+ # * a list of possible tag numbers,
12
+ # * any private-class tag.
13
+ #
14
+ # == Binding to a single tag
15
+ # # Context-class tag 5 (default class is :context)
16
+ # generic = Tag.new(tag: 5, value: [Integer.new, OctetString.new])
17
+ # # Application-class tag 10
18
+ # generic = Tag.new(tag: 10, class: :application, value: [Integer.new])
19
+ #
20
+ # == Binding to a list of tags
21
+ # generic = Tag.new(tag: [5, 6, 7], value: [Integer.new, OctetString.new])
22
+ #
23
+ # == Binding to any private tag
24
+ # generic = Tag.new(tag: :private, value: [Integer.new, OctetString.new])
25
+ # # or equivalently
26
+ # generic = Tag.new(any_private: true, value: [Integer.new])
27
+ class Tag < Constructed
28
+ # Placeholder ID — not used directly; tag is always dynamic.
29
+ ID = 0
30
+
31
+ # @return [Integer, nil] the tag number that was matched during parsing
32
+ attr_reader :matched_tag
33
+
34
+ # @param [Hash] options
35
+ # @option options [Integer, Array<Integer>, :private] :tag tag number(s) to
36
+ # bind to, or +:private+ to accept any private-class tag
37
+ # @option options [Boolean] :any_private if +true+, accept any private-class tag
38
+ # @see Base#initialize
39
+ def initialize(options={})
40
+ extract_tag_binding(options)
41
+ super
42
+ @no_value = false
43
+ @value ||= []
44
+ end
45
+
46
+ # Deep copy
47
+ def initialize_copy(*)
48
+ super
49
+ @value = case @value
50
+ when Array then @value.map(&:dup)
51
+ else @value.dup
52
+ end
53
+ @accepted_tags = @accepted_tags.dup
54
+ end
55
+
56
+ # @return [Array]
57
+ def void_value
58
+ []
59
+ end
60
+
61
+ # Get element by index or name
62
+ # @param [Integer, String, Symbol] idx_or_name
63
+ # @return [Object, nil]
64
+ def [](idx_or_name)
65
+ return unless @value.is_a?(Array)
66
+
67
+ case idx_or_name
68
+ when ::Integer
69
+ @value[idx_or_name.to_i]
70
+ when String, Symbol
71
+ @value.find { |elt| elt.name == idx_or_name }
72
+ end
73
+ end
74
+
75
+ # Parse constructed content from DER
76
+ # @param [String] der
77
+ # @param [Boolean] ber
78
+ # @return [void]
79
+ def der_to_value(der, ber: false) # rubocop:disable Lint/UnusedMethodArgument
80
+ if @value.is_a?(Array) && !@value.empty?
81
+ nb_bytes = 0
82
+ @value.each do |element|
83
+ nb_bytes += element.parse!(der[nb_bytes..])
84
+ end
85
+ else
86
+ @value = der
87
+ der.length
88
+ end
89
+ end
90
+
91
+ private
92
+
93
+ def extract_tag_binding(options)
94
+ tag = options.delete(:tag)
95
+ @any_private = !options.delete(:any_private).nil?
96
+
97
+ @accepted_tags = case tag
98
+ when Array then tag
99
+ when ::Integer then [tag]
100
+ when :private
101
+ @any_private = true
102
+ []
103
+ else
104
+ []
105
+ end
106
+
107
+ if @any_private
108
+ options[:class] ||= :private
109
+ options[:implicit] = @accepted_tags.first if @accepted_tags.any?
110
+ elsif @accepted_tags.any?
111
+ options[:class] ||= :context
112
+ options[:implicit] = @accepted_tags.first
113
+ end
114
+ end
115
+
116
+ def check_id(der)
117
+ return check_any_private_id(der) if @any_private && @accepted_tags.empty?
118
+ return check_multi_tag_id(der) if @accepted_tags.length > 1
119
+
120
+ super
121
+ end
122
+
123
+ def check_any_private_id(der)
124
+ return no_match(der) if der.nil? || der.empty?
125
+
126
+ first_octet = der.unpack1('C').to_i
127
+ asn1_class_bits = first_octet & CLASS_MASK
128
+ if asn1_class_bits == CLASSES[:private]
129
+ _, _, tag_id, = Types.decode_identifier_octets(der)
130
+ @id_value = tag_id
131
+ @matched_tag = tag_id
132
+ true
133
+ else
134
+ no_match(der)
135
+ end
136
+ end
137
+
138
+ def check_multi_tag_id(der)
139
+ original_id = @id_value
140
+ @accepted_tags.each do |tag|
141
+ @id_value = tag
142
+ expected_id = encode_identifier_octets
143
+ real_id = der[0, expected_id.size]
144
+ if real_id == expected_id
145
+ @matched_tag = tag
146
+ return true
147
+ end
148
+ end
149
+ @id_value = original_id
150
+ no_match(der)
151
+ end
152
+
153
+ def no_match(der)
154
+ if optional?
155
+ @no_value = true
156
+ @value = void_value
157
+ elsif !@default.nil?
158
+ @value = @default
159
+ else
160
+ raise_id_error(der)
161
+ end
162
+ false
163
+ end
164
+
165
+ def value_to_der
166
+ case @value
167
+ when Array then @value.map(&:to_der).join
168
+ else @value.to_s
169
+ end
170
+ end
171
+
172
+ def trace_data
173
+ ''
174
+ end
175
+
176
+ def explicit_type
177
+ opts = { name: name, value: @value }
178
+ if @accepted_tags.length == 1
179
+ opts[:tag] = @accepted_tags.first
180
+ elsif @accepted_tags.length > 1
181
+ opts[:tag] = @accepted_tags
182
+ end
183
+ opts[:any_private] = true if @any_private
184
+ opts[:class] = @asn1_class unless @asn1_class == :universal
185
+ self.class.new(opts)
186
+ end
187
+ end
188
+ end
189
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RASN1
4
+ module Types
5
+ # ASN.1 UniversalString
6
+ # @since 0.13.0
7
+ # @author zeroSteiner
8
+ class UniversalString < OctetString
9
+ # UniversalString id value
10
+ ID = 28
11
+ # UniversalString encoding
12
+ # @since 0.15.0
13
+ ENCODING = Encoding::UTF_32BE
14
+
15
+ # Get ASN.1 type
16
+ # @return [String]
17
+ def self.type
18
+ 'UniversalString'
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'date'
4
+
5
+ module RASN1
6
+ module Types
7
+ # ASN.1 UTCTime
8
+ #
9
+ # +{#value} of a +UtcTime+ should be a ruby Time.
10
+ #
11
+ # ===Notes
12
+ # When encoding, resulting string is always a UTC time, appended with +Z+.
13
+ # Seconds are always generated.
14
+ #
15
+ # On parsing, UTC times (ending with +Z+) and local times (ending with
16
+ # +sHHMM+, where +s+ is +++ or +-+, and +HHMM+ is the time differential
17
+ # betwen UTC and local time) are both supported. Seconds may be present or
18
+ # not.
19
+ # @author Sylvain Daubert
20
+ class UtcTime < Primitive
21
+ # UtcTime id value
22
+ ID = 23
23
+
24
+ # Get ASN.1 type
25
+ # @return [String]
26
+ def self.type
27
+ 'UTCTime'
28
+ end
29
+
30
+ # Make time value from +der+ string
31
+ # @param [String] der
32
+ # @param [::Boolean] ber
33
+ # @return [void]
34
+ # @raise [ASN1Error] unrecognized time format
35
+ def der_to_value(der, ber: false) # rubocop:disable Lint/UnusedMethodArgument
36
+ format = case der.size
37
+ when 11, 15
38
+ '%Y%m%d%H%M%z'
39
+ when 13, 17
40
+ '%Y%m%d%H%M%S%z'
41
+ else
42
+ prefix = @name.nil? ? type : "tag #{@name}"
43
+ raise ASN1Error, "#{prefix}: unrecognized format: #{der}"
44
+ end
45
+ century = (Time.now.year / 100).to_s
46
+ begin
47
+ @value = Strptime.new(format).exec(century + der)
48
+ rescue ArgumentError
49
+ prefix = @name.nil? ? type : "tag #{@name}"
50
+ raise ASN1Error, "#{prefix}: unrecognized format: #{der}"
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ def value_to_der
57
+ @value.getutc.strftime('%y%m%d%H%M%SZ')
58
+ end
59
+
60
+ def trace_data
61
+ return super if explicit?
62
+
63
+ +' ' << raw_data
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RASN1
4
+ module Types
5
+ # ASN.1 UTF8 String
6
+ # @author Sylvain Daubert
7
+ class Utf8String < OctetString
8
+ # Utf8String id value
9
+ ID = 12
10
+ # Utf8String encoding
11
+ # @since 0.15.0
12
+ ENCODING = Encoding::UTF_8
13
+
14
+ # Get ASN.1 type
15
+ # @return [String]
16
+ def self.type
17
+ 'UTF8String'
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RASN1
4
+ module Types
5
+ # Create VisibleString as a Constrained subclass of IA5String
6
+ Types.define_type(:VisibleString, from: IA5String) do |value|
7
+ value.nil? || value.chars.all? { |c| c.ord.between?(32, 126) }
8
+ end
9
+
10
+ # ASN.1 Visible String
11
+ #
12
+ # VisibleString may only contain ASCII characters from range 32 to 126.
13
+ # @author LemonTree55
14
+ # @since 0.3.0
15
+ # @since 0.15.0 VisibleString is defined as a constrained {IA5String}. It will now raise if value contains a non-visible character.
16
+ class VisibleString
17
+ # VisibleString id value
18
+ # @since 0.3.0
19
+ ID = 26
20
+
21
+ # Get ASN.1 type
22
+ # @return [String]
23
+ # @author Sylvain Daubert
24
+ # @since 0.3.0
25
+ def self.type
26
+ 'VisibleString'
27
+ end
28
+ end
29
+ end
30
+ end