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.
- checksums.yaml +7 -0
- data/LICENSE +22 -0
- data/README.md +4 -0
- data/lib/rasn1/errors.rb +55 -0
- data/lib/rasn1/helpers/colorize.rb +76 -0
- data/lib/rasn1/model.rb +831 -0
- data/lib/rasn1/schema_parser.rb +470 -0
- data/lib/rasn1/tracer.rb +200 -0
- data/lib/rasn1/types/any.rb +98 -0
- data/lib/rasn1/types/base.rb +675 -0
- data/lib/rasn1/types/bit_string.rb +100 -0
- data/lib/rasn1/types/bmp_string.rb +22 -0
- data/lib/rasn1/types/boolean.rb +57 -0
- data/lib/rasn1/types/choice.rb +158 -0
- data/lib/rasn1/types/constrained.rb +51 -0
- data/lib/rasn1/types/constructed.rb +51 -0
- data/lib/rasn1/types/enumerated.rb +44 -0
- data/lib/rasn1/types/generalized_time.rb +156 -0
- data/lib/rasn1/types/ia5_string.rb +21 -0
- data/lib/rasn1/types/integer.rb +166 -0
- data/lib/rasn1/types/null.rb +43 -0
- data/lib/rasn1/types/numeric_string.rb +41 -0
- data/lib/rasn1/types/object_id.rb +64 -0
- data/lib/rasn1/types/octet_string.rb +52 -0
- data/lib/rasn1/types/primitive.rb +13 -0
- data/lib/rasn1/types/printable_string.rb +42 -0
- data/lib/rasn1/types/sequence.rb +105 -0
- data/lib/rasn1/types/sequence_of.rb +199 -0
- data/lib/rasn1/types/set.rb +28 -0
- data/lib/rasn1/types/set_of.rb +18 -0
- data/lib/rasn1/types/tag.rb +189 -0
- data/lib/rasn1/types/universal_string.rb +22 -0
- data/lib/rasn1/types/utc_time.rb +67 -0
- data/lib/rasn1/types/utf8_string.rb +21 -0
- data/lib/rasn1/types/visible_string.rb +30 -0
- data/lib/rasn1/types.rb +149 -0
- data/lib/rasn1/value_notation.rb +256 -0
- data/lib/rasn1/version.rb +6 -0
- data/lib/rasn1/wrapper.rb +279 -0
- data/lib/rasn1.rb +51 -0
- metadata +123 -0
data/lib/rasn1/types.rb
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'types/constrained'
|
|
4
|
+
|
|
5
|
+
module RASN1
|
|
6
|
+
# This modules is a namesapce for all ASN.1 type classes.
|
|
7
|
+
# @author Sylvain Daubert
|
|
8
|
+
module Types
|
|
9
|
+
@primitives = []
|
|
10
|
+
@constructed = []
|
|
11
|
+
|
|
12
|
+
# Give all primitive types
|
|
13
|
+
# @return [Array<Types::Primitive>]
|
|
14
|
+
def self.primitives
|
|
15
|
+
return @primitives unless @primitives.empty?
|
|
16
|
+
|
|
17
|
+
@primitives = self.constants.map { |c| Types.const_get(c) }
|
|
18
|
+
.select { |klass| klass < Primitive }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Give all constructed types
|
|
22
|
+
# @return [Array<Types::Constructed>]
|
|
23
|
+
def self.constructed
|
|
24
|
+
return @constructed unless @constructed.empty?
|
|
25
|
+
|
|
26
|
+
@constructed = self.constants.map { |c| Types.const_get(c) }
|
|
27
|
+
.select { |klass| klass < Constructed }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# @private
|
|
31
|
+
# Decode a DER string to extract identifier octets.
|
|
32
|
+
# @param [String] der
|
|
33
|
+
# @return [Array] Return ASN.1 class as Symbol, contructed/primitive as Symbol,
|
|
34
|
+
# ID and size of identifier octets
|
|
35
|
+
def self.decode_identifier_octets(der)
|
|
36
|
+
first_octet = der.unpack1('C').to_i
|
|
37
|
+
asn1_class = Types::Base::CLASSES.key(first_octet & Types::Base::CLASS_MASK) || :universal
|
|
38
|
+
pc = first_octet.anybits?(Types::Constructed::ASN1_PC) ? :constructed : :primitive
|
|
39
|
+
id = first_octet & Types::Base::MULTI_OCTETS_ID
|
|
40
|
+
|
|
41
|
+
size = if id == Types::Base::MULTI_OCTETS_ID
|
|
42
|
+
id = 0
|
|
43
|
+
count = 1
|
|
44
|
+
der[1..].to_s.bytes.each do |octet|
|
|
45
|
+
count += 1
|
|
46
|
+
|
|
47
|
+
id = (id << 7) | (octet & 0x7f)
|
|
48
|
+
break if octet.nobits?(0x80)
|
|
49
|
+
end
|
|
50
|
+
count
|
|
51
|
+
else
|
|
52
|
+
1
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
[asn1_class, pc, id, size]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Give ASN.1 type from a DER string. If ID is unknown, return a {Types::Base}
|
|
59
|
+
# object.
|
|
60
|
+
# @param [String] der
|
|
61
|
+
# @return [Types::Base]
|
|
62
|
+
# @raise [ASN1Error] +tag+ is out of range
|
|
63
|
+
def self.id2type(der)
|
|
64
|
+
# Define a cache for well-known ASN.1 types
|
|
65
|
+
self.generate_id2type_cache unless defined? @id2types
|
|
66
|
+
|
|
67
|
+
asn1class, pc, id, = self.decode_identifier_octets(der)
|
|
68
|
+
# cache_id: check versus class and 5 LSB bits
|
|
69
|
+
cache_id = der.unpack1('C') & 0xdf
|
|
70
|
+
klass = cache_id < Types::Base::MULTI_OCTETS_ID ? @id2types[id] : Types::Base
|
|
71
|
+
is_constructed = (pc == :constructed)
|
|
72
|
+
options = { class: asn1class, constructed: is_constructed }
|
|
73
|
+
options[:tag_value] = id if klass == Types::Base
|
|
74
|
+
klass.new(options)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# @private Generate cache for {.id2type}
|
|
78
|
+
def self.generate_id2type_cache
|
|
79
|
+
constructed = self.constructed - [Types::SequenceOf, Types::SetOf]
|
|
80
|
+
primitives = self.primitives - [Types::Enumerated]
|
|
81
|
+
ary = (primitives + constructed).select { |type| type.const_defined?(:ID) }
|
|
82
|
+
.map { |type| [type.const_get(:ID), type] }
|
|
83
|
+
@id2types = ary.to_h
|
|
84
|
+
@id2types.default = Types::Base
|
|
85
|
+
@id2types.freeze
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Define a new ASN.1 type from a base one.
|
|
89
|
+
# This new type may have a constraint defines on it.
|
|
90
|
+
# @param [Symbol,String] name new type name. Must start with a capital letter.
|
|
91
|
+
# @param [Types::Base] from class from which inherits
|
|
92
|
+
# @param [Module] in_module module in which creates new type (default to {RASN1::Types})
|
|
93
|
+
# @return [Class] newly created class
|
|
94
|
+
# @yieldparam [Object] value value to set to type, or infered at parsing
|
|
95
|
+
# @yieldreturn [Boolean]
|
|
96
|
+
# @since 0.11.0
|
|
97
|
+
# @since 0.12.0 in_module parameter
|
|
98
|
+
# @example
|
|
99
|
+
# # Define a new UInt32 type
|
|
100
|
+
# # UInt32 ::= INTEGER (0 .. 4294967295)
|
|
101
|
+
# RASN1::Types.define_type('UInt32', from: RASN1::Types::Integer) do |value|
|
|
102
|
+
# (value >= 0) && (value < 2**32)
|
|
103
|
+
# end
|
|
104
|
+
def self.define_type(name, from:, in_module: self, &block)
|
|
105
|
+
constraint = block&.to_proc
|
|
106
|
+
|
|
107
|
+
new_klass = Class.new(from)
|
|
108
|
+
new_klass.include(Constrained)
|
|
109
|
+
new_klass.extend(Constrained::ClassMethods)
|
|
110
|
+
new_klass.constraint = constraint
|
|
111
|
+
|
|
112
|
+
in_module.const_set(name, new_klass)
|
|
113
|
+
accel_name = name.to_s.gsub(/([a-z])([A-Z])/, '\1_\2').downcase
|
|
114
|
+
Model.define_type_accel(accel_name, new_klass)
|
|
115
|
+
|
|
116
|
+
# Empty type caches
|
|
117
|
+
@primitives = []
|
|
118
|
+
@constructed = []
|
|
119
|
+
|
|
120
|
+
new_klass
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
require_relative 'types/base'
|
|
126
|
+
require_relative 'types/primitive'
|
|
127
|
+
require_relative 'types/boolean'
|
|
128
|
+
require_relative 'types/integer'
|
|
129
|
+
require_relative 'types/bit_string'
|
|
130
|
+
require_relative 'types/octet_string'
|
|
131
|
+
require_relative 'types/null'
|
|
132
|
+
require_relative 'types/object_id'
|
|
133
|
+
require_relative 'types/enumerated'
|
|
134
|
+
require_relative 'types/bmp_string'
|
|
135
|
+
require_relative 'types/universal_string'
|
|
136
|
+
require_relative 'types/utf8_string'
|
|
137
|
+
require_relative 'types/numeric_string'
|
|
138
|
+
require_relative 'types/printable_string'
|
|
139
|
+
require_relative 'types/ia5_string'
|
|
140
|
+
require_relative 'types/constructed'
|
|
141
|
+
require_relative 'types/sequence'
|
|
142
|
+
require_relative 'types/sequence_of'
|
|
143
|
+
require_relative 'types/set'
|
|
144
|
+
require_relative 'types/set_of'
|
|
145
|
+
require_relative 'types/choice'
|
|
146
|
+
require_relative 'types/any'
|
|
147
|
+
require_relative 'types/tag'
|
|
148
|
+
require_relative 'types/utc_time'
|
|
149
|
+
require_relative 'types/generalized_time'
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RASN1
|
|
4
|
+
# Support for ASN.1 value notation (text representation of ASN.1 data instances).
|
|
5
|
+
#
|
|
6
|
+
# ASN.1 value notation allows defining values for ASN.1 types in a human-readable
|
|
7
|
+
# text format. For example:
|
|
8
|
+
# myPerson PersonnelRecord ::= {
|
|
9
|
+
# name "John Doe",
|
|
10
|
+
# title "Engineer",
|
|
11
|
+
# age 30,
|
|
12
|
+
# employed TRUE
|
|
13
|
+
# }
|
|
14
|
+
#
|
|
15
|
+
# This module provides methods to parse such text into {Model} instances and
|
|
16
|
+
# to generate such text from {Model} instances.
|
|
17
|
+
#
|
|
18
|
+
# @example Parse a value notation string
|
|
19
|
+
# models = RASN1::SchemaParser.parse_file('schema.asn')
|
|
20
|
+
# record = RASN1::ValueNotation.parse(
|
|
21
|
+
# File.read('instance.asn'),
|
|
22
|
+
# models: models
|
|
23
|
+
# )
|
|
24
|
+
# record[:name].value # => "John Doe"
|
|
25
|
+
#
|
|
26
|
+
# @example Generate value notation from a model
|
|
27
|
+
# record = PersonnelRecord.new(name: 'John Doe', title: 'Engineer', age: 30, employed: true)
|
|
28
|
+
# text = RASN1::ValueNotation.emit(record, name: 'myPerson', type_name: 'PersonnelRecord')
|
|
29
|
+
#
|
|
30
|
+
# @author rickmark
|
|
31
|
+
# @since 0.17.0
|
|
32
|
+
module ValueNotation
|
|
33
|
+
# Error raised when parsing or emitting value notation fails
|
|
34
|
+
class Error < RASN1::Error; end
|
|
35
|
+
|
|
36
|
+
class << self
|
|
37
|
+
# Parse an ASN.1 value notation string and populate a model instance.
|
|
38
|
+
# @param [String] text the value notation text
|
|
39
|
+
# @param [Hash{String => Class}] models hash mapping type names to Model subclasses
|
|
40
|
+
# @return [Model] populated model instance
|
|
41
|
+
# @raise [Error] if the text cannot be parsed
|
|
42
|
+
def parse(text, models:)
|
|
43
|
+
text = text.strip
|
|
44
|
+
match = text.match(/\A(\w+)\s+(\w+)\s*::=\s*(.*)\z/m)
|
|
45
|
+
raise Error, 'Invalid value notation format' unless match
|
|
46
|
+
|
|
47
|
+
value_name = match[1]
|
|
48
|
+
type_name = match[2]
|
|
49
|
+
body = match[3].strip
|
|
50
|
+
|
|
51
|
+
model_class = models[type_name]
|
|
52
|
+
raise Error, "Unknown type: #{type_name}" unless model_class
|
|
53
|
+
|
|
54
|
+
values = parse_constructed_value(body)
|
|
55
|
+
instance = model_class.new(values)
|
|
56
|
+
instance.define_singleton_method(:value_name) { value_name }
|
|
57
|
+
instance.define_singleton_method(:type_name) { type_name }
|
|
58
|
+
instance
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Parse an ASN.1 value notation file.
|
|
62
|
+
# @param [String] filename path to the value notation file
|
|
63
|
+
# @param [Hash{String => Class}] models hash mapping type names to Model subclasses
|
|
64
|
+
# @return [Model] populated model instance
|
|
65
|
+
# @raise [Error] if the file cannot be parsed
|
|
66
|
+
def parse_file(filename, models:)
|
|
67
|
+
parse(File.read(filename), models: models)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Emit ASN.1 value notation text from a model instance.
|
|
71
|
+
# @param [Model] model the model instance to emit
|
|
72
|
+
# @param [String] name the value name (e.g. 'myPerson')
|
|
73
|
+
# @param [String] type_name the type name (e.g. 'PersonnelRecord')
|
|
74
|
+
# @param [Integer] indent indentation level (number of spaces per level)
|
|
75
|
+
# @return [String] value notation text
|
|
76
|
+
def emit(model, name: nil, type_name: nil)
|
|
77
|
+
name ||= model.respond_to?(:value_name) ? model.value_name : 'value'
|
|
78
|
+
type_name ||= model.respond_to?(:type_name) ? model.type_name : model.class.type
|
|
79
|
+
body = emit_constructed(model, indent_level: 1)
|
|
80
|
+
"#{name} #{type_name} ::= {\n#{body}\n}\n"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
# Parse a constructed value body: { field1 value1, field2 value2, ... }
|
|
86
|
+
# @param [String] text
|
|
87
|
+
# @return [Hash{Symbol => Object}]
|
|
88
|
+
def parse_constructed_value(text)
|
|
89
|
+
text = text.strip
|
|
90
|
+
raise Error, "Expected '{' at start of constructed value" unless text.start_with?('{')
|
|
91
|
+
raise Error, "Expected '}' at end of constructed value" unless text.end_with?('}')
|
|
92
|
+
|
|
93
|
+
inner = text[1..-2].strip
|
|
94
|
+
result = {}
|
|
95
|
+
remaining = inner
|
|
96
|
+
|
|
97
|
+
until remaining.nil? || remaining.strip.empty?
|
|
98
|
+
remaining = remaining.strip
|
|
99
|
+
field_name, value, remaining = parse_field(remaining)
|
|
100
|
+
result[field_name.to_sym] = value
|
|
101
|
+
# consume optional comma
|
|
102
|
+
remaining = remaining&.strip
|
|
103
|
+
remaining = remaining.sub(/\A,\s*/, '') if remaining&.start_with?(',')
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
result
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Parse a single field: name value
|
|
110
|
+
# @param [String] text
|
|
111
|
+
# @return [Array(String, Object, String)] field name, parsed value, remaining text
|
|
112
|
+
def parse_field(text)
|
|
113
|
+
# Match field name
|
|
114
|
+
match = text.match(/\A(\w+)\s+/)
|
|
115
|
+
raise Error, "Expected field name in: #{text[0..40]}" unless match
|
|
116
|
+
|
|
117
|
+
field_name = match[1]
|
|
118
|
+
rest = match.post_match
|
|
119
|
+
|
|
120
|
+
value, rest = parse_value(rest)
|
|
121
|
+
[field_name, value, rest]
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Parse a single value (string, integer, boolean, null, or nested constructed)
|
|
125
|
+
# @param [String] text
|
|
126
|
+
# @return [Array(Object, String)] parsed value and remaining text
|
|
127
|
+
def parse_value(text)
|
|
128
|
+
text = text.strip
|
|
129
|
+
case text
|
|
130
|
+
when /\A"/ then parse_string_value(text)
|
|
131
|
+
when /\ATRUE\b/i then [true, text.sub(/\ATRUE/i, '')]
|
|
132
|
+
when /\AFALSE\b/i then [false, text.sub(/\AFALSE/i, '')]
|
|
133
|
+
when /\ANULL\b/i then [nil, text.sub(/\ANULL/i, '')]
|
|
134
|
+
when /\A\{/ then parse_nested_constructed(text)
|
|
135
|
+
when /\A-?\d/ then parse_integer_value(text)
|
|
136
|
+
else
|
|
137
|
+
raise Error, "Unexpected value at: #{text[0..40]}"
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Parse a quoted string value
|
|
142
|
+
# @param [String] text
|
|
143
|
+
# @return [Array(String, String)]
|
|
144
|
+
def parse_string_value(text)
|
|
145
|
+
# Handle escaped quotes within the string
|
|
146
|
+
pos = 1
|
|
147
|
+
result = +''
|
|
148
|
+
while pos < text.length
|
|
149
|
+
ch = text[pos]
|
|
150
|
+
if ch == '"'
|
|
151
|
+
# Check for escaped quote (doubled)
|
|
152
|
+
return [result, text[(pos + 1)..]] unless pos + 1 < text.length && text[pos + 1] == '"'
|
|
153
|
+
|
|
154
|
+
result << '"'
|
|
155
|
+
pos += 2
|
|
156
|
+
|
|
157
|
+
else
|
|
158
|
+
result << ch
|
|
159
|
+
pos += 1
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
raise Error, 'Unterminated string value'
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Parse an integer value
|
|
166
|
+
# @param [String] text
|
|
167
|
+
# @return [Array(Integer, String)]
|
|
168
|
+
def parse_integer_value(text)
|
|
169
|
+
match = text.match(/\A(-?\d+)/)
|
|
170
|
+
raise Error, "Expected integer at: #{text[0..20]}" unless match
|
|
171
|
+
|
|
172
|
+
[match[1].to_i, match.post_match]
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Parse a nested constructed value { ... }
|
|
176
|
+
# @param [String] text
|
|
177
|
+
# @return [Array(Hash, String)]
|
|
178
|
+
def parse_nested_constructed(text)
|
|
179
|
+
# Find matching closing brace
|
|
180
|
+
depth = 0
|
|
181
|
+
pos = 0
|
|
182
|
+
text.each_char.with_index do |ch, idx|
|
|
183
|
+
depth += 1 if ch == '{'
|
|
184
|
+
depth -= 1 if ch == '}'
|
|
185
|
+
if depth.zero?
|
|
186
|
+
pos = idx
|
|
187
|
+
break
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
raise Error, 'Unmatched braces in constructed value' if depth != 0
|
|
191
|
+
|
|
192
|
+
inner_text = text[0..pos]
|
|
193
|
+
rest = text[(pos + 1)..]
|
|
194
|
+
values = parse_constructed_value(inner_text)
|
|
195
|
+
[values, rest]
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Emit the fields of a constructed model value
|
|
199
|
+
# @param [Model] model
|
|
200
|
+
# @param [Integer] indent_level
|
|
201
|
+
# @return [String]
|
|
202
|
+
def emit_constructed(model, indent_level: 1)
|
|
203
|
+
indent = ' ' * indent_level
|
|
204
|
+
fields = collect_fields(model)
|
|
205
|
+
|
|
206
|
+
lines = fields.map do |field_name, value|
|
|
207
|
+
formatted = format_value(value, indent_level: indent_level)
|
|
208
|
+
"#{indent}#{field_name} #{formatted}"
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
lines.join(",\n")
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Collect the named fields from a model's root sequence
|
|
215
|
+
# @param [Model] model
|
|
216
|
+
# @return [Array<Array(Symbol, Object)>]
|
|
217
|
+
def collect_fields(model)
|
|
218
|
+
root = model.root
|
|
219
|
+
return [] unless root.is_a?(Types::Sequence) || root.is_a?(Types::Set)
|
|
220
|
+
return [] unless root.value.is_a?(Array)
|
|
221
|
+
|
|
222
|
+
fields = []
|
|
223
|
+
root.value.each do |element|
|
|
224
|
+
case element
|
|
225
|
+
when Model
|
|
226
|
+
fields << [element.name, element]
|
|
227
|
+
when Types::Base
|
|
228
|
+
next if element.optional? && !element.value?
|
|
229
|
+
|
|
230
|
+
fields << [element.name, element.value]
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
fields
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# Format a value for value notation output
|
|
237
|
+
# @param [Object] value
|
|
238
|
+
# @param [Integer] indent_level
|
|
239
|
+
# @return [String]
|
|
240
|
+
def format_value(value, indent_level: 0)
|
|
241
|
+
case value
|
|
242
|
+
when Model
|
|
243
|
+
body = emit_constructed(value, indent_level: indent_level + 1)
|
|
244
|
+
"{\n#{body}\n#{' ' * indent_level}}"
|
|
245
|
+
when true then 'TRUE'
|
|
246
|
+
when false then 'FALSE'
|
|
247
|
+
when ::Integer then value.to_s
|
|
248
|
+
when String then "\"#{value.gsub('"', '""')}\""
|
|
249
|
+
when nil then 'NULL'
|
|
250
|
+
else
|
|
251
|
+
value.to_s
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
end
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'delegate'
|
|
4
|
+
|
|
5
|
+
module RASN1
|
|
6
|
+
# This class is used to wrap a {Types::Base} or {Model} instance to force its options.
|
|
7
|
+
#
|
|
8
|
+
# == Usage
|
|
9
|
+
# This class may be used to wrap another RASN1 object by 4 ways:
|
|
10
|
+
# * wrap an object to modify its options,
|
|
11
|
+
# * implicitly wrap an object (i.e. change its tag),
|
|
12
|
+
# * explicitly wrap an object (i.e wrap the object in another explicit ASN.1 tag),
|
|
13
|
+
# * wrap a choice model to reuse it in its own definition.
|
|
14
|
+
#
|
|
15
|
+
# @example Wrapping object
|
|
16
|
+
# # object to wrap
|
|
17
|
+
# int = RASN1::Types::Integer.new(implicit: 1) # its tag is 0x81
|
|
18
|
+
# # simple wrapper, change an option
|
|
19
|
+
# wrapper = RASN1::Wrapper.new(int, default: 1)
|
|
20
|
+
# # implicit wrapper
|
|
21
|
+
# wrapper = RASN1::Wrapper.new(int, implicit: 3) # wrapped int tag is now 0x83
|
|
22
|
+
# # explicit wrapper
|
|
23
|
+
# wrapper = RASN1::Wrapper.new(int, explicit: 4) # int tag is always 0x81, but it is wrapped in a 0x84 tag
|
|
24
|
+
# @example Wrapping a choice to make it recursive
|
|
25
|
+
# class RecursiveChoice < RASN1::Model
|
|
26
|
+
# choice :choice,
|
|
27
|
+
# content: [
|
|
28
|
+
# integer(:value, implicit: 1),
|
|
29
|
+
# wrapper(model(:recursive, RecursiveChoice), implicit:2)
|
|
30
|
+
# ]
|
|
31
|
+
# end
|
|
32
|
+
# @since 0.12.0
|
|
33
|
+
# @since 0.15.0 Wrappers are lazy. They allocate their inner element only when needed (i.e. when calling {#to_der}, {#parse!} or {#element})
|
|
34
|
+
# @author Sylvain Daubert
|
|
35
|
+
# @author LemonTree55
|
|
36
|
+
class Wrapper < SimpleDelegator
|
|
37
|
+
# @private Private class used to build/parse explicit wrappers
|
|
38
|
+
class ExplicitWrapper < Types::Base
|
|
39
|
+
ID = 0 # not used
|
|
40
|
+
ASN1_PC = 0 # not constructed
|
|
41
|
+
|
|
42
|
+
def self.type
|
|
43
|
+
''
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# @return [Boolean]
|
|
47
|
+
# @see Types::Base#can_build?
|
|
48
|
+
def can_build?
|
|
49
|
+
ok = super
|
|
50
|
+
return ok unless optional?
|
|
51
|
+
|
|
52
|
+
ok && @value.can_build?
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def value_to_der
|
|
58
|
+
@value.is_a?(String) ? @value : @value.to_der
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def inspect_value
|
|
62
|
+
''
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# @param [Types::Base,Model] element element to wrap
|
|
67
|
+
# @param [Hash] options
|
|
68
|
+
def initialize(element, options={})
|
|
69
|
+
@lazy = true
|
|
70
|
+
opts = explicit_implicit(options)
|
|
71
|
+
|
|
72
|
+
if explicit?
|
|
73
|
+
generate_explicit_wrapper(opts)
|
|
74
|
+
@element_options_to_merge = generate_explicit_wrapper_options(opts)
|
|
75
|
+
@options = opts
|
|
76
|
+
else
|
|
77
|
+
opts[:value] = element.value if element.respond_to?(:value)
|
|
78
|
+
@element_options_to_merge = opts
|
|
79
|
+
@options = {}
|
|
80
|
+
end
|
|
81
|
+
raise RASN1::Error, 'Cannot be implicit and explicit' if explicit? && implicit?
|
|
82
|
+
|
|
83
|
+
super(element)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Say if wrapper is an explicit one (i.e. add tag and length to its element)
|
|
87
|
+
# @return [Boolean]
|
|
88
|
+
def explicit?
|
|
89
|
+
!!@explicit
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Say if wrapper is an implicit one (i.e. change tag of its element)
|
|
93
|
+
# @return [Boolean]
|
|
94
|
+
def implicit?
|
|
95
|
+
!!@implicit
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Convert wrapper and its element to a DER string
|
|
99
|
+
# @return [String]
|
|
100
|
+
# @since 0.12.0
|
|
101
|
+
# @since 0.15.0 Allocate element (lazy wrapper)
|
|
102
|
+
def to_der
|
|
103
|
+
lazy_generation
|
|
104
|
+
if implicit?
|
|
105
|
+
el = generate_implicit_element
|
|
106
|
+
el.to_der
|
|
107
|
+
elsif explicit?
|
|
108
|
+
@explicit_wrapper.value = element
|
|
109
|
+
@explicit_wrapper.to_der
|
|
110
|
+
else
|
|
111
|
+
element.to_der
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Parse a DER string. This method updates object.
|
|
116
|
+
# @param [String] der DER string
|
|
117
|
+
# @param [Boolean] ber if +true+, accept BER encoding
|
|
118
|
+
# @return [Integer] total number of parsed bytes
|
|
119
|
+
# @raise [ASN1Error] error on parsing
|
|
120
|
+
# @since 0.12.0
|
|
121
|
+
# @since 0.15.0 Allocate element (lazy wrapper)
|
|
122
|
+
def parse!(der, ber: false)
|
|
123
|
+
lazy_generation
|
|
124
|
+
if implicit?
|
|
125
|
+
el = generate_implicit_element
|
|
126
|
+
parsed = el.parse!(der, ber: ber)
|
|
127
|
+
element.value = el.value
|
|
128
|
+
parsed
|
|
129
|
+
elsif explicit?
|
|
130
|
+
parsed = @explicit_wrapper.parse!(der, ber: ber)
|
|
131
|
+
element.parse!(@explicit_wrapper.value, ber: ber) if parsed.positive?
|
|
132
|
+
parsed
|
|
133
|
+
else
|
|
134
|
+
element.parse!(der, ber: ber)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# @private
|
|
139
|
+
# @see Types::Base#do_parse
|
|
140
|
+
def do_parse(der, ber: false)
|
|
141
|
+
if implicit?
|
|
142
|
+
generate_implicit_element(Types::Base.new(constructed: element.constructed?)).do_parse(der, ber: ber)
|
|
143
|
+
elsif explicit?
|
|
144
|
+
@explicit_wrapper.do_parse(der, ber: ber)
|
|
145
|
+
else
|
|
146
|
+
element.do_parse(der, ber: ber)
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# @return [Boolean]
|
|
151
|
+
# @see Types::Base#value?
|
|
152
|
+
def value?
|
|
153
|
+
if explicit?
|
|
154
|
+
@explicit_wrapper.value?
|
|
155
|
+
elsif __getobj__.is_a?(Class)
|
|
156
|
+
false
|
|
157
|
+
else
|
|
158
|
+
__getobj__.value?
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Return Wrapped element
|
|
163
|
+
# @return [Types::Base,Model]
|
|
164
|
+
# @since 0.12.0
|
|
165
|
+
# @since 0.15.0 Allocate element (lazy wrapper)
|
|
166
|
+
def element
|
|
167
|
+
lazy_generation
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# @return [::Integer]
|
|
171
|
+
# @see Types::Base#id
|
|
172
|
+
def id
|
|
173
|
+
if implicit?
|
|
174
|
+
@implicit
|
|
175
|
+
elsif explicit?
|
|
176
|
+
@explicit
|
|
177
|
+
else
|
|
178
|
+
lazy_generation(register: false).id
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# @return [Symbol]
|
|
183
|
+
# @see Types::Base#asn1_class
|
|
184
|
+
def asn1_class
|
|
185
|
+
return lazy_generation(register: false).asn1_class unless @options.key?(:class)
|
|
186
|
+
|
|
187
|
+
@options[:class]
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# @return [Boolean]
|
|
191
|
+
# @see Types::Base#constructed
|
|
192
|
+
def constructed?
|
|
193
|
+
return lazy_generation(register: false).constructed? unless @options.key?(:constructed)
|
|
194
|
+
|
|
195
|
+
@options[:constructed]
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# @return [Boolean]
|
|
199
|
+
# @see Types::Base#primitive
|
|
200
|
+
def primitive?
|
|
201
|
+
!constructed?
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# @param [::Integer] level
|
|
205
|
+
# @return [String]
|
|
206
|
+
def inspect(level=0)
|
|
207
|
+
return super() unless explicit?
|
|
208
|
+
|
|
209
|
+
@explicit_wrapper.inspect(level) << ' ' << super()
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
private
|
|
213
|
+
|
|
214
|
+
def explicit_implicit(options)
|
|
215
|
+
opts = options.dup
|
|
216
|
+
@explicit = tag_id_to_integer(opts.delete(:explicit))
|
|
217
|
+
@implicit = tag_id_to_integer(opts.delete(:implicit))
|
|
218
|
+
opts
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Convert a tag id to an integer.
|
|
222
|
+
# If given a String or Symbol, interprets the characters as big-endian octets
|
|
223
|
+
# (like Apple 4-character codes / 4CCs). Integers and +nil+ are returned as-is.
|
|
224
|
+
# @param [::Integer, String, Symbol, nil] value tag value
|
|
225
|
+
# @return [::Integer, nil]
|
|
226
|
+
def tag_id_to_integer(value)
|
|
227
|
+
case value
|
|
228
|
+
when ::Integer, NilClass
|
|
229
|
+
value
|
|
230
|
+
when ::String, ::Symbol
|
|
231
|
+
value.to_s.bytes.reduce(0) { |acc, b| (acc << 8) | b }
|
|
232
|
+
else
|
|
233
|
+
value
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def generate_explicit_wrapper(options)
|
|
238
|
+
# ExplicitWrapper is a hand-made explicit tag, but we have to use its implicit option
|
|
239
|
+
# to force its tag value.
|
|
240
|
+
@explicit_wrapper = ExplicitWrapper.new(options.merge(implicit: @explicit))
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def generate_explicit_wrapper_options(options)
|
|
244
|
+
new_opts = {}
|
|
245
|
+
new_opts[:default] = options[:default] if options.key?(:default)
|
|
246
|
+
new_opts[:optional] = options[:optional] if options.key?(:optional)
|
|
247
|
+
new_opts
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def generate_implicit_element(from=nil)
|
|
251
|
+
el = (from || element).dup
|
|
252
|
+
el.options = if element.explicit?
|
|
253
|
+
el.options.merge(explicit: @implicit)
|
|
254
|
+
else
|
|
255
|
+
el.options.merge(implicit: @implicit)
|
|
256
|
+
end
|
|
257
|
+
el
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def lazy_generation(register: true)
|
|
261
|
+
return __getobj__ unless @lazy
|
|
262
|
+
|
|
263
|
+
real_element = __getobj__
|
|
264
|
+
case real_element
|
|
265
|
+
when Types::Base, Model
|
|
266
|
+
real_element.options = real_element.options.merge(@element_options_to_merge)
|
|
267
|
+
@lazy = false
|
|
268
|
+
real_element
|
|
269
|
+
else
|
|
270
|
+
el = real_element.new(@element_options_to_merge)
|
|
271
|
+
if register
|
|
272
|
+
@lazy = false
|
|
273
|
+
__setobj__(el)
|
|
274
|
+
end
|
|
275
|
+
el
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
end
|