merkle-hash-tree 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []