derparse 0.1.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.
data/README.md ADDED
@@ -0,0 +1,141 @@
1
+ Ever wanted to do terrible, terrible things with DER-encoded ASN.1 data
2
+ structures? Now you can!
3
+
4
+ While there are plenty of DER parsers out there, they tend to suffer from the
5
+ misguided assumption that the data they're parsing is actually valid. Wild
6
+ idea, it'll never catch on. In contrast, `derparse` makes the opposite
7
+ assumption -- that everything is awful. This means that it makes an attempt to
8
+ parse incomplete and, to a limited degree, corrupted DER-encoded data
9
+ structures.
10
+
11
+ Why would you *ever* want this, you might ask? It's a niche requirement,
12
+ undoubtedly, and if you're asking the question, that's a good indication that
13
+ `derparse` probably isn't what you're looking for -- `OpenSSL::ASN1` does a
14
+ good job of parsing well-formed DER data.
15
+
16
+
17
+ # Installation
18
+
19
+ It's a gem:
20
+
21
+ gem install derparse
22
+
23
+ There's also the wonders of [the Gemfile](http://bundler.io):
24
+
25
+ gem 'derparse'
26
+
27
+ If you're the sturdy type that likes to run from git:
28
+
29
+ rake install
30
+
31
+ Or, if you've eschewed the convenience of Rubygems entirely, then you
32
+ presumably know what to do already.
33
+
34
+
35
+ # Usage
36
+
37
+ Instantiate a new `DerParse` object, giving it the DER-encoded data you want
38
+ to parse:
39
+
40
+ ```ruby
41
+ p = DerParse.new("0\x81\x80\x02\x01\x2a\x04\x44\x65\x72\x50\x61\x72\x73\x65\x02\x01\x45\x6e\x69\x63\x65")
42
+ ```
43
+
44
+ {DerParse} has a number of instance methods to help you examine and work with
45
+ the parsed ASN.1 data. For example, you can get a string description of the
46
+ data and its structure:
47
+
48
+ ```ruby
49
+ p.to_s # => "long, multi-line string of something something"
50
+ ```
51
+
52
+ which means, of course, that you can print it to screen:
53
+
54
+ ```ruby
55
+ puts p
56
+ ```
57
+
58
+ If you want to process the data, you can use `#traverse` to walk
59
+ through every element of the DER, which will yield a {`DerParse::Node`}
60
+ for each ASN.1 object that is encountered, along with info about the
61
+ parser state.
62
+
63
+ ```ruby
64
+ p.traverse do |node, depth, offset|
65
+ puts "Parsing depth: #{depth} Offset: #{offset}"
66
+ puts "Header length: #{node.header_length} Data length: #{node.data_length}"
67
+ puts "Tag number: #{node.tag} Tag class: #{node.tag_class} Constructed: #{node.constructed?}"
68
+ puts "Value: #{node.value}"
69
+ puts "INCOMPLETE!" unless node.complete?
70
+ end
71
+ ```
72
+
73
+ There are a whole bunch of methods which {`DerParse::Node`} has to help you
74
+ query what sort of ASN.1 object you're dealing with. It also handles a string
75
+ in which there are multiple ASN.1 objects encoded sequentially.
76
+
77
+ It's is particularly important to note the {`DerParse::Node#complete?`} method.
78
+ Because `DerParse` tries to give you as much data as it can, even if the DER is
79
+ incomplete or corrupt, it needs to let you know when the data it's passing back
80
+ is known to not be "correct", in a strict sense. What counts as an
81
+ "incomplete" value, and what impact it has on the value of a given node, can
82
+ get complicated. For full details, see the documentation for
83
+ {DerParse#traverse}.
84
+
85
+ If you call `#traverse` without a block, it'll return an iterator, which allows
86
+ you to be more flexible with your iteration, like constructing complicated handlers
87
+ for parsed DER structures.
88
+
89
+ Finally, you can try {`DerParse#to_a`}. This attempts to turn a DER blob into
90
+ a collection of Ruby objects. It handles many common ASN.1 data types,
91
+ including integers, octet and bit strings, sequences and sets (turns them into
92
+ arrays), and so on. If it hits an ASN.1 type it can't handle, it'll give up
93
+ and raise {`DerParse::IncompatibleDataTypeError`}.
94
+
95
+
96
+ # Compatibility and Coverage
97
+
98
+ The parts of ASN.1 that `DerParse` best supports are those which I've needed
99
+ for my own purposes -- mainly sequences, integers, octet strings, and to a
100
+ lesser extent OIDs. In particular, it's important to note that `DerParse` is
101
+ for ***DER*** structures, so any BER-specific things like indefinite lengths
102
+ are not, and probably will not, be supported. Support for additional ASN.1
103
+ types are welcomed via a well-tested PR.
104
+
105
+
106
+ # Security
107
+
108
+ ASN.1 has a long and glorious history of being difficult to parse safely.
109
+ While parsing in a memory-safe language reduces the attack surface, I don't
110
+ recommend unconstrained parsing of arbitrary input using `DerParse`.
111
+ Reasonable care has been taken to not produce any absolute bone-headed bugs,
112
+ but this code has not been intensively vetted for potential security issues.
113
+
114
+ That being said, `DerParse` is intended to be secure, and everyone wants it to
115
+ be secure. If you *do* find anything in `DerParse` that is security sensitive,
116
+ please e-mail [`mpalmer@hezmatt.org`](mailto:mpalmer@hezmatt.org).
117
+
118
+
119
+ # Contributing
120
+
121
+ See [CONTRIBUTING.md](CONTRIBUTING.md).
122
+
123
+
124
+ # Licence
125
+
126
+ Unless otherwise stated, everything in this repo is covered by the following
127
+ copyright notice:
128
+
129
+ Copyright (C) 2020 Matt Palmer <matt@hezmatt.org>
130
+
131
+ This program is free software: you can redistribute it and/or modify it
132
+ under the terms of the GNU General Public License version 3, as
133
+ published by the Free Software Foundation.
134
+
135
+ This program is distributed in the hope that it will be useful,
136
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
137
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
138
+ GNU General Public License for more details.
139
+
140
+ You should have received a copy of the GNU General Public License
141
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
data/derparse.gemspec ADDED
@@ -0,0 +1,35 @@
1
+ begin
2
+ require 'git-version-bump'
3
+ rescue LoadError
4
+ nil
5
+ end
6
+
7
+ Gem::Specification.new do |s|
8
+ s.name = "derparse"
9
+
10
+ s.version = GVB.version rescue "0.0.0.1.NOGVB"
11
+ s.date = GVB.date rescue Time.now.strftime("%Y-%m-%d")
12
+
13
+ s.platform = Gem::Platform::RUBY
14
+
15
+ s.summary = "DER parsing classes and routines"
16
+
17
+ s.authors = ["Matt Palmer"]
18
+ s.email = ["theshed+derparse@hezmatt.org"]
19
+ s.homepage = "http://github.com/mpalmer/derparse"
20
+
21
+ s.files = `git ls-files -z`.split("\0").reject { |f| f =~ /^(G|spec|Rakefile)/ }
22
+
23
+ s.required_ruby_version = ">= 2.5.0"
24
+
25
+ s.add_development_dependency 'bundler'
26
+ s.add_development_dependency 'github-release'
27
+ s.add_development_dependency 'guard-rspec'
28
+ s.add_development_dependency 'rake', '~> 10.4', '>= 10.4.2'
29
+ # Needed for guard
30
+ s.add_development_dependency 'rb-inotify', '~> 0.9'
31
+ s.add_development_dependency 'redcarpet'
32
+ s.add_development_dependency 'rspec'
33
+ s.add_development_dependency 'simplecov'
34
+ s.add_development_dependency 'yard'
35
+ end
data/lib/derparse.rb ADDED
@@ -0,0 +1,53 @@
1
+ class DerParse
2
+ class Error < StandardError; end
3
+ class IncompatibleDatatypeError < StandardError; end
4
+
5
+ class Bug < Exception; end
6
+
7
+ def initialize(s)
8
+ if s.respond_to?(:to_str)
9
+ @s = s.to_str
10
+ else
11
+ raise ArgumentError,
12
+ "Must provide string to parse"
13
+ end
14
+ end
15
+
16
+ def traverse(&blk)
17
+ if blk
18
+ node(@s, 0, 0, &blk)
19
+ self
20
+ else
21
+ to_enum(:traverse)
22
+ end
23
+ end
24
+
25
+ def to_a
26
+ [].tap do |v|
27
+ r = @s
28
+
29
+ until r.empty?
30
+ n = DerParse::Node.factory(r)
31
+ v << n.value
32
+ r = n.rest
33
+ end
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ def node(der, depth, offset, &blk)
40
+ node = DerParse::Node.factory(der, depth: depth, offset: offset)
41
+ yield node
42
+
43
+ if node.constructed?
44
+ node(node.data, depth + 1, offset + node.header_length, &blk)
45
+ end
46
+
47
+ unless node.rest.empty?
48
+ node(node.rest, depth, offset + node.der_length, &blk)
49
+ end
50
+ end
51
+ end
52
+
53
+ require_relative "./derparse/node"
@@ -0,0 +1,142 @@
1
+ require "derparse"
2
+
3
+ class DerParse
4
+ class Node
5
+ def self.factory(der, *args)
6
+ klass_name = DerParse::Node.constants.find do |const|
7
+ DerParse::Node.const_get(const).handles?(der)
8
+ end
9
+
10
+ klass = klass_name.nil? ? DerParse::Node : const_get(klass_name)
11
+
12
+ klass.new(der, *args)
13
+ end
14
+
15
+ attr_reader :tag, :tag_class, :tag_length, :length_length, :data_length, :data, :depth, :offset, :rest
16
+
17
+ def initialize(der, depth: 0, offset: 0)
18
+ @depth, @offset = depth, offset
19
+ @tag = @tag_class = @constructed = @tag_length = @data_length = @length_length, @data = nil
20
+ @complete = true
21
+
22
+ begin
23
+ r = parse_type(der)
24
+ r = parse_length(r)
25
+ @rest = parse_data(r)
26
+ rescue IncompleteDer
27
+ @complete = false
28
+ end
29
+ end
30
+
31
+ def der_length
32
+ if data_length.nil? || header_length.nil?
33
+ nil
34
+ else
35
+ data_length + header_length
36
+ end
37
+ end
38
+
39
+ def header_length
40
+ if tag_length.nil? || length_length.nil?
41
+ nil
42
+ else
43
+ tag_length + length_length
44
+ end
45
+ end
46
+
47
+ def constructed?
48
+ @constructed
49
+ end
50
+
51
+ def primitive?
52
+ !constructed?
53
+ end
54
+
55
+ def complete?
56
+ @complete
57
+ end
58
+
59
+ def value
60
+ raise IncompatibleDatatypeError
61
+ end
62
+
63
+ private
64
+
65
+ class IncompleteDer < Error; end
66
+ private_constant :IncompleteDer
67
+
68
+ def parse_type(s)
69
+ if s.empty? || s.nil?
70
+ # This *technically* shouldn't be possible
71
+ #:nocov:
72
+ raise Bug, "parse_type(#{s.inspect})"
73
+ #:nocov:
74
+ end
75
+
76
+ t0, s = ss(s)
77
+
78
+ @tag_class = (t0 & 0xc0) >> 6
79
+ @constructed = ((t0 & 0x20) >> 5) == 1
80
+ @tag = t0 & 0x1f
81
+ tl = 1
82
+
83
+ =begin
84
+ # As-yet unneeded big-tag support
85
+ if tag == 0x1f
86
+ tag = 0
87
+ more = true
88
+
89
+ while more
90
+ c = ss(s)
91
+ tag = tag * 128 + c & 0x7f
92
+ more = (c & 0x80) == 0x80
93
+ tl += 1
94
+ end
95
+ end
96
+ =end
97
+
98
+ @tag_length = tl
99
+
100
+ s
101
+ end
102
+
103
+ def parse_length(s)
104
+ l0, s = ss(s)
105
+
106
+ if (l0 & 0x80) == 0
107
+ @data_length = l0
108
+ @length_length = 1
109
+ else
110
+ ll = l0 & 0x7f
111
+ @length_length = ll + 1
112
+ @data_length = ll.times.inject(0) { |a, _| c, s = ss(s); a * 256 + c }
113
+ end
114
+
115
+ s
116
+ end
117
+
118
+ def parse_data(s)
119
+ if s.length < @data_length
120
+ @data = s
121
+ raise IncompleteDer
122
+ else
123
+ @data = s[0, @data_length]
124
+ end
125
+
126
+ s[@data_length..]
127
+ end
128
+
129
+ def ss(s)
130
+ raise IncompleteDer if s.empty?
131
+
132
+ c, r = s.unpack("Ca*")
133
+ # Make sure that we always return a string as the second element
134
+ [c, r || ""]
135
+ end
136
+ end
137
+ end
138
+
139
+ require_relative "./node/integer"
140
+ require_relative "./node/octet_string"
141
+ require_relative "./node/null"
142
+ require_relative "./node/sequence"
@@ -0,0 +1,15 @@
1
+ class DerParse
2
+ class Node
3
+ class Integer < Node
4
+ def self.handles?(der)
5
+ der[0] == "\x02"
6
+ end
7
+
8
+ def value
9
+ sign = (@data[0].ord & 0x80) >> 7
10
+
11
+ @data.split(//).inject(0) { |a, c| sign *= 256; a * 256 + c.ord } - sign
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,13 @@
1
+ class DerParse
2
+ class Node
3
+ class Null < Node
4
+ def self.handles?(der)
5
+ der[0] == "\x05"
6
+ end
7
+
8
+ def value
9
+ nil
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ class DerParse
2
+ class Node
3
+ class OctetString < Node
4
+ def self.handles?(der)
5
+ der[0] == "\x04"
6
+ end
7
+
8
+ def value
9
+ @data
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,21 @@
1
+ class DerParse
2
+ class Node
3
+ class Sequence < Node
4
+ def self.handles?(der)
5
+ der[0] == "\x30"
6
+ end
7
+
8
+ def value
9
+ [].tap do |v|
10
+ r = @data
11
+
12
+ until r == ""
13
+ n = DerParse::Node.factory(r)
14
+ v << n.value
15
+ r = n.rest
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
metadata ADDED
@@ -0,0 +1,188 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: derparse
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Matt Palmer
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-05-08 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: github-release
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: guard-rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '10.4'
62
+ - - ">="
63
+ - !ruby/object:Gem::Version
64
+ version: 10.4.2
65
+ type: :development
66
+ prerelease: false
67
+ version_requirements: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - "~>"
70
+ - !ruby/object:Gem::Version
71
+ version: '10.4'
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: 10.4.2
75
+ - !ruby/object:Gem::Dependency
76
+ name: rb-inotify
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '0.9'
82
+ type: :development
83
+ prerelease: false
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '0.9'
89
+ - !ruby/object:Gem::Dependency
90
+ name: redcarpet
91
+ requirement: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ type: :development
97
+ prerelease: false
98
+ version_requirements: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ - !ruby/object:Gem::Dependency
104
+ name: rspec
105
+ requirement: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ type: :development
111
+ prerelease: false
112
+ version_requirements: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: '0'
117
+ - !ruby/object:Gem::Dependency
118
+ name: simplecov
119
+ requirement: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - ">="
122
+ - !ruby/object:Gem::Version
123
+ version: '0'
124
+ type: :development
125
+ prerelease: false
126
+ version_requirements: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - ">="
129
+ - !ruby/object:Gem::Version
130
+ version: '0'
131
+ - !ruby/object:Gem::Dependency
132
+ name: yard
133
+ requirement: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - ">="
136
+ - !ruby/object:Gem::Version
137
+ version: '0'
138
+ type: :development
139
+ prerelease: false
140
+ version_requirements: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - ">="
143
+ - !ruby/object:Gem::Version
144
+ version: '0'
145
+ description:
146
+ email:
147
+ - theshed+derparse@hezmatt.org
148
+ executables: []
149
+ extensions: []
150
+ extra_rdoc_files: []
151
+ files:
152
+ - ".editorconfig"
153
+ - ".gitignore"
154
+ - ".yardopts"
155
+ - CODE_OF_CONDUCT.md
156
+ - CONTRIBUTING.md
157
+ - LICENCE
158
+ - README.md
159
+ - derparse.gemspec
160
+ - lib/derparse.rb
161
+ - lib/derparse/node.rb
162
+ - lib/derparse/node/integer.rb
163
+ - lib/derparse/node/null.rb
164
+ - lib/derparse/node/octet_string.rb
165
+ - lib/derparse/node/sequence.rb
166
+ homepage: http://github.com/mpalmer/derparse
167
+ licenses: []
168
+ metadata: {}
169
+ post_install_message:
170
+ rdoc_options: []
171
+ require_paths:
172
+ - lib
173
+ required_ruby_version: !ruby/object:Gem::Requirement
174
+ requirements:
175
+ - - ">="
176
+ - !ruby/object:Gem::Version
177
+ version: 2.5.0
178
+ required_rubygems_version: !ruby/object:Gem::Requirement
179
+ requirements:
180
+ - - ">="
181
+ - !ruby/object:Gem::Version
182
+ version: '0'
183
+ requirements: []
184
+ rubygems_version: 3.0.3
185
+ signing_key:
186
+ specification_version: 4
187
+ summary: DER parsing classes and routines
188
+ test_files: []