veriform 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +13 -0
- data/.rspec +4 -0
- data/.rubocop.yml +40 -0
- data/.ruby-version +1 -0
- data/Gemfile +15 -0
- data/Guardfile +18 -0
- data/README.md +76 -0
- data/Rakefile +25 -0
- data/lib/veriform.rb +24 -0
- data/lib/veriform/decoder.rb +43 -0
- data/lib/veriform/exceptions.rb +27 -0
- data/lib/veriform/object.rb +86 -0
- data/lib/veriform/parser.rb +89 -0
- data/lib/veriform/varint.rb +106 -0
- data/lib/veriform/version.rb +5 -0
- data/lib/veriform/zhash.rb +120 -0
- data/veriform.gemspec +28 -0
- metadata +77 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: d487a0eb69dec625c5b1a3504ca9efbcc13f2ec2
|
4
|
+
data.tar.gz: 1bb60e85054d6c70f692ccdc33311faac9bf57e7
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: a36d7843ddb5fbb7dc80f48f396f00f0e11972a130c37e6334f56e2f9bc333854117ae7a7639e6a84a6d988ba663ac77195eb7ecf58d5cb2907ad0e94da66a45
|
7
|
+
data.tar.gz: 06f7d19d295d31c700b42a9463cf384015a014145712fec5e1638d9598e32a662b153c6473663497851e2e5d7c7f3dd33fda0a8a86d944211af2f085ea738e17
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
AllCops:
|
2
|
+
DisplayCopNames: true
|
3
|
+
|
4
|
+
#
|
5
|
+
# Style
|
6
|
+
#
|
7
|
+
|
8
|
+
Style/ModuleFunction:
|
9
|
+
Enabled: false
|
10
|
+
|
11
|
+
Style/NumericPredicate:
|
12
|
+
Enabled: false
|
13
|
+
|
14
|
+
Style/StringLiterals:
|
15
|
+
EnforcedStyle: double_quotes
|
16
|
+
|
17
|
+
#
|
18
|
+
# Metrics
|
19
|
+
#
|
20
|
+
|
21
|
+
Metrics/AbcSize:
|
22
|
+
Max: 30
|
23
|
+
|
24
|
+
Metrics/BlockLength:
|
25
|
+
Enabled: false
|
26
|
+
|
27
|
+
Metrics/ClassLength:
|
28
|
+
Max: 100
|
29
|
+
|
30
|
+
Metrics/CyclomaticComplexity:
|
31
|
+
Max: 25
|
32
|
+
|
33
|
+
Metrics/LineLength:
|
34
|
+
Max: 128
|
35
|
+
|
36
|
+
Metrics/MethodLength:
|
37
|
+
Max: 25
|
38
|
+
|
39
|
+
Metrics/PerceivedComplexity:
|
40
|
+
Max: 25
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.4.1
|
data/Gemfile
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
source "https://rubygems.org"
|
4
|
+
ruby RUBY_VERSION
|
5
|
+
|
6
|
+
gemspec
|
7
|
+
|
8
|
+
group :development, :test do
|
9
|
+
gem "benchmark-ips"
|
10
|
+
gem "guard-rspec"
|
11
|
+
gem "rake"
|
12
|
+
gem "rspec", "~> 3.5"
|
13
|
+
gem "rubocop", "0.49.1"
|
14
|
+
gem "tjson", "~> 0.5"
|
15
|
+
end
|
data/Guardfile
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# More info at https://github.com/guard/guard#readme
|
4
|
+
|
5
|
+
guard :rspec, cmd: "GUARD_RSPEC=1 bundle exec rspec --no-profile" do
|
6
|
+
require "guard/rspec/dsl"
|
7
|
+
dsl = Guard::RSpec::Dsl.new(self)
|
8
|
+
|
9
|
+
# RSpec files
|
10
|
+
rspec = dsl.rspec
|
11
|
+
watch(rspec.spec_helper) { rspec.spec_dir }
|
12
|
+
watch(rspec.spec_support) { rspec.spec_dir }
|
13
|
+
watch(rspec.spec_files)
|
14
|
+
|
15
|
+
# Ruby files
|
16
|
+
ruby = dsl.ruby
|
17
|
+
dsl.watch_spec_files_for(ruby.lib_files)
|
18
|
+
end
|
data/README.md
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
# veriform.rb [![Latest Version][gem-shield]][gem-link] [![Build Status][build-image]][build-link] [![MIT licensed][license-image]][license-link]
|
2
|
+
|
3
|
+
[gem-shield]: https://badge.fury.io/rb/veriform.svg
|
4
|
+
[gem-link]: https://rubygems.org/gems/veriform
|
5
|
+
[build-image]: https://secure.travis-ci.org/zcred/veriform.svg?branch=master
|
6
|
+
[build-link]: http://travis-ci.org/zcred/veriform
|
7
|
+
[license-image]: https://img.shields.io/badge/license-MIT-blue.svg
|
8
|
+
[license-link]: https://github.com/zcred/veriform/blob/master/LICENSE.txt
|
9
|
+
|
10
|
+
Ruby implementation of **Veriform**: a cryptographically verifiable data
|
11
|
+
serialization format inspired by Protocol Buffers, useful for things like
|
12
|
+
credentials, transparency logs, and "blockchain" applications.
|
13
|
+
|
14
|
+
For more information, see the [toplevel README.md].
|
15
|
+
|
16
|
+
[toplevel README.md]: https://github.com/zcred/veriform/blob/master/README.md
|
17
|
+
|
18
|
+
## Help and Discussion
|
19
|
+
|
20
|
+
Have questions? Want to suggest a feature or change?
|
21
|
+
|
22
|
+
* [Gitter]: web-based chat about zcred projects including **Veriform**
|
23
|
+
* [Google Group]: join via web or email ([zcred+subscribe@googlegroups.com])
|
24
|
+
|
25
|
+
[Gitter]: https://gitter.im/zcred/Lobby
|
26
|
+
[Google Group]: https://groups.google.com/forum/#!forum/zcred
|
27
|
+
[zcred+subscribe@googlegroups.com]: mailto:zcred+subscribe@googlegroups.com
|
28
|
+
|
29
|
+
## Requirements
|
30
|
+
|
31
|
+
This library is tested against the following MRI versions:
|
32
|
+
|
33
|
+
- 2.2
|
34
|
+
- 2.3
|
35
|
+
- 2.4
|
36
|
+
|
37
|
+
Other Ruby versions may work, but are not officially supported.
|
38
|
+
|
39
|
+
## Installation
|
40
|
+
|
41
|
+
Add this line to your application's Gemfile:
|
42
|
+
|
43
|
+
```ruby
|
44
|
+
gem "veriform"
|
45
|
+
```
|
46
|
+
|
47
|
+
And then execute:
|
48
|
+
|
49
|
+
$ bundle
|
50
|
+
|
51
|
+
Or install it yourself as:
|
52
|
+
|
53
|
+
$ gem install veriform
|
54
|
+
|
55
|
+
## API
|
56
|
+
|
57
|
+
### Veriform.parse
|
58
|
+
|
59
|
+
To parse a **veriform** message, use the `Veriform.parse` method:
|
60
|
+
|
61
|
+
```ruby
|
62
|
+
>> Veriform.parse("\x15\x07\x02\x03\x55".b)
|
63
|
+
=> {1=>{24=>42}}
|
64
|
+
```
|
65
|
+
|
66
|
+
## Contributing
|
67
|
+
|
68
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/zcred/veriform
|
69
|
+
|
70
|
+
## Copyright
|
71
|
+
|
72
|
+
Copyright (c) 2017 [The Zcred Developers][AUTHORS].
|
73
|
+
See [LICENSE.txt] for further details.
|
74
|
+
|
75
|
+
[AUTHORS]: https://github.com/zcred/zcred/blob/master/AUTHORS.md
|
76
|
+
[LICENSE.txt]: https://github.com/zcred/veriform/blob/master/LICENSE.txt
|
data/Rakefile
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "bundler/gem_tasks"
|
4
|
+
|
5
|
+
require "rspec/core/rake_task"
|
6
|
+
RSpec::Core::RakeTask.new
|
7
|
+
|
8
|
+
require "rubocop/rake_task"
|
9
|
+
RuboCop::RakeTask.new
|
10
|
+
|
11
|
+
task default: %w[spec rubocop]
|
12
|
+
|
13
|
+
task :bench do
|
14
|
+
require "benchmark/ips"
|
15
|
+
require "veriform"
|
16
|
+
|
17
|
+
Benchmark.ips do |b|
|
18
|
+
input = "\xE9\xF4\x81\x80\x80\x80@".dup.force_encoding("BINARY").freeze
|
19
|
+
|
20
|
+
b.report("vint64 encode") { Veriform::Varint.encode(281_474_976_741_993) }
|
21
|
+
b.report("vint64 decode") { Veriform::Varint.decode(input) }
|
22
|
+
|
23
|
+
b.compare!
|
24
|
+
end
|
25
|
+
end
|
data/lib/veriform.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "veriform/version"
|
4
|
+
require "veriform/exceptions"
|
5
|
+
|
6
|
+
require "veriform/decoder"
|
7
|
+
require "veriform/object"
|
8
|
+
require "veriform/parser"
|
9
|
+
require "veriform/varint"
|
10
|
+
require "veriform/zhash"
|
11
|
+
|
12
|
+
# Cryptographically verifiable data serialization format inspired by Protocol Buffers
|
13
|
+
module Veriform
|
14
|
+
# Parse the given self-describing Veriform message
|
15
|
+
#
|
16
|
+
# @param message [String] binary encoded Veriform message
|
17
|
+
#
|
18
|
+
# @return [Veriform::Object] `::Hash`-like object representing message
|
19
|
+
def self.parse(message)
|
20
|
+
parser = Veriform::Parser.new(Veriform::Decoder.new)
|
21
|
+
parser.parse(message)
|
22
|
+
parser.finish
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Veriform
|
4
|
+
# Build Veriform::Objects from Veriform's self-describing messages
|
5
|
+
class Decoder
|
6
|
+
# Create a new decoder object which will construct a Veriform::Object tree
|
7
|
+
def initialize
|
8
|
+
@stack = [Veriform::Object.new]
|
9
|
+
end
|
10
|
+
|
11
|
+
# Add a uint64 to the current object
|
12
|
+
def uint64(id, value)
|
13
|
+
raise TypeError, "expected Integer, got #{value.class}" unless value.is_a?(Integer)
|
14
|
+
@stack.last[id] = value
|
15
|
+
end
|
16
|
+
|
17
|
+
# Add binary data to the current object
|
18
|
+
def binary(id, value)
|
19
|
+
raise TypeError, "expected String, got #{value.class}" unless value.is_a?(String)
|
20
|
+
raise EncodingError, "expected BINARY encoding, got #{value.encoding}" unless value.encoding == Encoding::BINARY
|
21
|
+
@stack.last[id] = value
|
22
|
+
end
|
23
|
+
|
24
|
+
# Push down the internal stack, constructing a new Veriform::Object
|
25
|
+
def begin_nested
|
26
|
+
@stack << Veriform::Object.new
|
27
|
+
end
|
28
|
+
|
29
|
+
# Complete the pushdown, adding the newly constructed object to the next one in the stack
|
30
|
+
def end_nested(id)
|
31
|
+
value = @stack.pop
|
32
|
+
raise StateError, "not inside a nested message" if @stack.empty?
|
33
|
+
@stack.last[id] = value
|
34
|
+
end
|
35
|
+
|
36
|
+
# Finish decoding, returning the parent Veriform::Object
|
37
|
+
def finish
|
38
|
+
result = @stack.pop
|
39
|
+
raise StateError, "objects remaining in stack" unless @stack.empty?
|
40
|
+
result
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Veriform
|
4
|
+
# Base class of all Veriform errors
|
5
|
+
Error = Class.new(StandardError)
|
6
|
+
|
7
|
+
# Generic parse error
|
8
|
+
ParseError = Class.new(Error)
|
9
|
+
|
10
|
+
# Data is not in the correct character encoding
|
11
|
+
EncodingError = Class.new(ParseError)
|
12
|
+
|
13
|
+
# Unexpected end of input
|
14
|
+
TruncatedMessageError = Class.new(ParseError)
|
15
|
+
|
16
|
+
# Message is larger than our maximum configured size
|
17
|
+
OversizeMessageError = Class.new(ParseError)
|
18
|
+
|
19
|
+
# Nested message structure is too deep
|
20
|
+
DepthError = Class.new(ParseError)
|
21
|
+
|
22
|
+
# Parser is in the wrong state to perform the given task
|
23
|
+
StateError = Class.new(ParseError)
|
24
|
+
|
25
|
+
# Field repeated in message
|
26
|
+
DuplicateFieldError = Class.new(ParseError)
|
27
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "forwardable"
|
4
|
+
|
5
|
+
module Veriform
|
6
|
+
# Key/value pairs ala JSON objects or Protobuf messages
|
7
|
+
class Object
|
8
|
+
extend Enumerable
|
9
|
+
extend Forwardable
|
10
|
+
|
11
|
+
# Delegate certain Hash functions to the underlying hash
|
12
|
+
def_delegators :@fields, :each, :keys
|
13
|
+
|
14
|
+
# Create a Veriform::Object from a TJSON::Object
|
15
|
+
def self.from_tjson(obj)
|
16
|
+
raise TypeError, "expected TJSON::Object, got #{obj.class}" unless obj.is_a?(TJSON::Object)
|
17
|
+
|
18
|
+
new.tap do |result|
|
19
|
+
obj.each do |key, value|
|
20
|
+
result[Integer(key, 10)] = value.is_a?(TJSON::Object) ? from_tjson(value) : value
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# Create a new Veriform::Object
|
26
|
+
#
|
27
|
+
# @return [Veriform::Object]
|
28
|
+
def initialize
|
29
|
+
@fields = {}
|
30
|
+
end
|
31
|
+
|
32
|
+
# Retrieve the value associated with a field identifier in a Veriform::Object
|
33
|
+
#
|
34
|
+
# @param key [Integer] field identifier
|
35
|
+
#
|
36
|
+
# @return [Object] value associated with this key
|
37
|
+
def [](key)
|
38
|
+
@fields[key]
|
39
|
+
end
|
40
|
+
|
41
|
+
# Sets the value associated with a field identifier
|
42
|
+
#
|
43
|
+
# @param key [Integer] field identifier
|
44
|
+
# @param value [Object] value associated with the given key
|
45
|
+
#
|
46
|
+
# @raise [TypeError] non-Integer key given
|
47
|
+
# @raise [Veriform::DuplicateFieldError] attempt to set field that's already been set
|
48
|
+
#
|
49
|
+
# @return [Object] newly set value
|
50
|
+
def []=(key, value)
|
51
|
+
raise TypeError, "key must be an integer: #{key.inspect}" unless key.is_a?(Integer)
|
52
|
+
raise RangeError, "key must be positive: #{key.inspect}" if key < 0
|
53
|
+
raise DuplicateFieldError, "duplicate field ID: #{key}" if @fields.key?(key)
|
54
|
+
|
55
|
+
@fields[key] = value
|
56
|
+
end
|
57
|
+
|
58
|
+
# Return a hash representation of this object (and its children).
|
59
|
+
# This is akin to an `#as_json` method as seen in e.g. Rails.
|
60
|
+
#
|
61
|
+
# @return [Hash] a hash representation of this object
|
62
|
+
def to_h
|
63
|
+
result = {}
|
64
|
+
|
65
|
+
@fields.each do |k, v|
|
66
|
+
result[k] = v.is_a?(self.class) ? v.to_h : v
|
67
|
+
end
|
68
|
+
|
69
|
+
result
|
70
|
+
end
|
71
|
+
|
72
|
+
# Compare two Veriform::Objects by value for equality
|
73
|
+
def eql?(other)
|
74
|
+
return false unless other.is_a?(self.class)
|
75
|
+
return false unless keys.length == other.keys.length
|
76
|
+
|
77
|
+
keys.each do |key|
|
78
|
+
return false unless self[key].eql?(other[key])
|
79
|
+
end
|
80
|
+
|
81
|
+
true
|
82
|
+
end
|
83
|
+
|
84
|
+
alias == eql?
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Veriform
|
4
|
+
# Parses encoded Veriform messages, invoking callbacks in the given handler
|
5
|
+
# (i.e. this is a "push parser" which supports different backends)
|
6
|
+
class Parser
|
7
|
+
# Default maximum length of a Veriform message. This is a conservative choice
|
8
|
+
# as Veriform's main intended use is a credential format.
|
9
|
+
MAX_LENGTH = 1024
|
10
|
+
|
11
|
+
# Default maximum depth (i.e. number of levels of child objects)
|
12
|
+
MAX_DEPTH = 8
|
13
|
+
|
14
|
+
# Create a new message parser with the given parse event handler
|
15
|
+
def initialize(handler, max_length = MAX_LENGTH, max_depth = MAX_DEPTH)
|
16
|
+
@handler = handler
|
17
|
+
@max_length = max_length
|
18
|
+
@max_depth = max_depth
|
19
|
+
@remaining = []
|
20
|
+
end
|
21
|
+
|
22
|
+
# Parse the given Veriform message, invoking callbacks as necessary
|
23
|
+
def parse(msg)
|
24
|
+
raise OversizeMessageError, "length #{msg.length} exceeds max of #{@max_length}" if msg.length > @max_length
|
25
|
+
raise EncodingError, "expected BINARY encoding, got #{msg.encoding}" unless msg.encoding == Encoding::BINARY
|
26
|
+
@remaining << msg
|
27
|
+
|
28
|
+
raise DepthError, "exceeded max depth of #{@max_depth}" if @remaining.size > @max_depth
|
29
|
+
|
30
|
+
until @remaining.last.empty?
|
31
|
+
id, wiretype = parse_field_prefix
|
32
|
+
|
33
|
+
case wiretype
|
34
|
+
when 0 then parse_uint64(id)
|
35
|
+
when 2 then parse_message(id)
|
36
|
+
when 3 then parse_binary(id)
|
37
|
+
else raise ParseError, "unknown wiretype: #{wiretype.inspect}"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
@remaining.pop
|
42
|
+
|
43
|
+
true
|
44
|
+
end
|
45
|
+
|
46
|
+
# Finish parsing, returning the resulting object produced by the builder
|
47
|
+
def finish
|
48
|
+
@handler.finish
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
# Parse a varint which also stores a wiretype
|
54
|
+
def parse_field_prefix
|
55
|
+
result, remaining = Veriform::Varint.decode(@remaining.pop)
|
56
|
+
@remaining << remaining
|
57
|
+
wiretype = result & 0x7
|
58
|
+
[result >> 3, wiretype]
|
59
|
+
end
|
60
|
+
|
61
|
+
# Parse an unsigned 64-bit integer
|
62
|
+
def parse_uint64(id)
|
63
|
+
value, remaining = Veriform::Varint.decode(@remaining.pop)
|
64
|
+
@remaining << remaining
|
65
|
+
@handler.uint64(id, value)
|
66
|
+
end
|
67
|
+
|
68
|
+
# Parse a data type stored with a length prefix
|
69
|
+
def parse_length_prefixed_data
|
70
|
+
length, remaining = Veriform::Varint.decode(@remaining.pop)
|
71
|
+
raise TruncatedMessageError, "not enough bytes remaining in input" if remaining.bytesize < length
|
72
|
+
data = remaining.byteslice(0, length)
|
73
|
+
@remaining << remaining.byteslice(length, remaining.bytesize - length)
|
74
|
+
data
|
75
|
+
end
|
76
|
+
|
77
|
+
# Parse a nested message
|
78
|
+
def parse_message(id)
|
79
|
+
@handler.begin_nested
|
80
|
+
parse(parse_length_prefixed_data)
|
81
|
+
@handler.end_nested(id)
|
82
|
+
end
|
83
|
+
|
84
|
+
# Parse length-prefixed binary data
|
85
|
+
def parse_binary(id)
|
86
|
+
@handler.binary(id, parse_length_prefixed_data)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
# encoding: binary
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Veriform
|
5
|
+
# Little Endian 64-bit Unsigned Prefix Varints
|
6
|
+
module Varint
|
7
|
+
# Maximum value we can encode as a vint64
|
8
|
+
MAX = (2**64) - 1
|
9
|
+
|
10
|
+
# :nodoc: Lookup table for the number of trailing zeroes in a byte
|
11
|
+
CTZ_TABLE = [8, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
|
12
|
+
4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
|
13
|
+
5, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
|
14
|
+
4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
|
15
|
+
6, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
|
16
|
+
4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
|
17
|
+
5, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
|
18
|
+
4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
|
19
|
+
7, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
|
20
|
+
4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
|
21
|
+
5, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
|
22
|
+
4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
|
23
|
+
6, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
|
24
|
+
4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
|
25
|
+
5, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
|
26
|
+
4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0].freeze
|
27
|
+
|
28
|
+
# Encode the given unsignedinteger value as a vint64
|
29
|
+
#
|
30
|
+
# @param value [Integer] unsigned integer value to encode as a vint64
|
31
|
+
#
|
32
|
+
# @raise [TypeError] non-integer value given
|
33
|
+
# @raise [ArgumentError] value outside the unsigned 64-bit integer range
|
34
|
+
#
|
35
|
+
# @return [String] serialized vint64 value
|
36
|
+
def self.encode(value)
|
37
|
+
raise TypeError, "value must be an Integer" unless value.is_a?(Integer)
|
38
|
+
raise ArgumentError, "value must be zero or greater" if value < 0
|
39
|
+
raise ArgumentError, "value must be in the 64-bit unsigned range" if value > MAX
|
40
|
+
|
41
|
+
length = 1
|
42
|
+
result = (value << 1) | 1
|
43
|
+
max = 1 << 7
|
44
|
+
|
45
|
+
while value >= max
|
46
|
+
# 9-byte special case
|
47
|
+
return [0, value].pack("CQ<") if length == 8
|
48
|
+
|
49
|
+
result <<= 1
|
50
|
+
max <<= 7
|
51
|
+
length += 1
|
52
|
+
end
|
53
|
+
|
54
|
+
[result].pack("Q<")[0, length].force_encoding(Encoding::BINARY)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Decode a vint64-serialized value into an unsignedinteger
|
58
|
+
#
|
59
|
+
# @param input [String] serialized vint64 to decode
|
60
|
+
#
|
61
|
+
# @raise [TypeError] non-String input given
|
62
|
+
# @raise [ArgumentError] empty input given
|
63
|
+
#
|
64
|
+
# @return [Array<Integer, String>] decoded integer and remaining data
|
65
|
+
def self.decode(input)
|
66
|
+
raise TypeError, "input must be a String" unless input.is_a?(String)
|
67
|
+
raise ArgumentError, "input cannot be empty" if input.empty?
|
68
|
+
|
69
|
+
prefix = input.getbyte(0)
|
70
|
+
input_len = input.bytesize
|
71
|
+
|
72
|
+
# 9-byte special case
|
73
|
+
if prefix.zero?
|
74
|
+
raise TruncatedMessageError, "not enough bytes to decode varint" if input_len < 9
|
75
|
+
length = 9
|
76
|
+
result = decode_le64(input[1, 8])
|
77
|
+
else
|
78
|
+
# Count trailing zeroes
|
79
|
+
length = CTZ_TABLE[prefix] + 1
|
80
|
+
result = decode_le64(input[0, length]) >> length
|
81
|
+
raise TruncatedMessageError, "not enough bytes to decode varint" if input_len < length
|
82
|
+
end
|
83
|
+
|
84
|
+
if length > 1 && result < (1 << (7 * (length - 1)))
|
85
|
+
raise ParseError, "malformed varint"
|
86
|
+
end
|
87
|
+
|
88
|
+
[result, input.byteslice(length, input_len - length)]
|
89
|
+
end
|
90
|
+
|
91
|
+
class << self
|
92
|
+
private
|
93
|
+
|
94
|
+
# Decode a little endian integer (without allocating memory, unlike pack)
|
95
|
+
def decode_le64(bytes)
|
96
|
+
result = 0
|
97
|
+
|
98
|
+
(bytes.bytesize - 1).downto(0) do |i|
|
99
|
+
result = (result << 8) | bytes.getbyte(i)
|
100
|
+
end
|
101
|
+
|
102
|
+
result
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "digest"
|
4
|
+
|
5
|
+
module Veriform
|
6
|
+
# Computes astructured hash of a Veriform message
|
7
|
+
class Zhash
|
8
|
+
# One character "tag" values used to separate zhash domains
|
9
|
+
module Tags
|
10
|
+
# "Objects" represent Veriform messages
|
11
|
+
OBJECT = "O"
|
12
|
+
|
13
|
+
# 8-bit clean binary data
|
14
|
+
BINARY = "d"
|
15
|
+
|
16
|
+
# 64-bit unsigned integers
|
17
|
+
UINT64 = "u"
|
18
|
+
end
|
19
|
+
|
20
|
+
# By default we compute zhashes using SHA-256
|
21
|
+
DEFAULT_HASH_ALGORITHM = Digest::SHA256
|
22
|
+
|
23
|
+
# Calculate the zhash digest of the given object
|
24
|
+
#
|
25
|
+
# @param algorithm [Class] a class which behaves like a `Digest`
|
26
|
+
#
|
27
|
+
# @return [String] bytestring containing the resulting digest
|
28
|
+
def self.digest(object, algorithm: DEFAULT_HASH_ALGORITHM)
|
29
|
+
new(algorithm: algorithm).digest(object)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Calculate an object's zhash digest, hex encoding the result.
|
33
|
+
# Takes the same parameters as `Veriform::Zhash.digest`
|
34
|
+
#
|
35
|
+
# @return [String] hex encoded string containing the resulting digest
|
36
|
+
def self.hexdigest(object, **args)
|
37
|
+
digest(object, **args).unpack("H*").first
|
38
|
+
end
|
39
|
+
|
40
|
+
# Create a new `Zhash` instance
|
41
|
+
#
|
42
|
+
# @param algorithm [Class] a class which behaves like a `Digest` (i.e. implements `reset`, `update`, `digest`)
|
43
|
+
#
|
44
|
+
# @return [Veriform::Zhash]
|
45
|
+
def initialize(algorithm: DEFAULT_HASH_ALGORITHM)
|
46
|
+
@algorithm = algorithm
|
47
|
+
@hasher = algorithm.new
|
48
|
+
end
|
49
|
+
|
50
|
+
# Compute the zhash of any object allowed in a Veriform message
|
51
|
+
#
|
52
|
+
# @param object [Veriform::Object, String, Integer] object to compute a Zhash from
|
53
|
+
#
|
54
|
+
# @return [String] bytestring containing the resulting digest
|
55
|
+
def digest(object)
|
56
|
+
case object
|
57
|
+
when Veriform::Object, Hash then object_digest(object)
|
58
|
+
when String then binary_digest(object)
|
59
|
+
when Integer then uint64_digest(object)
|
60
|
+
else raise TypeError, "can't compute zhash of #{object.class}"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# Calculate an object's zhash digest, hex encoding the result.
|
65
|
+
# Takes the same parameters as `Veriform::Zhash#digest`
|
66
|
+
#
|
67
|
+
# @return [String] hex encoded string containing the resulting digest
|
68
|
+
def hexdigest(object)
|
69
|
+
digest(object).unpack("H*").first
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
# Compute digest of a `Veriform::Object`
|
75
|
+
def object_digest(message)
|
76
|
+
hasher = @algorithm.new
|
77
|
+
hasher.update Tags::OBJECT
|
78
|
+
|
79
|
+
message.keys.sort.each do |key|
|
80
|
+
hasher.update(encode_uint64(key))
|
81
|
+
hasher.update(digest(message[key]))
|
82
|
+
end
|
83
|
+
|
84
|
+
hasher.digest
|
85
|
+
end
|
86
|
+
|
87
|
+
# Compute digest of a bytestring
|
88
|
+
def binary_digest(bytes)
|
89
|
+
raise EncodingError, "expected BINARY encoding, got #{value.encoding}" unless bytes.encoding == Encoding::BINARY
|
90
|
+
compute_tagged_digest(Tags::BINARY, bytes)
|
91
|
+
end
|
92
|
+
|
93
|
+
# Compute digest of an `Integer`
|
94
|
+
def uint64_digest(value)
|
95
|
+
compute_tagged_digest(Tags::UINT64, encode_uint64(value))
|
96
|
+
end
|
97
|
+
|
98
|
+
# Compute a hash of the given bytes, tweaking the first byte of the
|
99
|
+
# resulting digest with the given domain separator tag.
|
100
|
+
def compute_tagged_digest(tag, bytes)
|
101
|
+
raise ArgumentError, "tag must be 1-byte" if tag.bytesize != 1
|
102
|
+
@hasher.reset
|
103
|
+
@hasher.update(tag)
|
104
|
+
@hasher.update(bytes)
|
105
|
+
@hasher.digest
|
106
|
+
end
|
107
|
+
|
108
|
+
# Encode a uin64 value as little endian
|
109
|
+
#
|
110
|
+
# @param value [Integer] a positive, up-to-64-bit integer value
|
111
|
+
#
|
112
|
+
# @return [String] a bytestring containing a little endian-encoded value
|
113
|
+
def encode_uint64(value)
|
114
|
+
raise TypeError, "expected Integer, got #{value.class}" unless value.is_a?(Integer)
|
115
|
+
raise RangeError, "integer value must be positive" if value < 0
|
116
|
+
raise RangeError, "integer value exceeds 2**64-1" if value > 18_446_744_073_709_551_615
|
117
|
+
[value].pack("Q<")
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
data/veriform.gemspec
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
lib = File.expand_path("../lib", __FILE__)
|
5
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
6
|
+
require "veriform/version"
|
7
|
+
|
8
|
+
Gem::Specification.new do |spec|
|
9
|
+
spec.name = "veriform"
|
10
|
+
spec.version = Veriform::VERSION
|
11
|
+
spec.authors = ["Tony Arcieri"]
|
12
|
+
spec.email = ["bascule@gmail.com"]
|
13
|
+
spec.summary = "Cryptographically verifiable data serialization format inspired by Protocol Buffers"
|
14
|
+
spec.description = <<-DESCRIPTION.strip.gsub(/\s+/, " ")
|
15
|
+
Cryptographically verifiable data serialization format inspired by Protocol Buffers,
|
16
|
+
useful for things like credentials, transparency logs, and blockchain applications.
|
17
|
+
Misuse-resistant symmetric encryption using the AES-SIV (RFC 5297)
|
18
|
+
DESCRIPTION
|
19
|
+
spec.homepage = "https://github.com/zcred/veriform/tree/master/ruby/"
|
20
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
21
|
+
spec.bindir = "exe"
|
22
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
23
|
+
spec.require_paths = ["lib"]
|
24
|
+
|
25
|
+
spec.required_ruby_version = ">= 2.2.2"
|
26
|
+
|
27
|
+
spec.add_development_dependency "bundler", "~> 1.14"
|
28
|
+
end
|
metadata
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: veriform
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Tony Arcieri
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-11-11 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.14'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.14'
|
27
|
+
description: Cryptographically verifiable data serialization format inspired by Protocol
|
28
|
+
Buffers, useful for things like credentials, transparency logs, and blockchain applications.
|
29
|
+
Misuse-resistant symmetric encryption using the AES-SIV (RFC 5297)
|
30
|
+
email:
|
31
|
+
- bascule@gmail.com
|
32
|
+
executables: []
|
33
|
+
extensions: []
|
34
|
+
extra_rdoc_files: []
|
35
|
+
files:
|
36
|
+
- ".gitignore"
|
37
|
+
- ".rspec"
|
38
|
+
- ".rubocop.yml"
|
39
|
+
- ".ruby-version"
|
40
|
+
- Gemfile
|
41
|
+
- Guardfile
|
42
|
+
- README.md
|
43
|
+
- Rakefile
|
44
|
+
- lib/veriform.rb
|
45
|
+
- lib/veriform/decoder.rb
|
46
|
+
- lib/veriform/exceptions.rb
|
47
|
+
- lib/veriform/object.rb
|
48
|
+
- lib/veriform/parser.rb
|
49
|
+
- lib/veriform/varint.rb
|
50
|
+
- lib/veriform/version.rb
|
51
|
+
- lib/veriform/zhash.rb
|
52
|
+
- veriform.gemspec
|
53
|
+
homepage: https://github.com/zcred/veriform/tree/master/ruby/
|
54
|
+
licenses: []
|
55
|
+
metadata: {}
|
56
|
+
post_install_message:
|
57
|
+
rdoc_options: []
|
58
|
+
require_paths:
|
59
|
+
- lib
|
60
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
61
|
+
requirements:
|
62
|
+
- - ">="
|
63
|
+
- !ruby/object:Gem::Version
|
64
|
+
version: 2.2.2
|
65
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - ">="
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
requirements: []
|
71
|
+
rubyforge_project:
|
72
|
+
rubygems_version: 2.6.12
|
73
|
+
signing_key:
|
74
|
+
specification_version: 4
|
75
|
+
summary: Cryptographically verifiable data serialization format inspired by Protocol
|
76
|
+
Buffers
|
77
|
+
test_files: []
|