sidetree 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,50 @@
1
+ module Sidetree
2
+ module Model
3
+ class Service
4
+ MAX_TYPE_LENGTH = 30
5
+
6
+ attr_reader :id # String
7
+ attr_reader :type # String
8
+ attr_reader :endpoint # URI string or JSON object
9
+
10
+ # @raise [Sidetree::Error]
11
+ def initialize(id, type, endpoint)
12
+ Sidetree::Validator.validate_id!(id)
13
+ raise Error, 'type should be String.' unless type.is_a?(String)
14
+ if type.length > MAX_TYPE_LENGTH
15
+ raise Error,
16
+ "Service endpoint type length #{type.length} exceeds max allowed length of #{MAX_TYPE_LENGTH}."
17
+ end
18
+ if endpoint.is_a?(Array)
19
+ raise Error, 'Service endpoint value cannot be an array.'
20
+ end
21
+
22
+ Sidetree::Validator.validate_uri!(endpoint) if endpoint.is_a?(String)
23
+ @id = id
24
+ @type = type
25
+ @endpoint = endpoint
26
+ end
27
+
28
+ # Generate service from json object.
29
+ # @param [Hash] data Hash params.
30
+ # @option data [String] :id id
31
+ # @option data [String] :type type
32
+ # @option data [String || Object] :endpoint endpoint url
33
+ # @raise [Sidetree::Error]
34
+ # @return [Sidetree::Model::Service]
35
+ def self.from_hash(data)
36
+ Service.new(data['id'], data['type'], data['serviceEndpoint'])
37
+ end
38
+
39
+ # Convert data to Hash object.
40
+ # @return [Hash]
41
+ def to_h
42
+ hash = {}
43
+ hash['id'] = id if id
44
+ hash['type'] = type if type
45
+ hash['serviceEndpoint'] = endpoint if endpoint
46
+ hash
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,34 @@
1
+ module Sidetree
2
+ module Model
3
+ class Suffix
4
+ attr_reader :delta_hash, :recovery_commitment
5
+
6
+ # @param [String] delta_hash Base64 encoded delta hash.
7
+ # @param [String] recovery_commitment Base64 encoded recovery commitment.
8
+ def initialize(delta_hash, recovery_commitment)
9
+ @delta_hash = delta_hash
10
+ @recovery_commitment = recovery_commitment
11
+ end
12
+
13
+ # Generate Suffix object from Hash object.
14
+ # @return [Sidetree::Model::Suffix]
15
+ # @raise [Sidetree::Error]
16
+ def self.parse(object)
17
+ Sidetree::Validator.validate_suffix_data!(object)
18
+ Suffix.new(object[:deltaHash], object[:recoveryCommitment])
19
+ end
20
+
21
+ # Convert data to Hash object.
22
+ # @return [Hash]
23
+ def to_h
24
+ { deltaHash: delta_hash, recoveryCommitment: recovery_commitment }
25
+ end
26
+
27
+ # Calculate unique suffix
28
+ # @return [String] unique suffix
29
+ def unique_suffix
30
+ Sidetree.to_hash(to_h)
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,8 @@
1
+ module Sidetree
2
+ module Model
3
+ autoload :Suffix, 'sidetree/model/suffix'
4
+ autoload :Delta, 'sidetree/model/delta'
5
+ autoload :Document, 'sidetree/model/document'
6
+ autoload :Service, 'sidetree/model/service'
7
+ end
8
+ end
@@ -0,0 +1,11 @@
1
+ module Sidetree
2
+ module OP
3
+ class Base
4
+ # Return operation type.
5
+ # @return [String] see Sidetree::OP::Type
6
+ def type
7
+ raise NotImplementedError
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,69 @@
1
+ module Sidetree
2
+ module OP
3
+ # Create operation class.
4
+ class Create < Base
5
+ attr_reader :suffix, :delta
6
+
7
+ # @param [Sidetree::Model::Suffix] suffix
8
+ # @param [Sidetree::Model::Delta] delta
9
+ def initialize(suffix, delta)
10
+ @delta = delta
11
+ @suffix = suffix
12
+ end
13
+
14
+ def type
15
+ Sidetree::OP::Type::CREATE
16
+ end
17
+
18
+ # Check whether suffix's delta_hash equal to hash of delta.
19
+ # @return [Boolean] result
20
+ def match_delta_hash?
21
+ suffix.delta_hash == delta.to_hash
22
+ end
23
+
24
+ # @return [Sidetree::OP::Create] create operation.
25
+ def self.from_base64(base64_str)
26
+ jcs = Base64.urlsafe_decode64(base64_str)
27
+ begin
28
+ json = JSON.parse(jcs, symbolize_names: true)
29
+
30
+ # validate jcs
31
+ expected_base64 =
32
+ Base64.urlsafe_encode64(json.to_json_c14n, padding: false)
33
+ unless expected_base64 == base64_str
34
+ raise Error, 'Initial state object and JCS string mismatch.'
35
+ end
36
+
37
+ Create.new(
38
+ Sidetree::Model::Suffix.parse(json[:suffixData]),
39
+ Sidetree::Model::Delta.parse(json[:delta])
40
+ )
41
+ rescue JSON::ParserError
42
+ raise Error, 'Long form initial state should be encoded jcs.'
43
+ end
44
+ end
45
+
46
+ def to_h
47
+ { suffixData: suffix.to_h, delta: delta.to_h }
48
+ end
49
+
50
+ # Generate long_suffix for DID.
51
+ # @return [String] Base64 encoded long_suffix.
52
+ def long_suffix
53
+ Base64.urlsafe_encode64(to_h.to_json_c14n, padding: false)
54
+ end
55
+
56
+ # Generate DID
57
+ # @param [String] method DID method.
58
+ # @param [Boolean] include_long
59
+ # @return [String] DID
60
+ def did(method: Sidetree::Params::DEFAULT_METHOD, include_long: false)
61
+ did = "did:#{method}"
62
+ did += ":#{Sidetree::Params.network}" if Sidetree::Params.testnet?
63
+ did += ":#{suffix.unique_suffix}"
64
+ did += ":#{long_suffix}" if include_long
65
+ did
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,37 @@
1
+ module Sidetree
2
+ module OP
3
+ module Type
4
+ CREATE = 'create'
5
+ UPDATE = 'update'
6
+ RECOVER = 'recover'
7
+ DEACTIVATE = 'deactivate'
8
+ end
9
+
10
+ # Sidetree patch actions. These are the valid values in the action property of a patch.
11
+ module PatchAction
12
+ REPLACE = 'replace'
13
+ ADD_PUBLIC_KEYS = 'add-public-keys'
14
+ REMOVE_PUBLIC_KEYS = 'remove-public-keys'
15
+ ADD_SERVICES = 'add-services'
16
+ REMOVE_SERVICES = 'remove-services'
17
+ end
18
+
19
+ # DID Document public key purpose.
20
+ module PublicKeyPurpose
21
+ AUTHENTICATION = 'authentication'
22
+ ASSERTION_METHOD = 'assertionMethod'
23
+ CAPABILITY_INVOCATION = 'capabilityInvocation'
24
+ CAPABILITY_DELEGATION = 'capabilityDelegation'
25
+ KEY_AGREEMENT = 'keyAgreement'
26
+
27
+ module_function
28
+
29
+ def values
30
+ PublicKeyPurpose.constants.map { |c| PublicKeyPurpose.const_get(c) }
31
+ end
32
+ end
33
+
34
+ autoload :Base, 'sidetree/op/base'
35
+ autoload :Create, 'sidetree/op/create'
36
+ end
37
+ end
@@ -0,0 +1,250 @@
1
+ module Sidetree
2
+ module Validator
3
+ module_function
4
+
5
+ # @param [Hash] delta delta object.
6
+ # @return [Sidetree::Error]
7
+ def validate_delta!(delta)
8
+ raise Error, 'Delta does not defined.' unless delta
9
+ delta_size = delta.to_json_c14n.bytesize
10
+ if delta_size > Sidetree::Params::MAX_DELTA_SIZE
11
+ raise Error,
12
+ "#{delta_size} bytes of 'delta' exceeded limit of #{Sidetree::Params::MAX_DELTA_SIZE} bytes."
13
+ end
14
+
15
+ if delta.instance_of?(Array)
16
+ raise Error, 'Delta object cannot be an array.'
17
+ end
18
+ delta.keys.each do |k|
19
+ unless %w[patches updateCommitment].include?(k.to_s)
20
+ raise Error, "Property '#{k}' is not allowed in delta object."
21
+ end
22
+ end
23
+
24
+ unless delta[:patches].instance_of?(Array)
25
+ raise Error, 'Patches object in delta must be an array.'
26
+ end
27
+ delta[:patches].each { |p| validate_patch!(p) }
28
+
29
+ validate_encoded_multi_hash!(delta[:updateCommitment], 'updateCommitment')
30
+ end
31
+
32
+ # @param [Hash] patch patch object.
33
+ # @raise [Sidetree::Error]
34
+ def validate_patch!(patch)
35
+ case patch[:action]
36
+ when OP::PatchAction::REPLACE
37
+ validate_document!(patch[:document])
38
+ when OP::PatchAction::ADD_PUBLIC_KEYS
39
+ validate_add_public_keys_patch!(patch)
40
+ when OP::PatchAction::REMOVE_PUBLIC_KEYS
41
+ validate_remove_public_keys_patch!(patch)
42
+ when OP::PatchAction::ADD_SERVICES
43
+ validate_add_services_patch!(patch)
44
+ when OP::PatchAction::REMOVE_SERVICES
45
+ validate_remove_services_patch!(patch)
46
+ else
47
+ raise Error, "#{patch[:action]} is unknown patch action."
48
+ end
49
+ end
50
+
51
+ def validate_document!(document)
52
+ raise Error, 'Document object missing in patch object' unless document
53
+ document.keys.each do |k|
54
+ unless %w[publicKeys services].include?(k.to_s)
55
+ raise Error, "Property '#{k}' is not allowed in document object."
56
+ end
57
+ end
58
+ validate_public_keys!(document[:publicKeys]) if document[:publicKeys]
59
+ validate_services!(document[:services]) if document[:services]
60
+ end
61
+
62
+ def validate_add_public_keys_patch!(patch)
63
+ unless patch.keys.length == 2
64
+ raise Error, 'Patch missing or unknown property.'
65
+ end
66
+ validate_public_keys!(patch[:publicKeys])
67
+ end
68
+
69
+ def validate_remove_public_keys_patch!(patch)
70
+ patch.keys.each do |k|
71
+ unless %w[action ids].include?(k.to_s)
72
+ raise Error, "Unexpected property '#{k}' in remove-public-keys patch."
73
+ end
74
+ end
75
+ unless patch[:ids].instance_of?(Array)
76
+ raise Error, 'Patch public key ids not an array.'
77
+ end
78
+
79
+ patch[:ids].each { |id| validate_id!(id) }
80
+ end
81
+
82
+ def validate_add_services_patch!(patch)
83
+ unless patch.keys.length == 2
84
+ raise Error, 'Patch missing or unknown property.'
85
+ end
86
+ unless patch[:services].instance_of?(Array)
87
+ raise Error, 'Patch services not an array.'
88
+ end
89
+ validate_services!(patch[:services])
90
+ end
91
+
92
+ def validate_remove_services_patch!(patch)
93
+ patch.keys.each do |k|
94
+ unless %w[action ids].include?(k.to_s)
95
+ raise Error, "Unexpected property '#{k}' in remove-services patch."
96
+ end
97
+ end
98
+ unless patch[:ids].instance_of?(Array)
99
+ raise Error, 'Patch service ids not an array.'
100
+ end
101
+
102
+ patch[:ids].each { |id| validate_id!(id) }
103
+ end
104
+
105
+ def validate_public_keys!(public_keys)
106
+ unless public_keys.instance_of?(Array)
107
+ raise Error, 'publicKeys must be an array.'
108
+ end
109
+ pubkey_ids = []
110
+ public_keys.each do |public_key|
111
+ public_key.keys.each do |k|
112
+ unless %w[id type purposes publicKeyJwk].include?(k.to_s)
113
+ raise Error, "Property '#{k}' is not allowed in publicKeys object."
114
+ end
115
+ end
116
+ if public_key[:publicKeyJwk].instance_of?(Array)
117
+ raise Error, 'publicKeyJwk object cannot be an array.'
118
+ end
119
+ if public_key[:type] && !public_key[:type].is_a?(String)
120
+ raise Error, "Public key type #{public_key[:type]} is incorrect."
121
+ end
122
+
123
+ validate_id!(public_key[:id])
124
+
125
+ if pubkey_ids.include?(public_key[:id])
126
+ raise Error, 'Public key id is duplicated.'
127
+ end
128
+ pubkey_ids << public_key[:id]
129
+
130
+ if public_key[:purposes]
131
+ unless public_key[:purposes].instance_of?(Array)
132
+ raise Error, 'purposes must be an array.'
133
+ end
134
+ unless public_key[:purposes].count == public_key[:purposes].uniq.count
135
+ raise Error, 'purpose is duplicated.'
136
+ end
137
+ public_key[:purposes].each do |purpose|
138
+ unless OP::PublicKeyPurpose.values.include?(purpose)
139
+ raise Error, "purpose #{} is invalid."
140
+ end
141
+ end
142
+ end
143
+ end
144
+ end
145
+
146
+ def validate_services!(services)
147
+ unless services.instance_of?(Array)
148
+ raise Error, 'services must be an array.'
149
+ end
150
+
151
+ service_ids = []
152
+ services.each do |service|
153
+ unless service.keys.length == 3
154
+ raise Error, 'Service has missing or unknown property.'
155
+ end
156
+
157
+ validate_id!(service[:id])
158
+
159
+ if service_ids.include?(service[:id])
160
+ raise Error, 'Service id has to be unique.'
161
+ end
162
+ service_ids << service[:id]
163
+
164
+ unless service[:type].is_a?(String)
165
+ raise Error, "Service type #{service[:type]} is incorrect."
166
+ end
167
+ raise Error, 'Service type too long.' if service[:type].length > 30
168
+
169
+ endpoint = service[:serviceEndpoint]
170
+ if endpoint.instance_of?(String)
171
+ validate_uri!(endpoint)
172
+ elsif endpoint.instance_of?(Hash)
173
+
174
+ else
175
+ raise Error, 'ServiceEndpoint must be string or object.'
176
+ end
177
+ end
178
+ end
179
+
180
+ def valid_base64_encoding?(base64)
181
+ /^[A-Za-z0-9_-]+$/.match?(base64)
182
+ end
183
+
184
+ # Validate uri
185
+ # @param [String] uri uri
186
+ # @return [Sidetree::Error] Occurs if it is an incorrect URI
187
+ def validate_uri!(uri)
188
+ begin
189
+ URI.parse(uri)
190
+ unless uri =~ /\A#{URI.regexp(%w[http https])}\z/
191
+ raise Error, "URI string '#{uri}' is not a valid URI."
192
+ end
193
+ rescue StandardError
194
+ raise Error, "URI string '#{uri}' is not a valid URI."
195
+ end
196
+ end
197
+
198
+ def validate_id!(id)
199
+ raise Error, 'id does not string.' unless id.instance_of?(String)
200
+ raise Error, 'id is too long.' if id.length > 50
201
+ unless valid_base64_encoding?(id)
202
+ raise Error, 'id does not use base64url character set.'
203
+ end
204
+ end
205
+
206
+ def validate_encoded_multi_hash!(multi_hash, target)
207
+ begin
208
+ decoded = Multihashes.decode(Base64.urlsafe_decode64(multi_hash))
209
+ unless Params::HASH_ALGORITHM.include?(decoded[:code])
210
+ raise Error,
211
+ "Given #{target} uses unsupported multihash algorithm with code #{decoded[:code]}."
212
+ end
213
+ rescue StandardError
214
+ raise Error,
215
+ "Given #{target} string '#{multi_hash}' is not a multihash."
216
+ end
217
+ end
218
+
219
+ def validate_did_type!(type)
220
+ return unless type
221
+ raise Error, 'DID type must be a string.' unless type.instance_of?(String)
222
+ if type.length > 4
223
+ raise Error,
224
+ "DID type string '#{type}' cannot be longer than 4 characters."
225
+ end
226
+ unless valid_base64_encoding?(type)
227
+ raise Error,
228
+ "DID type string '#{type}' contains a non-Base64URL character."
229
+ end
230
+ end
231
+
232
+ def validate_suffix_data!(suffix)
233
+ if suffix.instance_of?(Array)
234
+ raise Error, 'Suffix data can not be an array.'
235
+ end
236
+ suffix.keys.each do |k|
237
+ unless %w[deltaHash recoveryCommitment type].include?(k.to_s)
238
+ raise Error, "Property '#{k}' is not allowed in publicKeys object."
239
+ end
240
+ end
241
+ validate_encoded_multi_hash!(suffix[:deltaHash], 'delta hash')
242
+ validate_encoded_multi_hash!(
243
+ suffix[:recoveryCommitment],
244
+ 'recovery commitment'
245
+ )
246
+ validate_encoded_multi_hash!(suffix[:deltaHash], 'delta hash')
247
+ validate_did_type!(suffix[:type])
248
+ end
249
+ end
250
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sidetree
4
+ VERSION = '0.1.0'
5
+ end
data/lib/sidetree.rb ADDED
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'sidetree/version'
4
+ require 'ecdsa'
5
+ require 'json/jwt'
6
+ require 'base64'
7
+ require 'json'
8
+ require 'json/canonicalization'
9
+ require 'uri'
10
+ require 'multihashes'
11
+
12
+ module Sidetree
13
+ class Error < StandardError
14
+ end
15
+
16
+ autoload :Key, 'sidetree/key'
17
+ autoload :DID, 'sidetree/did'
18
+ autoload :Model, 'sidetree/model'
19
+ autoload :OP, 'sidetree/op'
20
+ autoload :Validator, 'sidetree/validator'
21
+
22
+ module Params
23
+ # Algorithm for generating hashes of protocol-related values. 0x12 = sha2-256
24
+ HASH_ALGORITHM = [0x12]
25
+ HASH_ALGORITH_STRING = 'sha2-256'
26
+
27
+ # Maximum canonicalized operation delta buffer size.
28
+ MAX_DELTA_SIZE = 1000
29
+
30
+ # Default DID method
31
+ DEFAULT_METHOD = 'sidetree'
32
+
33
+ # Supported did methods.
34
+ METHODS = { default: DEFAULT_METHOD, ion: 'ion' }
35
+
36
+ @network = nil
37
+
38
+ def self.network=(network)
39
+ @network = network
40
+ end
41
+
42
+ def self.network
43
+ @network
44
+ end
45
+
46
+ def self.testnet?
47
+ @network == Network::TESTNET
48
+ end
49
+
50
+ module Network
51
+ MAINNET = 'mainnet'
52
+ TESTNET = 'test'
53
+ end
54
+ end
55
+
56
+ module_function
57
+
58
+ # Calculate Base64 encoded hash of hash object.
59
+ # @param [Hash] data
60
+ # @return [String] Base64 encoded hash value
61
+ def to_hash(data)
62
+ digest = Digest::SHA256.digest(data.is_a?(Hash) ? data.to_json_c14n : data)
63
+ hash = Multihashes.encode(digest, Params::HASH_ALGORITH_STRING)
64
+
65
+ # TODO Need to decide on what hash algorithm to use when hashing suffix data
66
+ # - https://github.com/decentralized-identity/sidetree/issues/965
67
+ Base64.urlsafe_encode64(hash, padding: false)
68
+ end
69
+ end
data/sidetree.gemspec ADDED
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/sidetree/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'sidetree'
7
+ spec.version = Sidetree::VERSION
8
+ spec.authors = ['azuchi']
9
+ spec.email = ['azuchi@chaintope.com']
10
+
11
+ spec.summary = 'Ruby implementation for Sidetree protocol.'
12
+ spec.description = 'Ruby implementation for Sidetree protocol.'
13
+ spec.homepage = 'https://github.com/azuchi/sidetreerb'
14
+ spec.license = 'MIT'
15
+ spec.required_ruby_version = '>= 2.6.0'
16
+
17
+ spec.metadata['homepage_uri'] = spec.homepage
18
+ spec.metadata['source_code_uri'] = spec.homepage
19
+ spec.metadata['changelog_uri'] = spec.homepage
20
+
21
+ # Specify which files should be added to the gem when it is released.
22
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
23
+ spec.files =
24
+ Dir.chdir(File.expand_path(__dir__)) do
25
+ `git ls-files -z`.split("\x0").reject do |f|
26
+ f.match(%r{\A(?:test|spec|features)/})
27
+ end
28
+ end
29
+ spec.bindir = 'exe'
30
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
31
+ spec.require_paths = ['lib']
32
+
33
+ # Uncomment to register a new dependency of your gem
34
+ spec.add_dependency 'ecdsa', '~> 1.2.0'
35
+ spec.add_dependency 'json-jwt', '~> 1.13.0'
36
+ spec.add_dependency 'json-canonicalization', '~> 0.3.0'
37
+ spec.add_dependency 'multihashes', '~> 0.2.0'
38
+ spec.add_runtime_dependency 'thor'
39
+
40
+ # For more information and examples about making a new gem, checkout our
41
+ # guide at: https://bundler.io/guides/creating_gem.html
42
+ spec.add_development_dependency 'rspec', '~> 3.0'
43
+ spec.add_development_dependency 'prettier'
44
+ end