sidetree 0.1.1 → 0.1.4

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: 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