sidetree 0.1.1 → 0.1.4

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: 5a4b7ce3f8ab94c646d21e2083664437b75877ee43264b172fdcadd8f8c9fffc
4
- data.tar.gz: 3ed1dee9aaba4e13f3122d57f9b875c5279c2148a3cbf1e6818d2073e9d7759e
3
+ metadata.gz: 9d82e9b251c67c82450b5e76b413a69a660f8b941799c66692a731621213cd8d
4
+ data.tar.gz: a0a28978b8dcd21c631e6a1a2e0ff102c96ac22898485bce9e3877e9a558b13d
5
5
  SHA512:
6
- metadata.gz: 4d87cd2fae36188a3e24d37f73f99637a6a55ef8d2e300261f46595811194d2cbccc2ea9b6da3ad77564f3312ce2ee51561814a9cf02c261031b35438b2df0d5
7
- data.tar.gz: f9fbb814e7536a672f3f12b2c83fbf9af4f5072a0ee210e9935ce8b627972d659d28d36df9f7425e5b3c44edd7589a8fec7b68cb1b1d34c2af711c3eedd02316
6
+ metadata.gz: cc74695690231772786246306b93d0a674ee2da899cd953edcef91997bc778acab7d624a817e0274a0c1157ee1abe48e00d4e00626a818c485141216947b01dc
7
+ data.tar.gz: 678b4c0013e26cdca615f48669b373d82ff2d669be9b8190680b75704a68b1c0476f73c75d9aa3dca362b2e0f9131aa8f04fc8d6d59c45d7d084d2038be7744b
@@ -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
@@ -48,8 +48,8 @@ module Sidetree
48
48
  raise Error, "recovery_key must be Sidetree::Key instance."
49
49
  end
50
50
 
51
- patches = [{ action: OP::PatchAction::REPLACE, document: document.to_h }]
52
- delta = Model::Delta.new(patches, update_key.to_commitment)
51
+ delta =
52
+ Model::Delta.new([document.to_replace_patch], update_key.to_commitment)
53
53
  suffix =
54
54
  Sidetree::Model::Suffix.new(delta.to_hash, recovery_key.to_commitment)
55
55
  DID.new(
data/lib/sidetree/key.rb CHANGED
@@ -12,7 +12,7 @@ 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)
@@ -56,21 +56,26 @@ 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)
78
+ def self.from_jwk(data)
74
79
  key_data = data["publicKeyJwk"] ? data["publicKeyJwk"] : data
75
80
  key_type = key_data["kty"]
76
81
  curve = key_data["crv"]
@@ -123,10 +128,11 @@ 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(
135
+ Sidetree::Util::JWK.parse(
130
136
  kty: "EC",
131
137
  crv: "secp256k1",
132
138
  x:
@@ -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
@@ -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
@@ -0,0 +1,197 @@
1
+ module Sidetree
2
+ module Model
3
+ # https://identity.foundation/sidetree/spec/#core-index-file
4
+ class CoreIndexFile < CASFileBase
5
+ attr_reader :core_proof_file_uri
6
+ attr_reader :provisional_index_file_uri
7
+ attr_reader :writer_lock_id
8
+ attr_reader :create_ops
9
+ attr_reader :recover_ops
10
+ attr_reader :deactivate_ops
11
+
12
+ # @param [Array[Sidetree::OP::Create]] create_ops
13
+ # @param [Array[Sidetree::OP::Recover]] recover_ops
14
+ # @param [Array[Sidetree::OP::Deactivate]] deactivate_ops
15
+ # @param [String] provisional_index_file_uri
16
+ # @param [String] core_proof_file_uri
17
+ # @raise [Sidetree::Error]
18
+ def initialize(
19
+ create_ops: [],
20
+ recover_ops: [],
21
+ deactivate_ops: [],
22
+ provisional_index_file_uri: nil,
23
+ core_proof_file_uri: nil,
24
+ writer_lock_id: nil
25
+ )
26
+ (create_ops + recover_ops + deactivate_ops).each do |operation|
27
+ unless operation.is_a?(Sidetree::OP::Base)
28
+ raise Sidetree::Error, "Invalid operation class specified."
29
+ end
30
+ end
31
+ @create_ops = create_ops
32
+ @recover_ops = recover_ops
33
+ @deactivate_ops = deactivate_ops
34
+ @provisional_index_file_uri = provisional_index_file_uri
35
+ @core_proof_file_uri = core_proof_file_uri
36
+ @writer_lock_id = writer_lock_id
37
+ unless did_suffixes.length == did_suffixes.uniq.length
38
+ raise Sidetree::Error,
39
+ "Core index file multiple operations for the same DID"
40
+ end
41
+ if writer_lock_id &&
42
+ writer_lock_id.bytesize > Sidetree::Params::MAX_WRITER_LOCK_ID_SIZE
43
+ raise Sidetree::Error,
44
+ "Writer lock ID of #{writer_lock_id.bytesize} bytes exceeded the maximum size of #{Sidetree::Params::MAX_WRITER_LOCK_ID_SIZE} bytes"
45
+ end
46
+ if provisional_index_file_uri
47
+ Validator.validate_cas_file_uri!(
48
+ provisional_index_file_uri,
49
+ "provisional index file URI"
50
+ )
51
+ else
52
+ if (create_ops.length + recover_ops.length) > 0
53
+ raise Sidetree::Error, "Provisional index file uri missing"
54
+ end
55
+ end
56
+ if recover_ops.length > 0 || deactivate_ops.length > 0
57
+ Validator.validate_cas_file_uri!(
58
+ core_proof_file_uri,
59
+ "core proof file URI"
60
+ )
61
+ else
62
+ if core_proof_file_uri
63
+ raise Sidetree::Error,
64
+ "Core proof file is specified but there is no recover and no deactivate operation"
65
+ end
66
+ end
67
+ end
68
+
69
+ # Parse core index file.
70
+ # @param [String] index_data core index file data.
71
+ # @param [Boolean] compressed Whether the index_data is compressed or not, default: true.
72
+ # @return [Sidetree::Model::ProvisionalIndexFile]
73
+ def self.parse(index_data, compressed: true)
74
+ decompressed =
75
+ (
76
+ if compressed
77
+ decompress(index_data, Sidetree::Params::MAX_CORE_INDEX_FILE_SIZE)
78
+ else
79
+ index_data
80
+ end
81
+ )
82
+ begin
83
+ json = JSON.parse(decompressed, symbolize_names: true)
84
+ create_ops, recover_ops, deactivate_ops = [], [], []
85
+ core_proof_uri, provisional_index_uri = nil, nil
86
+ json.each do |k, v|
87
+ case k
88
+ when :provisionalIndexFileUri
89
+ provisional_index_uri = v
90
+ when :coreProofFileUri
91
+ core_proof_uri = v
92
+ when :operations
93
+ create_ops, recover_ops, deactivate_ops = parse_operations(v)
94
+ when :writerLockId
95
+ unless v.is_a?(String)
96
+ raise Sidetree::Error, "Core index file writerLockId not string"
97
+ end
98
+ else
99
+ raise Sidetree::Error,
100
+ "Unexpected property #{k.to_s} in core index file"
101
+ end
102
+ end
103
+ CoreIndexFile.new(
104
+ create_ops: create_ops,
105
+ recover_ops: recover_ops,
106
+ deactivate_ops: deactivate_ops,
107
+ provisional_index_file_uri: provisional_index_uri,
108
+ core_proof_file_uri: core_proof_uri,
109
+ writer_lock_id: json[:writerLockId]
110
+ )
111
+ rescue JSON::ParserError
112
+ raise Sidetree::Error, "Core index file is not json"
113
+ end
114
+ end
115
+
116
+ def self.parse_operations(operations)
117
+ create_ops, recover_ops, deactivate_ops = [], [], []
118
+ operations.each do |o_k, o_v|
119
+ case o_k
120
+ when :create
121
+ unless o_v.is_a?(Array)
122
+ raise Sidetree::Error, "Core index file create property not array"
123
+ end
124
+ create_ops =
125
+ o_v.map do |create|
126
+ Sidetree::OP::Create.from_json(create.to_json_c14n)
127
+ end
128
+ when :recover
129
+ unless o_v.is_a?(Array)
130
+ raise Sidetree::Error,
131
+ "Core index file recover property not array"
132
+ end
133
+ recover_ops =
134
+ o_v.map do |recover|
135
+ Sidetree::OP::Recover.from_json(recover.to_json_c14n)
136
+ end
137
+ when :deactivate
138
+ unless o_v.is_a?(Array)
139
+ raise Sidetree::Error,
140
+ "Core index file deactivate property not array"
141
+ end
142
+ deactivate_ops =
143
+ o_v.map do |deactivate|
144
+ Sidetree::OP::Deactivate.from_json(deactivate.to_json_c14n)
145
+ end
146
+ else
147
+ raise Sidetree::Error,
148
+ "Unexpected property #{o_k.to_s} in operations property in core index file"
149
+ end
150
+ end
151
+ [create_ops, recover_ops, deactivate_ops]
152
+ end
153
+
154
+ private_class_method :parse_operations
155
+
156
+ def did_suffixes
157
+ create_ops.map { |o| o.suffix.unique_suffix } +
158
+ (recover_ops + deactivate_ops).map { |o| o.did_suffix }
159
+ end
160
+
161
+ # Build json string to be stored in CAS.
162
+ # @return [String] json string.
163
+ def to_json
164
+ params = {}
165
+ params[
166
+ :provisionalIndexFileUri
167
+ ] = provisional_index_file_uri if provisional_index_file_uri
168
+ operations = {}
169
+ unless create_ops.empty?
170
+ operations[:create] = create_ops.map do |create|
171
+ { suffixData: create.suffix.to_h }
172
+ end
173
+ end
174
+ unless recover_ops.empty?
175
+ operations[:recover] = recover_ops.map do |recover|
176
+ {
177
+ didSuffix: recover.did_suffix,
178
+ revealValue: recover.revealed_value
179
+ }
180
+ end
181
+ end
182
+ unless deactivate_ops.empty?
183
+ operations[:deactivate] = deactivate_ops.map do |deactivate|
184
+ {
185
+ didSuffix: deactivate.did_suffix,
186
+ revealValue: deactivate.revealed_value
187
+ }
188
+ end
189
+ end
190
+ params[:operations] = operations
191
+ params[:coreProofFileUri] = core_proof_file_uri if core_proof_file_uri
192
+ params[:writerLockId] = writer_lock_id if writer_lock_id
193
+ params.to_json
194
+ end
195
+ end
196
+ end
197
+ end
@@ -0,0 +1,105 @@
1
+ module Sidetree
2
+ module Model
3
+ # https://identity.foundation/sidetree/spec/#core-proof-file
4
+ class CoreProofFile < CASFileBase
5
+ attr_reader :recover_proofs
6
+ attr_reader :deactivate_proofs
7
+
8
+ def initialize(recover_proofs: [], deactivate_proofs: [])
9
+ @recover_proofs = recover_proofs
10
+ @deactivate_proofs = deactivate_proofs
11
+ end
12
+
13
+ # Parse core proof file from compressed data.
14
+ # @param [String] proof_file compressed core proof file.
15
+ # @param [Boolean] compressed Whether the proof_file is compressed or not, default: true.
16
+ # @return [Sidetree::Model::CoreProofFile]
17
+ # @raise [Sidetree::Error]
18
+ def self.parse(proof_file, compressed: true)
19
+ decompressed =
20
+ (
21
+ if compressed
22
+ decompress(proof_file, Sidetree::Params::MAX_PROOF_FILE_SIZE)
23
+ else
24
+ proof_file
25
+ end
26
+ )
27
+ begin
28
+ json = JSON.parse(decompressed, symbolize_names: true)
29
+ recover_proofs, deactivate_proofs = [], []
30
+ json.keys.each do |k|
31
+ unless k == :operations
32
+ raise Sidetree::Error,
33
+ "Unexpected property #{k.to_s} in core proof file"
34
+ end
35
+ end
36
+ unless json[:operations]
37
+ raise Sidetree::Error,
38
+ "Core proof file does not have any operation proofs"
39
+ end
40
+ json[:operations].keys.each do |k|
41
+ unless k == :recover || k == :deactivate
42
+ raise Sidetree::Error,
43
+ "Unexpected property #{k.to_s} in core proof file"
44
+ end
45
+ end
46
+ if json[:operations][:recover]
47
+ unless json[:operations][:recover].is_a?(Array)
48
+ raise Sidetree::Error,
49
+ "Core proof file recover property not array"
50
+ end
51
+ recover_proofs =
52
+ json[:operations][:recover].each.map do |update|
53
+ update.keys.each do |k|
54
+ unless k == :signedData
55
+ raise Sidetree::Error,
56
+ "Unexpected property #{k.to_s} in core proof file"
57
+ end
58
+ end
59
+ Sidetree::Util::JWS.parse(update[:signedData])
60
+ end
61
+ end
62
+ if json[:operations][:deactivate]
63
+ unless json[:operations][:deactivate].is_a?(Array)
64
+ raise Sidetree::Error,
65
+ "Core proof file deactivate property not array"
66
+ end
67
+ deactivate_proofs =
68
+ json[:operations][:deactivate].each.map do |update|
69
+ update.keys.each do |k|
70
+ unless k == :signedData
71
+ raise Sidetree::Error,
72
+ "Unexpected property #{k.to_s} in core proof file"
73
+ end
74
+ end
75
+ Sidetree::Util::JWS.parse(update[:signedData])
76
+ end
77
+ end
78
+ if recover_proofs.length + deactivate_proofs.length == 0
79
+ raise Sidetree::Error, "Core proof file has no proof"
80
+ end
81
+ CoreProofFile.new(
82
+ recover_proofs: recover_proofs,
83
+ deactivate_proofs: deactivate_proofs
84
+ )
85
+ rescue JSON::ParserError
86
+ raise Sidetree::Error, "Core proof file is not json"
87
+ end
88
+ end
89
+
90
+ # Build json string to be stored in CAS.
91
+ # @return [String] json string.
92
+ def to_json
93
+ operations = {}
94
+ operations[:recover] = recover_proofs.map do |u|
95
+ { signedData: u.to_s }
96
+ end unless recover_proofs.empty?
97
+ operations[:deactivate] = deactivate_proofs.map do |u|
98
+ { signedData: u.to_s }
99
+ end unless deactivate_proofs.empty?
100
+ params = { operations: operations }
101
+ params.to_json
102
+ end
103
+ end
104
+ end
105
+ end