sidetree 0.1.0 → 0.1.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 55fc6b6c312458a0e0d627daeb1f1f5cd84e87a6f1b74c0a9a9d584848e31a8a
4
- data.tar.gz: 34a4aa6ee3457c84b0d1148108d19f5e7afea5f32d1210299ca530bdd00d6a59
3
+ metadata.gz: b65f50d961dcfc15f1440ae26225fad38a7b42f43874764005b9d01cc624bb3a
4
+ data.tar.gz: dbe04b5d7072da5b1cbec36b90089d29cb69e6077cc60a501045d0eddff38d92
5
5
  SHA512:
6
- metadata.gz: 47cfc0743c8da5ccf12bae7fa2114474607a0f68814fccf0738390f87bc52855e96b2eaedc70b68328a8b7b0b7dbc892f6eba199a0ba87b842b369e71603d472
7
- data.tar.gz: 0032c7d6faedaf3873fdc1594f19eb3ceb431129cb5fd2dd436e322dfb9b5a155e5901c87c0530e9b42bb6d3ca01562e393c83bbc62f51ec61d4f4c4c37a8c25
6
+ metadata.gz: e7aa774c61569b33aa98de41aa6ab265c58107346704aaebf673e45bc464f250f1073ea70994ae1a884cbd92964dc7e84b1841e2cbfc917601157a2eea1a7854
7
+ data.tar.gz: a68ccd2615046db978b08dcffa585611f2ed44ee59a765e0c53614074892ad83e5f733df493bc8f06a15a4b264c777c52867f486562e13918b5cf4fff236c758
@@ -7,7 +7,7 @@ jobs:
7
7
  strategy:
8
8
  fail-fast: true
9
9
  matrix:
10
- ruby: [2.6, 2.7, 3.0, 3.1]
10
+ ruby: [2.7, 3.0, 3.1]
11
11
  runs-on: ubuntu-latest
12
12
  steps:
13
13
  - uses: actions/checkout@v2
@@ -0,0 +1,20 @@
1
+ module Sidetree
2
+ module CAS
3
+ class FetchResult
4
+ CODE_CAS_NOT_REACHABLE = "cas_not_reachable"
5
+ CODE_INVALID_HASH = "content_hash_invalid"
6
+ CODE_MAX_SIZE_EXCEEDED = "content_exceeds_maximum_allowed_size"
7
+ CODE_NOT_FILE = "content_not_a_file"
8
+ CODE_NOT_FOUND = "content_not_found"
9
+ CODE_SUCCESS = "success"
10
+
11
+ attr_reader :code
12
+ attr_reader :content
13
+
14
+ def initialize(code, content = nil)
15
+ @code = code
16
+ @content = content
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,101 @@
1
+ require "securerandom"
2
+ require "uri"
3
+ require "net/http"
4
+
5
+ module Sidetree
6
+ module CAS
7
+ class IPFS
8
+ attr_reader :base_url
9
+ attr_reader :fetch_timeout
10
+
11
+ # @raise [Sidetree::Error]
12
+ def initialize(
13
+ schema: "http",
14
+ host: "localhost",
15
+ port: 5001,
16
+ base_path: "/api/v0",
17
+ fetch_timeout: nil
18
+ )
19
+ @base_url = "#{schema}://#{host}:#{port}#{base_path}"
20
+ @fetch_timeout = fetch_timeout
21
+ raise Sidetree::Error, "Failed to connect to IPFS endpoint." unless up?
22
+ end
23
+
24
+ # Writes the given content to CAS.
25
+ # @param [String] content content to be stored.
26
+ # @return [String] SHA256 hash in base64url encoding which represents the address of the content.
27
+ # @raise [Sidetree::Error] If IPFS write fails
28
+ def write(content)
29
+ multipart_boundary = SecureRandom.hex(32)
30
+ uri = URI("#{base_url}/add")
31
+ http = Net::HTTP.new(uri.host, uri.port)
32
+ http.use_ssl = uri.scheme == "https"
33
+ req = Net::HTTP::Post.new(uri)
34
+ req[
35
+ "Content-Type"
36
+ ] = "multipart/form-data; boundary=#{multipart_boundary}"
37
+ req["Content-Disposition"] = 'form-data; name=; filename=""'
38
+ req.body = build_body(multipart_boundary, content)
39
+ res = http.request(req)
40
+ if res.is_a?(Net::HTTPSuccess)
41
+ results = JSON.parse(res.body)
42
+ results["Hash"]
43
+ else
44
+ raise Sidetree::Error, "Failed writing content. #{res.body}"
45
+ end
46
+ end
47
+
48
+ # Get content from IPFS.
49
+ # @param [String] addr cas uri.
50
+ # @return [Sidetree::CAS::FetchResult] Fetch result containing the content if found.
51
+ # The result code is set to FetchResultCode.MaxSizeExceeded if the content exceeds the +max_bytesize+.
52
+ def read(addr)
53
+ fetch_url = "#{base_url}/cat?arg=#{addr}"
54
+ begin
55
+ res = Net::HTTP.post_form(URI(fetch_url), {})
56
+ if res.is_a?(Net::HTTPSuccess)
57
+ FetchResult.new(FetchResult::CODE_SUCCESS, res.body)
58
+ else
59
+ FetchResult.new(FetchResult::CODE_NOT_FOUND)
60
+ end
61
+ rescue Errno::ECONNREFUSED
62
+ FetchResult.new(FetchResult::CODE_CAS_NOT_REACHABLE)
63
+ rescue StandardError
64
+ FetchResult.new(FetchResult::CODE_NOT_FOUND)
65
+ end
66
+ end
67
+
68
+ # Get node information from IPFS endpoint.
69
+ # @return [String] node information.
70
+ def id
71
+ res = Net::HTTP.post_form(URI("#{base_url}/id"), {})
72
+ res.body if res.is_a?(Net::HTTPSuccess)
73
+ end
74
+
75
+ # Check IPFS endpoint are up and running.
76
+ # @return [Boolean]
77
+ def up?
78
+ begin
79
+ id
80
+ true
81
+ rescue Errno::ECONNREFUSED
82
+ false
83
+ end
84
+ end
85
+
86
+ private
87
+
88
+ # Fetch content from IPFS.
89
+ def fetch(uri, max_bytesize: nil)
90
+ end
91
+
92
+ def build_body(boundary, content)
93
+ begin_boundary = "--#{boundary}\n"
94
+ first_part_content_type = "Content-Disposition: form-data;\n"
95
+ first_part_content_type += "Content-Type: application/octet-stream\n\n"
96
+ end_boundary = "\n--#{boundary}--"
97
+ begin_boundary + first_part_content_type + content + end_boundary
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,6 @@
1
+ module Sidetree
2
+ module CAS
3
+ autoload :FetchResult, "sidetree/cas/fetch_result"
4
+ autoload :IPFS, "sidetree/cas/ipfs"
5
+ end
6
+ end
data/lib/sidetree/did.rb CHANGED
@@ -6,21 +6,21 @@ module Sidetree
6
6
 
7
7
  # @raise [Sidetree::Error]
8
8
  def initialize(did)
9
- if !did.start_with?('did:ion:') && !did.start_with?('did:sidetree:')
10
- raise Error, 'Expected DID method not given in DID.'
9
+ if !did.start_with?("did:ion:") && !did.start_with?("did:sidetree:")
10
+ raise Error, "Expected DID method not given in DID."
11
11
  end
12
- if did.count(':') > (Sidetree::Params.testnet? ? 4 : 3)
13
- raise Error, 'Unsupported DID format.'
12
+ if did.count(":") > (Sidetree::Params.testnet? ? 4 : 3)
13
+ raise Error, "Unsupported DID format."
14
14
  end
15
15
  if Sidetree::Params.testnet?
16
- _, @method, _, @suffix, @long_suffix = did.split(':')
16
+ _, @method, _, @suffix, @long_suffix = did.split(":")
17
17
  else
18
- _, @method, @suffix, @long_suffix = did.split(':')
18
+ _, @method, @suffix, @long_suffix = did.split(":")
19
19
  end
20
20
 
21
21
  if @long_suffix
22
22
  unless suffix == create_op.suffix.unique_suffix
23
- raise Error, 'DID document mismatches short-form DID.'
23
+ raise Error, "DID document mismatches short-form DID."
24
24
  end
25
25
  end
26
26
  end
@@ -39,19 +39,17 @@ module Sidetree
39
39
  method: Sidetree::Params::DEFAULT_METHOD
40
40
  )
41
41
  unless document.is_a?(Sidetree::Model::Document)
42
- raise Error, 'document must be Sidetree::Model::Document instance.'
42
+ raise Error, "document must be Sidetree::Model::Document instance."
43
43
  end
44
44
  unless update_key.is_a?(Sidetree::Key)
45
- raise Error, 'update_key must be Sidetree::Key instance.'
45
+ raise Error, "update_key must be Sidetree::Key instance."
46
46
  end
47
47
  unless recovery_key.is_a?(Sidetree::Key)
48
- raise Error, 'recovery_key must be Sidetree::Key instance.'
48
+ raise Error, "recovery_key must be Sidetree::Key instance."
49
49
  end
50
50
 
51
- patches = [
52
- { 'action': OP::PatchAction::REPLACE, 'document': document.to_h }
53
- ]
54
- delta = Model::Delta.new(patches, update_key.to_commitment)
51
+ delta =
52
+ Model::Delta.new([document.to_replace_patch], update_key.to_commitment)
55
53
  suffix =
56
54
  Sidetree::Model::Suffix.new(delta.to_hash, recovery_key.to_commitment)
57
55
  DID.new(
data/lib/sidetree/key.rb CHANGED
@@ -12,18 +12,18 @@ module Sidetree
12
12
  public_key: nil,
13
13
  id: nil,
14
14
  purposes: [],
15
- type: nil
15
+ type: Sidetree::Params::DEFAULT_PUBKEY_TYPE
16
16
  )
17
17
  if private_key
18
18
  unless Key.valid_private_key?(private_key)
19
- raise Error, 'private key is invalid range.'
19
+ raise Error, "private key is invalid range."
20
20
  end
21
21
 
22
22
  @private_key = private_key
23
23
  pub = ECDSA::Group::Secp256k1.generator.multiply_by_scalar(private_key)
24
24
  if public_key
25
25
  unless pub == public_key
26
- raise Error, 'Public and private keys do not match.'
26
+ raise Error, "Public and private keys do not match."
27
27
  end
28
28
  else
29
29
  public_key = pub
@@ -31,13 +31,13 @@ module Sidetree
31
31
  end
32
32
 
33
33
  unless public_key
34
- raise Error, 'Specify either the private key or the public key'
34
+ raise Error, "Specify either the private key or the public key"
35
35
  end
36
36
  unless public_key.is_a?(ECDSA::Point)
37
- raise Error, 'public key must be an ECDSA::Point instance.'
37
+ raise Error, "public key must be an ECDSA::Point instance."
38
38
  end
39
39
  unless ECDSA::Group::Secp256k1.valid_public_key?(public_key)
40
- raise Error, 'public key is invalid.'
40
+ raise Error, "public key is invalid."
41
41
  end
42
42
 
43
43
  @public_key = public_key
@@ -56,62 +56,67 @@ module Sidetree
56
56
  end
57
57
 
58
58
  # Generate Secp256k1 key.
59
- # @option [String] id Public key ID.
60
- # @option [String] purpose Purpose for public key. Supported values defined by [Sidetree::PublicKeyPurpose].
59
+ # @param [String] id Public key ID.
60
+ # @param [String] purpose Purpose for public key. Supported values defined by [Sidetree::PublicKeyPurpose].
61
+ # @param [String] type The type of public key defined by https://w3c-ccg.github.io/ld-cryptosuite-registry/.
61
62
  # @return [Sidetree::Key]
62
63
  # @raise [Sidetree::Error]
63
- def self.generate(id: nil, purposes: [])
64
+ def self.generate(
65
+ id: nil,
66
+ purposes: [],
67
+ type: Sidetree::Params::DEFAULT_PUBKEY_TYPE
68
+ )
64
69
  private_key =
65
70
  1 + SecureRandom.random_number(ECDSA::Group::Secp256k1.order - 1)
66
- Key.new(private_key: private_key, purposes: purposes, id: id)
71
+ Key.new(private_key: private_key, purposes: purposes, id: id, type: type)
67
72
  end
68
73
 
69
74
  # Generate key instance from jwk Hash.
70
75
  # @param [Hash] data jwk Hash object.
71
76
  # @return [Sidetree::Key]
72
77
  # @raise [Sidetree::Error]
73
- def self.from_hash(data)
74
- key_data = data['publicKeyJwk'] ? data['publicKeyJwk'] : data
75
- key_type = key_data['kty']
76
- curve = key_data['crv']
77
- if key_type.nil? || key_type != 'EC'
78
+ def self.from_jwk(data)
79
+ key_data = data["publicKeyJwk"] ? data["publicKeyJwk"] : data
80
+ key_type = key_data["kty"]
81
+ curve = key_data["crv"]
82
+ if key_type.nil? || key_type != "EC"
78
83
  raise Error, "Unsupported key type '#{key_type}' specified."
79
84
  end
80
- if curve.nil? || curve != 'secp256k1'
85
+ if curve.nil? || curve != "secp256k1"
81
86
  raise Error, "Unsupported curve '#{curve}' specified."
82
87
  end
83
- raise Error, 'x property required.' unless key_data['x']
84
- raise Error, 'y property required.' unless key_data['y']
88
+ raise Error, "x property required." unless key_data["x"]
89
+ raise Error, "y property required." unless key_data["y"]
85
90
 
86
91
  # `x` and `y` need 43 Base64URL encoded bytes to contain 256 bits.
87
- unless key_data['x'].length == 43
92
+ unless key_data["x"].length == 43
88
93
  raise Error, "Secp256k1 JWK 'x' property must be 43 bytes."
89
94
  end
90
- unless key_data['y'].length == 43
95
+ unless key_data["y"].length == 43
91
96
  raise Error, "Secp256k1 JWK 'y' property must be 43 bytes."
92
97
  end
93
98
 
94
- x = Base64.urlsafe_decode64(key_data['x'])
95
- y = Base64.urlsafe_decode64(key_data['y'])
99
+ x = Base64.urlsafe_decode64(key_data["x"])
100
+ y = Base64.urlsafe_decode64(key_data["y"])
96
101
  point =
97
102
  ECDSA::Format::PointOctetString.decode(
98
- ['04'].pack('H*') + x + y,
103
+ ["04"].pack("H*") + x + y,
99
104
  ECDSA::Group::Secp256k1
100
105
  )
101
106
  private_key =
102
- if key_data['d']
103
- Base64.urlsafe_decode64(key_data['d']).unpack1('H*').to_i(16)
107
+ if key_data["d"]
108
+ Base64.urlsafe_decode64(key_data["d"]).unpack1("H*").to_i(16)
104
109
  else
105
110
  nil
106
111
  end
107
112
 
108
- purposes = data['purposes'] ? data['purposes'] : []
113
+ purposes = data["purposes"] ? data["purposes"] : []
109
114
  Key.new(
110
115
  public_key: point,
111
116
  private_key: private_key,
112
117
  purposes: purposes,
113
- id: data['id'],
114
- type: data['type']
118
+ id: data["id"],
119
+ type: data["type"]
115
120
  )
116
121
  end
117
122
 
@@ -123,12 +128,13 @@ module Sidetree
123
128
  end
124
129
 
125
130
  # Generate JSON::JWK object.
131
+ # @param [Boolean] include_privkey whether include private key or not.
126
132
  # @return [JSON::JWK]
127
- def to_jwk
133
+ def to_jwk(include_privkey: false)
128
134
  jwk =
129
- JSON::JWK.new(
130
- kty: 'EC',
131
- crv: 'secp256k1',
135
+ Sidetree::Util::JWK.parse(
136
+ kty: "EC",
137
+ crv: "secp256k1",
132
138
  x:
133
139
  Base64.urlsafe_encode64(
134
140
  ECDSA::Format::FieldElementOctetString.encode(
@@ -146,18 +152,30 @@ module Sidetree
146
152
  padding: false
147
153
  )
148
154
  )
149
- jwk['d'] = encoded_private_key if private_key
155
+ jwk["d"] = encoded_private_key if include_privkey && private_key
150
156
  jwk
151
157
  end
152
158
 
159
+ # Convert the private key to the format (OpenSSL::PKey::EC) in which it will be signed in JWS.
160
+ # @return [OpenSSL::PKey::EC]
161
+ def jws_sign_key
162
+ return nil unless private_key
163
+ to_jwk(include_privkey: true).to_key
164
+ end
165
+
153
166
  # Generate commitment for this key.
154
167
  # @return [String] Base64 encoded commitment.
155
168
  def to_commitment
156
169
  digest = Digest::SHA256.digest(to_jwk.normalize.to_json_c14n)
157
-
158
170
  Sidetree.to_hash(digest)
159
171
  end
160
172
 
173
+ # Generate reveal value for this key.
174
+ # @return [String] Base64 encoded reveal value.
175
+ def to_reveal_value
176
+ Sidetree.to_hash(to_jwk.normalize.to_json_c14n)
177
+ end
178
+
161
179
  def to_h
162
180
  h = { publicKeyJwk: to_jwk.normalize, purposes: purposes }
163
181
  h[:id] = id if id
@@ -170,7 +188,7 @@ module Sidetree
170
188
  def encoded_private_key
171
189
  if private_key
172
190
  Base64.urlsafe_encode64(
173
- [private_key.to_s(16).rjust(32 * 2, '0')].pack('H*'),
191
+ [private_key.to_s(16).rjust(32 * 2, "0")].pack("H*"),
174
192
  padding: false
175
193
  )
176
194
  else
@@ -0,0 +1,40 @@
1
+ module Sidetree
2
+ module Model
3
+ class CASFileBase
4
+ include Sidetree::Util::Compressor
5
+
6
+ # Decompress +data+.
7
+ # @param [String] data compressed data.
8
+ # @param [Integer] max_size
9
+ # @return [String] decompressed data.
10
+ # @raise [Sidetree::Error]
11
+ def self.decompress(data, max_size)
12
+ begin
13
+ Sidetree::Util::Compressor.decompress(
14
+ data,
15
+ max_bytes:
16
+ max_size *
17
+ Sidetree::Util::Compressor::ESTIMATE_DECOMPRESSION_MULTIPLIER
18
+ )
19
+ rescue Zlib::GzipFile::Error
20
+ raise Sidetree::Error,
21
+ "#{self.name.split("::").last.split(/(?=[A-Z])/).join(" ")} decompression failure"
22
+ end
23
+ end
24
+
25
+ # Build json string to be stored in CAS.
26
+ # Child classes must implement this method.
27
+ # @return [String]
28
+ def to_json
29
+ raise NotImplementedError,
30
+ "You must implement #{self.class}##{__method__}"
31
+ end
32
+
33
+ # Generate compressed data via to_json to be stored in CAS.
34
+ # @return [String] compressed data.
35
+ def to_compress
36
+ compress(to_json)
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,21 @@
1
+ module Sidetree
2
+ module Model
3
+ class Chunk
4
+ attr_reader :chunk_file_uri
5
+
6
+ # Initializer
7
+ # @param [String] chunk_file_uri chunk file uri.
8
+ # @raise [Sidetree::Error]
9
+ def initialize(chunk_file_uri)
10
+ unless chunk_file_uri.is_a?(String)
11
+ raise Sidetree::Error, "chunk_file_uri must be String"
12
+ end
13
+ @chunk_file_uri = chunk_file_uri
14
+ end
15
+
16
+ def to_h
17
+ { chunkFileUri: chunk_file_uri }
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,72 @@
1
+ module Sidetree
2
+ module Model
3
+ # https://identity.foundation/sidetree/spec/#chunk-files
4
+ class ChunkFile < CASFileBase
5
+ attr_reader :deltas # Array of Sidetree::Model::Delta
6
+
7
+ def initialize(deltas = [])
8
+ deltas.each do |delta|
9
+ unless delta.is_a?(Sidetree::Model::Delta)
10
+ raise Sidetree::Error,
11
+ "deltas contains data that is not Sidetree::Model::Delta object."
12
+ end
13
+ end
14
+ @deltas = deltas
15
+ end
16
+
17
+ # Generate chunk file from operations.
18
+ # @param [Array[Sidetree::OP::Create]] create_ops
19
+ # @param [Array[Sidetree::OP::Recover]] recover_ops
20
+ # @param [Array[Sidetree::OP::Update]] update_ops
21
+ def self.create_from_ops(create_ops: [], recover_ops: [], update_ops: [])
22
+ deltas = (create_ops + recover_ops + update_ops).map(&:delta)
23
+ ChunkFile.new(deltas)
24
+ end
25
+
26
+ # Parse chunk file from compressed data.
27
+ # @param [String] chunk_file compressed chunk file data.
28
+ # @param [Boolean] compressed Whether the chunk_file is compressed or not, default: true.
29
+ # @return [Sidetree::Model::ChunkFile]
30
+ # @raise [Sidetree::Error]
31
+ def self.parse(chunk_file, compressed: true)
32
+ decompressed =
33
+ (
34
+ if compressed
35
+ decompress(chunk_file, Sidetree::Params::MAX_CHUNK_FILE_SIZE)
36
+ else
37
+ chunk_file
38
+ end
39
+ )
40
+ json = JSON.parse(decompressed, symbolize_names: true)
41
+ json.keys.each do |k|
42
+ unless k == :deltas
43
+ raise Sidetree::Error,
44
+ "Unexpected property #{k.to_s} in chunk file."
45
+ end
46
+ end
47
+ unless json[:deltas].is_a?(Array)
48
+ raise Sidetree::Error,
49
+ "Invalid chunk file, deltas property is not an array."
50
+ end
51
+ ChunkFile.new(
52
+ json[:deltas].map do |delta|
53
+ Sidetree::Model::Delta.from_object(delta)
54
+ end
55
+ )
56
+ end
57
+
58
+ # Build json string to be stored in CAS.
59
+ # @return [String] json string.
60
+ def to_json
61
+ { deltas: deltas.map(&:to_h) }.to_json
62
+ end
63
+
64
+ # Check if the +other+ object have the same chunk data.
65
+ # @return [Boolean]
66
+ def ==(other)
67
+ return false unless other.is_a?(ChunkFile)
68
+ deltas == other.deltas
69
+ end
70
+ end
71
+ end
72
+ end