merkle-hash-tree 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -0
- data/Gemfile +3 -0
- data/Guardfile +13 -0
- data/README.md +148 -0
- data/Rakefile +38 -0
- data/doc/DAI.md +111 -0
- data/lib/merkle-hash-tree.rb +283 -0
- data/lib/range_extensions.rb +13 -0
- data/merkle-hash-tree.gemspec +31 -0
- data/spec/audit_proof_spec.rb +116 -0
- data/spec/consistency_proof_spec.rb +41 -0
- data/spec/dai_caching_spec.rb +42 -0
- data/spec/head_n_spec.rb +36 -0
- data/spec/head_spec.rb +45 -0
- data/spec/power_of_2_smaller_than_spec.rb +37 -0
- data/spec/spec_helper.rb +34 -0
- metadata +243 -0
data/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
/Gemfile.lock
|
data/Gemfile
ADDED
data/Guardfile
ADDED
@@ -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
|
data/README.md
ADDED
@@ -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.
|
data/Rakefile
ADDED
@@ -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
|
data/doc/DAI.md
ADDED
@@ -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,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
|
data/spec/head_n_spec.rb
ADDED
@@ -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
|
data/spec/head_spec.rb
ADDED
@@ -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
|
data/spec/spec_helper.rb
ADDED
@@ -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: []
|