sidetree 0.1.0 → 0.1.3

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