merkle-hash-tree 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.
@@ -0,0 +1 @@
1
+ /Gemfile.lock
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org/'
2
+
3
+ gemspec
@@ -0,0 +1,13 @@
1
+ guard 'spork' do
2
+ watch('Gemfile') { :rspec }
3
+ watch('Gemfile.lock') { :rspec }
4
+ watch('spec/spec_helper.rb') { :rspec }
5
+ end
6
+
7
+ guard 'rspec',
8
+ :cmd => "rspec --drb",
9
+ :all_on_start => true,
10
+ :all_after_pass => true do
11
+ watch(%r{^spec/.+_spec\.rb$})
12
+ watch(%r{^lib/}) { "spec" }
13
+ end
@@ -0,0 +1,148 @@
1
+ This gem contains an implementation of "Merkle Hash Trees" (MHT).
2
+ Specifically, it implements the variant described in
3
+ [RFC6962](http://tools.ietf.org/html/rfc6962), as the initial use-case for
4
+ this gem was for an implementation of a [Certificate
5
+ Transparency](http://www.certificate-transparency.org/) log server.
6
+
7
+ # Installation
8
+
9
+ Installation should be trivial, if you're using rubygems:
10
+
11
+ gem install merkle-hash-tree
12
+
13
+ If you want to install directly from the git repo, run `rake install`.
14
+
15
+
16
+ # Usage
17
+
18
+ Using `MerkleHashTree` is relatively straightforward, although it does have
19
+ one or two intricacies. Because MHTs typically deal with large volumes of
20
+ data, it isn't enough to just load a giant list of objects into memory and
21
+ go to town -- you'll run out of memory pretty quickly, and on a large tree
22
+ you'll likely burn a lot of CPU time computing hashes. Instead, in order to
23
+ instantiate an MHT you must first construct an object that implements a
24
+ specific interface, which the MHT implementation then uses to interact with
25
+ your dataset.
26
+
27
+ ## Basic Usage
28
+
29
+ For now, though, let's assume that you have such an object, named
30
+ `mht_data`, and we'll look at how to use the MHT. (It might be useful to
31
+ understand [how MHT proofs
32
+ work](http://www.certificate-transparency.org/log-proofs-work) before you go
33
+ too deeply into this).
34
+
35
+ For starters, we'll create a new MHT:
36
+
37
+ mht = MerkleHashTree.new(mht_data, Digest::SHA256)
38
+
39
+ The `MerkleHashTree` constructor takes exactly two arguments: an object that
40
+ implements the data access interface we'll talk about later, and a class (or
41
+ object) which implements the same `digest` method signature as the core
42
+ `Digest::Base` class. Typically, this will simply be a `Digest` subclass,
43
+ such as `Digest::MD5`, `Digest::SHA1`, or (as in the example above)
44
+ `Digest::SHA256`. This second argument is the way that the MHT calculates
45
+ hashes in the tree -- it simply calls the `#digest` method on whatever you
46
+ pass in as the second argument, passing in a string and expecting raw octets
47
+ out the other end.
48
+
49
+ Once we have our MHT object, we can start to do things with it. For
50
+ example, we can get the hash of the "head" of the tree:
51
+
52
+ mht.head # => "<some long string of octets>"
53
+
54
+ You can also get the head of any subtree, by specifying the
55
+ first and last elements of the list to be covered by the subtree:
56
+
57
+ mht.head(16, 20) # => "<some more octets>"
58
+
59
+ Note that the beginning element must be a power of 2.
60
+
61
+ If you want to get the subtree from 0 to an arbitrary element in the list,
62
+ you can just specify the last element:
63
+
64
+ mht.head(42) # => "<some other long string of octets>"
65
+ # equivalent to
66
+ mht.head(0, 42)
67
+
68
+ We can also ask for a "consistency proof" between any two subtrees:
69
+
70
+ mht.consistency_proof(42, 69) # => ["<hash>", "<hash>", ... ]
71
+
72
+ If we want a consistency proof between a subtree and the current head, we
73
+ can drop the second parameter:
74
+
75
+ mht.consistency_proof(42) # => ["<hash>", "<hash>", ... ]
76
+
77
+ I'm not going to describe Merkle consistency proofs here; the Internet does
78
+ a far better job than I ever will. The return value of `#consistency_proof`
79
+ is simply an array of the hashes that are required by a client to prove that
80
+ the smaller subtree is, indeed, a subtree of the larger one (and nothing
81
+ dodgy has gone on behind the scenes). [RFC6962,
82
+ s2.1.2](http://tools.ietf.org/html/rfc6962#section-2.1.2) has all the gory
83
+ details of how to calculate it and how to use the result.
84
+
85
+ There are also such things as "audit proofs" (again, I'm not going to
86
+ explain them here), which you get by specifying a single leaf number and a
87
+ subtree ID:
88
+
89
+ mht.audit_proof(13, 42) # => ["<hash>", "<hash>", ... ]
90
+
91
+ In this example, the audit proof will return a list of hashes, starting from
92
+ the leaf node's sibling and working up towards the root node for a hash tree
93
+ containing 42 elements, that demonstrate that leaf 13 is in the tree and
94
+ hasn't been removed or altered.
95
+
96
+ You can also drop the second argument, in which case you get an audit proof
97
+ for the tree that represents the entire list as it currently exists:
98
+
99
+ mht.audit_proof(13) # => ["<hash>", "<hash>", ... ]
100
+
101
+ And that's it! There really isn't much you can do from the outside. All
102
+ the fun happens inside.
103
+
104
+
105
+ ## The Data Access Interface
106
+
107
+ Rather than trying to work with an entire dataset in memory,
108
+ `MerkleHashTree` is capable of working with a dataset far larger than what
109
+ could fit in memory, by using a data access object to fetch items and cache
110
+ intermediate results (the hashes of nodes in the tree). To do this, though,
111
+ a fair number of methods need to be implemented.
112
+
113
+ How you implement them is up to you -- you could query a backend database,
114
+ or just make up data as you felt like it. In the minimal case, you *can*
115
+ pass in an instance of Array, although I doubt you'll enjoy the performance
116
+ on any but the smallest possible hash tree.
117
+
118
+ The complete interface definition is given in `doc/DAI.md`, for those who
119
+ wish to implement their own interface. Essentially, you *must* to implement
120
+ `[](n)`, which returns the `n`th entry in the (zero-indexed) list, as well
121
+ as `length`, which returns the current size of the list. You can also
122
+ implement `cache_set(n1, n2, s)` and `cache_get(n1, n2)`, which set and get
123
+ entries in the cache of node values. If you don't implement these, then
124
+ `MerkleHashTree` will need to recalculate every hash in the tree repeatedly
125
+ for most every operation -- which will be *very* slow for anything other
126
+ than the most trivial result.
127
+
128
+ As I said before, you *can* just use Array, if you want to, which could look
129
+ something like this:
130
+
131
+ a = Array.new
132
+ mht = MerkleHashTree.new(a, Digest::MD5)
133
+
134
+ a << 'a'
135
+ a << 'b'
136
+ a << 'c'
137
+ a << 'd'
138
+ a << 'e'
139
+
140
+ mht.head # => "O\xA2\x03\x12\xF6\x0F\xFBtU\x95GY\xE53\x17\x8D"
141
+
142
+
143
+ ## Further Info
144
+
145
+ In a reversal of standard operating procedure, I heavily document all the
146
+ methods and interfaces I write. You can get complete API documentation by
147
+ using `ri` (or a descendent thereof), or via your web-based rdoc browser of
148
+ choice.
@@ -0,0 +1,38 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+
4
+ task :default => :test
5
+
6
+ begin
7
+ Bundler.setup(:default, :development)
8
+ rescue Bundler::BundlerError => e
9
+ $stderr.puts e.message
10
+ $stderr.puts "Run `bundle install` to install missing gems"
11
+ exit e.status_code
12
+ end
13
+
14
+ require 'git-version-bump/rake-tasks'
15
+
16
+ Bundler::GemHelper.install_tasks
17
+
18
+ require 'rdoc/task'
19
+
20
+ Rake::RDocTask.new do |rd|
21
+ rd.main = "README.md"
22
+ rd.title = 'lvmsync'
23
+ rd.rdoc_files.include("README.md", "lib/**/*.rb")
24
+ end
25
+
26
+ desc "Run guard"
27
+ task :guard do
28
+ require 'guard'
29
+ ::Guard.start(:clear => true)
30
+ while ::Guard.running do
31
+ sleep 0.5
32
+ end
33
+ end
34
+
35
+ require 'rspec/core/rake_task'
36
+ RSpec::Core::RakeTask.new :test do |t|
37
+ t.pattern = "spec/**/*_spec.rb"
38
+ end
@@ -0,0 +1,111 @@
1
+ The Data Access Interface for this library is a flexible way for the tree to
2
+ retrieve and cache information it needs. This is important, because the use
3
+ case for this library is to provide hash trees for datasets *far* larger
4
+ than what can be reasonably stored in memory by Ruby objects, and
5
+ potentially in diverse and application-specific stores. Therefore, it is
6
+ important that the interface between instances of `MerkleHashTree` and the
7
+ underlying list data is as flexible as possible.
8
+
9
+ The interface is designed so that an instance of `Array` *will* work, in the
10
+ minimal case, although it won't perform particularly well. In order to
11
+ maximise performance, it is recommended that the optional caching methods
12
+ also be implemented, with the cache data stored either in memory, or in a
13
+ fast network-accessable cache such as memcached or Redis.
14
+
15
+
16
+ # Mandatory Methods
17
+
18
+ There are two mandatory methods which *must* be implemented by any object
19
+ which is passed in as the data object to a call to `MerkleHashTree.new`.
20
+ They might look familiar.
21
+
22
+ Both of these methods are called *very* frequently and repeatedly; it is
23
+ highly recommended that they perform their own caching of results if
24
+ retrieval from backing store is an expensive operation. Caching is quite
25
+ easy in this system, because no value ever changes once it has been defined
26
+ (with the exception of `length`).
27
+
28
+
29
+ ## `length`
30
+
31
+ This method returns the number of items in the list. This number must be
32
+ monotonically increasing with each call -- that is, there must never be a
33
+ case where a call to `#length` on a given data object returns a
34
+ value less than that returned by a previous call to `#length` on that same
35
+ object. Failure to observe this property will absolutely and with
36
+ guaranteed certainty lead to heartbreak.
37
+
38
+
39
+ ## `[](n)`
40
+
41
+ This method returns the `n`th item in the list, indexed from zero. All
42
+ values of `[](n)` for `0 <= n < length` must return an object which responds
43
+ to `to_s` correctly (the value of `to_s` is used as the value passed to the
44
+ hashing function which calculates the leaf hash value).
45
+
46
+ If `n` is greater than or equal to a value previously returned from a call
47
+ to `length` on the same object, it is permissible to either return `nil`,
48
+ raise `ArgumentError`, or do whatever you like -- if `MerkleHashTree` ever
49
+ does that, it's a big fat stinky bug in this library.
50
+
51
+ Once a call to this method with a given value of `n` has been made, *every*
52
+ future call for the same value of `n` MUST return an object whose `to_s`
53
+ method returns an identical string. Failure to observe this requirement
54
+ will surely cause demons to fly out of your nose.
55
+
56
+
57
+ # Optional Methods
58
+
59
+ There are two optional methods which your DAI object may choose to
60
+ implement. If you implement one, though, you must implement both. They are
61
+ used to cache intermediate hashes within the tree nodes, and can
62
+ significantly improve performance on large trees, because it's a lot quicker
63
+ to retrieve a value from a database than it is to recalculate a few hundred
64
+ thousand SHA256 hashes.
65
+
66
+
67
+ ## `mht_cache_set(key, value)`
68
+
69
+ This method takes a string `key` and a string `value`, and should store that
70
+ association somewhere convenient for later retrieval. The return value is
71
+ ignored (although raising an exception is Just Not On).
72
+
73
+ For a given `key`, only one `value` will *ever* be passed (for a given DAI
74
+ object). If this allows you to optimise some part of your cache
75
+ implementation, mazel tov.
76
+
77
+
78
+ ## `mht_cache_get(key)`
79
+
80
+ This method takes a string `key` and returns either a string `value` or
81
+ `nil`. If a string is returned, that string MUST be the value passed to a
82
+ previous call to `mht_cache_set` for the same `key`.
83
+
84
+ Since this is a caching interface, It is entirely permissible to return
85
+ `nil` to a call to `mht_cache_get` for a given key when a previous call for
86
+ the same key returned a string `value`. The cache entry may well have
87
+ expired in the interim. `MerkleHashTree` will *always* handle a call to
88
+ `mht_cache_get` returning `nil` (by recalculating any and all hashes
89
+ required to regenerate the value that has not been cached).
90
+
91
+ This method MAY be called with a given `key` without a previous call to
92
+ `mht_cache_set` being made for the same `key`, and your implementation must
93
+ handle that gracefully (by returning `nil`).
94
+
95
+
96
+ # Item Methods
97
+
98
+ The objects returned from calls to `[](n)` must implement a `to_s` method
99
+ that returns a string. There is no requirement for the value returned by
100
+ `to_s` to be unique amongst all objects returned from `[](n)`, but I
101
+ certainly wouldn't recommend them all returning the same value (it would be
102
+ a very boring-looking hash tree).
103
+
104
+ To slightly improve performance, objects can also implement an accessor
105
+ method pair, `mht_leaf_hash` and `mht_leaf_hash=(s)`. If available,
106
+ `mht_leaf_hash` will be called to determine the hash value of the object; if
107
+ this method returns `nil`, then the hash value will be calculated from the
108
+ string returned by `to_s`, and then cached in the object by calling
109
+ `mht_leaf_hash=(h)`. It is not recommended that you try to be clever by
110
+ implementing a hashing scheme yourself in `mht_leaf_hash`; that way lies
111
+ madness.
@@ -0,0 +1,283 @@
1
+ require 'range_extensions'
2
+
3
+ # Implement an RFC6962-compliant Merkle Hash Tree.
4
+ #
5
+ class MerkleHashTree
6
+ # Instantiate a new MerkleHashTree.
7
+ #
8
+ # Arguments:
9
+ #
10
+ # * `data_access` -- An object which implements the Data Access Interface
11
+ # specified in `doc/DAI.md`. `Array` implements the basic interface,
12
+ # but for performance you'll want to implement the caching methods
13
+ # described in `doc/DAI.md`.
14
+ #
15
+ # The MerkleHashTree gets all of its data from this object.
16
+ #
17
+ # * `hash_class` -- An object which provides a `.digest` method which
18
+ # behaves identically to `Digest::Base.digest` -- that is, it takes
19
+ # an arbitrary string and returns another string, with the requirement
20
+ # that every call with the same input will return the same output.
21
+ #
22
+ # Raises:
23
+ #
24
+ # * `ArgumentError` -- If either argument does not meet the basic
25
+ # requirements specified above (that is, the objects don't implement
26
+ # the defined interface).
27
+ #
28
+ def initialize(data_access, hash_class)
29
+ @data = data_access
30
+ unless @data.respond_to?(:[])
31
+ raise ArgumentError,
32
+ "data_access (#{@data}) does not implement #[]"
33
+ end
34
+ unless @data.respond_to?(:length)
35
+ raise ArgumentError,
36
+ "data_access (#{@data}) does not implement #length"
37
+ end
38
+
39
+ @digest = hash_class
40
+ unless @digest.respond_to?(:digest)
41
+ raise ArgumentError,
42
+ "hash_class (#{@digest}) does not implement #digest"
43
+ end
44
+ end
45
+
46
+ # Return the hash value of a subtree.
47
+ #
48
+ # Arguments:
49
+ #
50
+ # * `subtree` -- A range of the list items over which the tree hash will
51
+ # be calculated. If not specified, it defaults to the entire current
52
+ # list.
53
+ #
54
+ # Raises:
55
+ #
56
+ # * `ArgumentError` -- if the range doesn't consist of integers, or if the
57
+ # range is outside the bounds of the current list size.
58
+ #
59
+ def head(subtree = nil)
60
+ # Super-special case when we're asking for the hash of an entire list
61
+ # that... just happens to be empty
62
+ if subtree.nil? and @data.length == 0
63
+ return digest("")
64
+ end
65
+
66
+ subtree ||= 0..(@data.length-1)
67
+
68
+ unless subtree.min.is_a? Integer and subtree.max.is_a? Integer
69
+ raise ArgumentError,
70
+ "subtree is not all integers (got #{subtree.inspect})"
71
+ end
72
+
73
+ if subtree.min < 0
74
+ raise ArgumentError,
75
+ "subtree cannot go negative (#{subtree.inspect})"
76
+ end
77
+
78
+ if subtree.max >= @data.length
79
+ raise ArgumentError,
80
+ "subtree extends beyond list length (subtree is #{subtree.inspect}, list has #{@data.length} items)"
81
+ end
82
+
83
+ if subtree.max < subtree.min
84
+ raise ArgumentError,
85
+ "subtree goes backwards (#{subtree.inspect})"
86
+ end
87
+
88
+ if @data.respond_to?(:mht_cache_get) and h = @data.mht_cache_get(subtree.inspect)
89
+ return h
90
+ end
91
+
92
+ # No caching, or not in the cache... recalculate!
93
+ h = if subtree.size == 1
94
+ # We're at a leaf!
95
+ leaf_hash(subtree.min)
96
+ else
97
+ k = power_of_2_smaller_than(subtree.size)
98
+
99
+ node_hash(head((0..k-1)+subtree.min), head(subtree.min+k..subtree.max))
100
+ end
101
+
102
+ if @data.respond_to?(:mht_cache_set)
103
+ @data.mht_cache_set(subtree.inspect, h)
104
+ end
105
+
106
+ h
107
+ end
108
+
109
+ # Generate an "audit proof" for a list item.
110
+ #
111
+ # Arguments:
112
+ #
113
+ # * `item` -- Specifies the index in the list to retrieve the audit proof
114
+ # for. Must be a non-negative integer within the bounds of the current
115
+ # list.
116
+ #
117
+ # * `subtree` -- A range which defines the subset of list items within
118
+ # which to generate the audit proof. The bounds of the range must be
119
+ # within the bounds of the current list.
120
+ #
121
+ # The return value of this method is an array of node hashes which make
122
+ # up the audit proof. The first element of the array is the immediate
123
+ # sibling of the item requested; the last is a child of the root.
124
+ #
125
+ # Raises:
126
+ #
127
+ # * `ArgumentError` -- if any provided argument isn't an integer, or is
128
+ # negative, or is out of range.
129
+ #
130
+ # * `RuntimeError` -- if an attempt is made to request an audit proof on
131
+ # an empty list.
132
+ #
133
+ def audit_proof(item, subtree=nil)
134
+ if @data.length == 0
135
+ raise RuntimeError,
136
+ "Cannot calculate an audit proof on an empty list"
137
+ end
138
+
139
+ subtree ||= (0..@data.length - 1)
140
+
141
+ unless subtree.min.is_a? Integer and subtree.max.is_a? Integer
142
+ raise ArgumentError,
143
+ "subtree must be an integer range (got #{subtree.inspect})"
144
+ end
145
+
146
+ unless item.is_a? Integer
147
+ raise ArgumentError,
148
+ "item must be an integer (got #{item.inspect})"
149
+ end
150
+
151
+ if subtree.min < 0
152
+ raise ArgumentError,
153
+ "subtree range must be non-negative (subtree is #{subtree.inspect})"
154
+ end
155
+
156
+ if subtree.max >= @data.length
157
+ raise ArgumentError,
158
+ "subtree must not extend beyond the end of the list (subtree is #{subtree.inspect}, list has #{@data.length} items)"
159
+ end
160
+
161
+ if subtree.max < subtree.min
162
+ raise ArgumentError,
163
+ "subtree must be min..max (subtree is #{subtree.inspect})"
164
+ end
165
+
166
+ # And finally, after all that, we can start actually *doing* something
167
+ if subtree.size == 1
168
+ # Audit proof for a single item is defined as being empty
169
+ []
170
+ else
171
+ k = power_of_2_smaller_than(subtree.size)
172
+
173
+ if item < k
174
+ audit_proof(item, (0..k-1)+subtree.min) + [head(subtree.min+k..subtree.max)]
175
+ else
176
+ audit_proof(subtree.min+item-k, subtree.min+k..subtree.max) +
177
+ [head((0..k-1)+subtree.min)]
178
+ end
179
+ end
180
+ end
181
+
182
+ # Generate a consistency proof.
183
+ #
184
+ # Arguments:
185
+ #
186
+ # * `m` -- The smaller list size for which you wish to generate
187
+ # the consistency proof.
188
+ #
189
+ # * `n` -- The larger list size for which you wish to generate
190
+ # the consistency proof.
191
+ #
192
+ # Raises:
193
+ #
194
+ # * `ArgumentError` -- If the arguments aren't integers, or if they're
195
+ # negative, or if `n < m`.
196
+ #
197
+ def consistency_proof(m, n)
198
+ unless m.is_a? Integer
199
+ raise ArgumentError,
200
+ "m is not an integer (got #{m.inspect})"
201
+ end
202
+
203
+ unless n.is_a? Integer
204
+ raise ArgumentError,
205
+ "n is not an integer (got #{n.inspect})"
206
+ end
207
+
208
+ if m < 0
209
+ raise ArgumentError,
210
+ "m cannot be negative (m is #{m})"
211
+ end
212
+
213
+ if n > @data.length
214
+ raise ArgumentError,
215
+ "n cannot be larger than the list length (n is #{n}, list has #{@data.length} elements)"
216
+ end
217
+
218
+ if n < m
219
+ raise ArgumentError,
220
+ "n cannot be less than m (m is #{m}, n is #{n})"
221
+ end
222
+
223
+ # This is taken from in-practice behaviour of the Google pilot/aviator
224
+ # CT servers... when first=0, you always get an empty proof.
225
+ return [] if m == 0
226
+
227
+ # And now... on to the real show!
228
+ subproof(m, 0..n-1, true)
229
+ end
230
+
231
+ private
232
+ # :nodoc:
233
+
234
+ def subproof(m, n, b)
235
+ if n.max == m-1
236
+ if b
237
+ []
238
+ else
239
+ [head(n)]
240
+ end
241
+ elsif n.min == n.max
242
+ [head(n)]
243
+ else
244
+ k = power_of_2_smaller_than(n.size)
245
+
246
+ if m <= k+n.min
247
+ subproof(m, (0..k-1)+n.min, b) + [head((n.min+k)..n.max)]
248
+ else
249
+ subproof(m, ((n.min+k)..n.max), false) + [head((0..k-1)+n.min)]
250
+ end
251
+ end
252
+ end
253
+
254
+ def digest(s)
255
+ @digest.digest(s)
256
+ end
257
+
258
+ def leaf_hash(n)
259
+ if @data[n].respond_to?(:mht_leaf_hash) and h = @data[n].mht_leaf_hash
260
+ return h
261
+ end
262
+
263
+ h = digest("\0" + @data[n].to_s)
264
+
265
+ if @data[n].respond_to?(:mht_leaf_hash=)
266
+ @data[n].mht_leaf_hash = h
267
+ end
268
+
269
+ h
270
+ end
271
+
272
+ def node_hash(h1, h2)
273
+ digest("\x01" + h1 + h2)
274
+ end
275
+
276
+ # This is almost certainly horribly inefficient, but my math skills have
277
+ # atrophied embarrassingly
278
+ def power_of_2_smaller_than(n)
279
+ raise ArgumentError, "Too small, Jim" if n < 2
280
+ s = (n-1).to_s(2).length-1
281
+ 2**s
282
+ end
283
+ end
@@ -0,0 +1,13 @@
1
+ class Range
2
+ def length
3
+ last - first + 1
4
+ end
5
+
6
+ alias_method :size, :length
7
+
8
+ def +(n)
9
+ raise ArgumentError, "n must be integer" unless n.is_a? Integer
10
+
11
+ (min+n)..(max+n)
12
+ end
13
+ end
@@ -0,0 +1,31 @@
1
+ require 'git-version-bump'
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = "merkle-hash-tree"
5
+
6
+ s.version = GVB.version
7
+ s.date = GVB.date
8
+
9
+ s.platform = Gem::Platform::RUBY
10
+
11
+ s.homepage = "http://theshed.hezmatt.org/merkle-hash-tree"
12
+ s.summary = "An RFC6962-compliant implementation of Merkle Hash Trees"
13
+ s.authors = ["Matt Palmer"]
14
+
15
+ s.extra_rdoc_files = ["README.md"]
16
+ s.files = `git ls-files`.split("\n")
17
+
18
+ s.add_runtime_dependency "git-version-bump"
19
+
20
+ s.add_development_dependency 'bundler'
21
+ s.add_development_dependency 'guard-spork'
22
+ s.add_development_dependency 'guard-rspec'
23
+ s.add_development_dependency 'plymouth'
24
+ s.add_development_dependency 'pry-debugger'
25
+ s.add_development_dependency 'rake'
26
+ # Needed for guard
27
+ s.add_development_dependency 'rb-inotify', '~> 0.9'
28
+ s.add_development_dependency 'rdoc'
29
+ s.add_development_dependency 'rspec', '~> 2.11'
30
+ s.add_development_dependency 'rspec-mocks'
31
+ end
@@ -0,0 +1,116 @@
1
+ require_relative './spec_helper'
2
+ require_relative '../lib/merkle-hash-tree'
3
+
4
+ describe "MerkleHashTree#audit_proof" do
5
+ let(:mht) { MerkleHashTree.new(data, IdentityDigest) }
6
+
7
+ context "with a single element" do
8
+ let(:data) { %w{a} }
9
+
10
+ it "returns empty" do
11
+ expect(mht.audit_proof(0)).to eq([])
12
+ end
13
+ end
14
+
15
+ context "with a one-level tree" do
16
+ let(:data) { %w{a b} }
17
+
18
+ it "works for 'b'" do
19
+ expect(mht.audit_proof(1)).to eq(['a'])
20
+ end
21
+
22
+ it "works for 'a'" do
23
+ expect(mht.audit_proof(0)).to eq(['b'])
24
+ end
25
+ end
26
+
27
+ context "with a two-level tree" do
28
+ let(:data) { %w{a b c d} }
29
+
30
+ it "works for 'a'" do
31
+ expect(mht.audit_proof(0)).to eq(['b', 'cd'])
32
+ end
33
+
34
+ it "works for 'b'" do
35
+ expect(mht.audit_proof(1)).to eq(['a', 'cd'])
36
+ end
37
+
38
+ it "works for 'c'" do
39
+ expect(mht.audit_proof(2)).to eq(['d', 'ab'])
40
+ end
41
+
42
+ it "works for 'd'" do
43
+ expect(mht.audit_proof(3)).to eq(['c', 'ab'])
44
+ end
45
+ end
46
+
47
+ context "with an unbalanced three-level tree" do
48
+ let(:data) { %w{a b c d e} }
49
+
50
+ it "works for 'a'" do
51
+ expect(mht.audit_proof(0)).to eq(['b', 'cd', 'e'])
52
+ end
53
+
54
+ it "works for 'b'" do
55
+ expect(mht.audit_proof(1)).to eq(['a', 'cd', 'e'])
56
+ end
57
+
58
+ it "works for 'c'" do
59
+ expect(mht.audit_proof(2)).to eq(['d', 'ab', 'e'])
60
+ end
61
+
62
+ it "works for 'd'" do
63
+ expect(mht.audit_proof(3)).to eq(['c', 'ab', 'e'])
64
+ end
65
+
66
+ it "works for 'e'" do
67
+ # It makes sense if you drink *juuuuust* enough tequila
68
+ expect(mht.audit_proof(4)).to eq(['abcd'])
69
+ end
70
+ end
71
+
72
+ context "with a seven node tree" do
73
+ # Taken from RFC6962, s2.1.3
74
+ let(:data) { %w{a b c d e f g} }
75
+
76
+ it "works for 'a'" do
77
+ expect(mht.audit_proof(0)).to eq(%w{b cd efg})
78
+ end
79
+
80
+ it "works for 'd'" do
81
+ expect(mht.audit_proof(3)).to eq(%w{c ab efg})
82
+ end
83
+
84
+ it "works for 'e'" do
85
+ expect(mht.audit_proof(4)).to eq(%w{f g abcd})
86
+ end
87
+
88
+ it "works for 'g'" do
89
+ expect(mht.audit_proof(6)).to eq(%w{ef abcd})
90
+ end
91
+ end
92
+
93
+ context "with a large tree" do
94
+ let(:data) { %w{a b c d e f g h} }
95
+
96
+ it "works for 'd'" do
97
+ expect(mht.audit_proof(3)).to eq(%w{c ab efgh})
98
+ end
99
+ end
100
+
101
+ context "with a hueg tree" do
102
+ let(:data) { %w{a b c d e f g h i j k l m n o p q r s t u v w x y z} }
103
+
104
+ it "works for 'f'" do
105
+ expect(mht.audit_proof(5)).to eq(%w{e gh abcd ijklmnop qrstuvwxyz})
106
+ end
107
+
108
+ it "works for 'q'" do
109
+ expect(mht.audit_proof(15)).to eq(%w{o mn ijkl abcdefgh qrstuvwxyz})
110
+ end
111
+
112
+ it "works for 'z'" do
113
+ expect(mht.audit_proof(25)).to eq(%w{y qrstuvwx abcdefghijklmnop})
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,41 @@
1
+ require_relative './spec_helper'
2
+ require_relative '../lib/merkle-hash-tree'
3
+
4
+ describe "MerkleHashTree#consistency_proof" do
5
+ let(:mht) { MerkleHashTree.new(data, IdentityDigest) }
6
+
7
+ context "with a seven-node tree" do
8
+ # Taken from RFC6962, s2.1.3
9
+ let(:data) { %w{a b c d e f g} }
10
+
11
+ it "is empty for 0->7" do
12
+ expect(mht.consistency_proof(0, 7)).to eq([])
13
+ end
14
+
15
+ it "is empty for 7->7" do
16
+ expect(mht.consistency_proof(7, 7)).to eq([])
17
+ end
18
+
19
+ it "works for 3->7" do
20
+ expect(mht.consistency_proof(3, 7)).to eq(%w{c d ab efg})
21
+ end
22
+
23
+ it "works for 4->7" do
24
+ expect(mht.consistency_proof(4, 7)).to eq(%w{efg})
25
+ end
26
+
27
+ it "works for 6->7" do
28
+ expect(mht.consistency_proof(6, 7)).to eq(%w{ef g abcd})
29
+ end
30
+ end
31
+
32
+ context "with a full three-level tree" do
33
+ # Taken from www.certificate-transparency.org's "How Log Proofs Work"
34
+ # page
35
+ let(:data) { %w{a b c d e f g h} }
36
+
37
+ it "works for 6->8" do
38
+ expect(mht.consistency_proof(6, 8)).to eq(%w{ef gh abcd})
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,42 @@
1
+ require_relative './spec_helper'
2
+ require_relative '../lib/merkle-hash-tree'
3
+
4
+ describe "DAI caching" do
5
+ let(:dai) do
6
+ %w{a b c d e f g}
7
+ end
8
+
9
+ let(:mht) { MerkleHashTree.new(dai, IdentityDigest) }
10
+
11
+ it "tries to get cache values, but is OK with nil" do
12
+ dai.
13
+ should_receive(:mht_cache_get).
14
+ with(any_args).
15
+ at_least(:once).
16
+ and_return(nil)
17
+
18
+ mht.head
19
+ end
20
+
21
+ it "respects cache values" do
22
+ dai.
23
+ should_receive(:mht_cache_get).
24
+ with('1..5').
25
+ and_return('xyzzy')
26
+
27
+ expect(mht.head(1..5)).to eq('xyzzy')
28
+ end
29
+
30
+ it "tries to cache calculated values" do
31
+ dai.
32
+ should_receive(:mht_cache_get).
33
+ with(any_args).
34
+ at_least(:once).
35
+ and_return(nil)
36
+ dai.
37
+ should_receive(:mht_cache_set).
38
+ with('2..2', 'c')
39
+
40
+ mht.head(2..2)
41
+ end
42
+ end
@@ -0,0 +1,36 @@
1
+ require_relative './spec_helper'
2
+ require_relative '../lib/merkle-hash-tree'
3
+ require 'digest/sha2'
4
+
5
+ describe "MerkleHashTree#head" do
6
+ let(:data) do
7
+ ["", "\0", "\x10", "\x20\x21", "\x30\x31", "\x40\x41\x42\x43",
8
+ "\x50\x51\x52\x53\x54\x55\x56\x57",
9
+ "\x60\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f"
10
+ ]
11
+ end
12
+ let(:mht) { MerkleHashTree.new(data, Digest::SHA256) }
13
+
14
+ hashes = ["6e340b9cffb37a989ca544e6bb780a2c78901d3fb33738768511a30617afa01d",
15
+ "fac54203e7cc696cf0dfcb42c92a1d9dbaf70ad9e621f4bd8d98662f00e3c125",
16
+ "aeb6bcfe274b70a14fb067a5e5578264db0fa9b51af5e0ba159158f329e06e77",
17
+ "d37ee418976dd95753c1c73862b9398fa2a2cf9b4ff0fdfe8b30cd95209614b7",
18
+ "4e3bbb1f7b478dcfe71fb631631519a3bca12c9aefca1612bfce4c13a86264d4",
19
+ "76e67dadbcdf1e10e1b74ddc608abd2f98dfb16fbce75277b5232a127f2087ef",
20
+ "ddb89be403809e325750d3d263cd78929c2942b7942a34b77e122c9594a74c8c",
21
+ "5dc9da79a70659a9ad559cb701ded9a2ab9d823aad2f4960cfe370eff4604328"
22
+ ]
23
+
24
+
25
+ def hexstring(s)
26
+ s.scan(/./m).map { |c| sprintf("%02x", c.ord) }.join
27
+ end
28
+
29
+ 8.times do |i|
30
+ context "head(#{i})" do
31
+ it "gives a specific hash" do
32
+ expect(hexstring(mht.head(0..i))).to eq(hashes[i])
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,45 @@
1
+ require_relative './spec_helper'
2
+ require_relative '../lib/merkle-hash-tree'
3
+ require 'digest/sha2'
4
+
5
+ # These tests are taken directly from the CT reference implementation's
6
+ # merkle tree test suite; they're used to ensure we conform to that
7
+ # specification correctly
8
+ describe "MerkleHashTree#head" do
9
+ let(:mht) { MerkleHashTree.new(data, Digest::SHA256) }
10
+
11
+ hashes = [Digest::SHA256.hexdigest(""),
12
+ "6e340b9cffb37a989ca544e6bb780a2c78901d3fb33738768511a30617afa01d",
13
+ "fac54203e7cc696cf0dfcb42c92a1d9dbaf70ad9e621f4bd8d98662f00e3c125",
14
+ "aeb6bcfe274b70a14fb067a5e5578264db0fa9b51af5e0ba159158f329e06e77",
15
+ "d37ee418976dd95753c1c73862b9398fa2a2cf9b4ff0fdfe8b30cd95209614b7",
16
+ "4e3bbb1f7b478dcfe71fb631631519a3bca12c9aefca1612bfce4c13a86264d4",
17
+ "76e67dadbcdf1e10e1b74ddc608abd2f98dfb16fbce75277b5232a127f2087ef",
18
+ "ddb89be403809e325750d3d263cd78929c2942b7942a34b77e122c9594a74c8c",
19
+ "5dc9da79a70659a9ad559cb701ded9a2ab9d823aad2f4960cfe370eff4604328"
20
+ ]
21
+
22
+ leaves = ["",
23
+ "\0",
24
+ "\x10",
25
+ "\x20\x21",
26
+ "\x30\x31",
27
+ "\x40\x41\x42\x43",
28
+ "\x50\x51\x52\x53\x54\x55\x56\x57",
29
+ "\x60\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f"
30
+ ]
31
+
32
+ def string_from_hex(s)
33
+ s.scan(/../).map { |c| c.to_i(16).chr }.join
34
+ end
35
+
36
+ 9.times do |i|
37
+ context "with #{i} items" do
38
+ let(:data) { leaves.take(i) }
39
+
40
+ it "gives a specific hash" do
41
+ expect(mht.head).to eq(string_from_hex(hashes[i]))
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,37 @@
1
+ require_relative './spec_helper'
2
+ require_relative '../lib/merkle-hash-tree'
3
+ require 'digest/sha1'
4
+
5
+ describe "MerkleHashTree#power_of_2_smaller_than" do
6
+ tests = {
7
+ 2 => 1,
8
+ 3 => 2,
9
+ 4 => 2,
10
+ 5 => 4,
11
+ 6 => 4,
12
+ 7 => 4,
13
+ 8 => 4,
14
+ 9 => 8,
15
+ 10 => 8,
16
+ 15 => 8,
17
+ 16 => 8,
18
+ 17 => 16,
19
+ 18 => 16,
20
+ 31 => 16,
21
+ 32 => 16,
22
+ 33 => 32
23
+ }
24
+
25
+ let(:mht) { MerkleHashTree.new([], Digest::SHA1) }
26
+
27
+ it "bombs out for n=1" do
28
+ expect { mht.send(:power_of_2_smaller_than, 1) }.
29
+ to raise_error(ArgumentError, /Too small, Jim/)
30
+ end
31
+
32
+ tests.each_pair do |k, v|
33
+ it "(#{k}) => #{v}" do
34
+ expect(mht.send(:power_of_2_smaller_than, k)).to eq(v)
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,34 @@
1
+ require 'spork'
2
+
3
+ Spork.prefork do
4
+ require 'bundler'
5
+ Bundler.setup(:default, :test)
6
+ require 'rspec/core'
7
+
8
+ require 'rspec/mocks'
9
+
10
+ require 'pry'
11
+ # require 'plymouth'
12
+
13
+ RSpec.configure do |config|
14
+ config.fail_fast = true
15
+ # config.full_backtrace = true
16
+
17
+ config.expect_with :rspec do |c|
18
+ c.syntax = :expect
19
+ end
20
+ end
21
+
22
+ # Our super-special digest class to make it easier to understand WTF is
23
+ # going on
24
+ class IdentityDigest
25
+ def self.digest(s)
26
+ # Strip off the first character, it'll just be a \0 or \x1 anyway
27
+ s[1..-1]
28
+ end
29
+ end
30
+ end
31
+
32
+ Spork.each_run do
33
+ # Nothing to do here, specs will load the files they need
34
+ end
metadata ADDED
@@ -0,0 +1,243 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: merkle-hash-tree
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Matt Palmer
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2014-07-10 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: git-version-bump
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: bundler
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: guard-spork
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ - !ruby/object:Gem::Dependency
63
+ name: guard-rspec
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ - !ruby/object:Gem::Dependency
79
+ name: plymouth
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ type: :development
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ - !ruby/object:Gem::Dependency
95
+ name: pry-debugger
96
+ requirement: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ! '>='
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ type: :development
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ! '>='
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ - !ruby/object:Gem::Dependency
111
+ name: rake
112
+ requirement: !ruby/object:Gem::Requirement
113
+ none: false
114
+ requirements:
115
+ - - ! '>='
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ none: false
122
+ requirements:
123
+ - - ! '>='
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ - !ruby/object:Gem::Dependency
127
+ name: rb-inotify
128
+ requirement: !ruby/object:Gem::Requirement
129
+ none: false
130
+ requirements:
131
+ - - ~>
132
+ - !ruby/object:Gem::Version
133
+ version: '0.9'
134
+ type: :development
135
+ prerelease: false
136
+ version_requirements: !ruby/object:Gem::Requirement
137
+ none: false
138
+ requirements:
139
+ - - ~>
140
+ - !ruby/object:Gem::Version
141
+ version: '0.9'
142
+ - !ruby/object:Gem::Dependency
143
+ name: rdoc
144
+ requirement: !ruby/object:Gem::Requirement
145
+ none: false
146
+ requirements:
147
+ - - ! '>='
148
+ - !ruby/object:Gem::Version
149
+ version: '0'
150
+ type: :development
151
+ prerelease: false
152
+ version_requirements: !ruby/object:Gem::Requirement
153
+ none: false
154
+ requirements:
155
+ - - ! '>='
156
+ - !ruby/object:Gem::Version
157
+ version: '0'
158
+ - !ruby/object:Gem::Dependency
159
+ name: rspec
160
+ requirement: !ruby/object:Gem::Requirement
161
+ none: false
162
+ requirements:
163
+ - - ~>
164
+ - !ruby/object:Gem::Version
165
+ version: '2.11'
166
+ type: :development
167
+ prerelease: false
168
+ version_requirements: !ruby/object:Gem::Requirement
169
+ none: false
170
+ requirements:
171
+ - - ~>
172
+ - !ruby/object:Gem::Version
173
+ version: '2.11'
174
+ - !ruby/object:Gem::Dependency
175
+ name: rspec-mocks
176
+ requirement: !ruby/object:Gem::Requirement
177
+ none: false
178
+ requirements:
179
+ - - ! '>='
180
+ - !ruby/object:Gem::Version
181
+ version: '0'
182
+ type: :development
183
+ prerelease: false
184
+ version_requirements: !ruby/object:Gem::Requirement
185
+ none: false
186
+ requirements:
187
+ - - ! '>='
188
+ - !ruby/object:Gem::Version
189
+ version: '0'
190
+ description:
191
+ email:
192
+ executables: []
193
+ extensions: []
194
+ extra_rdoc_files:
195
+ - README.md
196
+ files:
197
+ - .gitignore
198
+ - Gemfile
199
+ - Guardfile
200
+ - README.md
201
+ - Rakefile
202
+ - doc/DAI.md
203
+ - lib/merkle-hash-tree.rb
204
+ - lib/range_extensions.rb
205
+ - merkle-hash-tree.gemspec
206
+ - spec/audit_proof_spec.rb
207
+ - spec/consistency_proof_spec.rb
208
+ - spec/dai_caching_spec.rb
209
+ - spec/head_n_spec.rb
210
+ - spec/head_spec.rb
211
+ - spec/power_of_2_smaller_than_spec.rb
212
+ - spec/spec_helper.rb
213
+ homepage: http://theshed.hezmatt.org/merkle-hash-tree
214
+ licenses: []
215
+ post_install_message:
216
+ rdoc_options: []
217
+ require_paths:
218
+ - lib
219
+ required_ruby_version: !ruby/object:Gem::Requirement
220
+ none: false
221
+ requirements:
222
+ - - ! '>='
223
+ - !ruby/object:Gem::Version
224
+ version: '0'
225
+ segments:
226
+ - 0
227
+ hash: 4544178081523726354
228
+ required_rubygems_version: !ruby/object:Gem::Requirement
229
+ none: false
230
+ requirements:
231
+ - - ! '>='
232
+ - !ruby/object:Gem::Version
233
+ version: '0'
234
+ segments:
235
+ - 0
236
+ hash: 4544178081523726354
237
+ requirements: []
238
+ rubyforge_project:
239
+ rubygems_version: 1.8.23
240
+ signing_key:
241
+ specification_version: 3
242
+ summary: An RFC6962-compliant implementation of Merkle Hash Trees
243
+ test_files: []