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 +4 -4
- data/lib/sidetree/cas/fetch_result.rb +20 -0
- data/lib/sidetree/cas/ipfs.rb +101 -0
- data/lib/sidetree/cas.rb +6 -0
- data/lib/sidetree/did.rb +2 -2
- data/lib/sidetree/key.rb +28 -10
- data/lib/sidetree/model/cas_file_base.rb +40 -0
- data/lib/sidetree/model/chunk.rb +21 -0
- data/lib/sidetree/model/chunk_file.rb +72 -0
- data/lib/sidetree/model/core_index_file.rb +197 -0
- data/lib/sidetree/model/core_proof_file.rb +105 -0
- data/lib/sidetree/model/delta.rb +10 -1
- data/lib/sidetree/model/document.rb +6 -0
- data/lib/sidetree/model/provisional_index_file.rb +129 -0
- data/lib/sidetree/model/provisional_proof_file.rb +80 -0
- data/lib/sidetree/model/suffix.rb +2 -2
- data/lib/sidetree/model.rb +7 -0
- data/lib/sidetree/op/create.rb +36 -3
- data/lib/sidetree/op/deactivate.rb +73 -1
- data/lib/sidetree/op/recover.rb +11 -2
- data/lib/sidetree/op/updatable.rb +98 -0
- data/lib/sidetree/op/update.rb +19 -0
- data/lib/sidetree/op.rb +2 -0
- data/lib/sidetree/util/anchored_data_serializer.rb +41 -0
- data/lib/sidetree/util/compressor.rb +38 -0
- data/lib/sidetree/util/jwk.rb +47 -0
- data/lib/sidetree/util/jws.rb +52 -0
- data/lib/sidetree/util.rb +8 -0
- data/lib/sidetree/validator.rb +23 -0
- data/lib/sidetree/version.rb +1 -1
- data/lib/sidetree.rb +18 -1
- data/sidetree.gemspec +2 -1
- metadata +35 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9d82e9b251c67c82450b5e76b413a69a660f8b941799c66692a731621213cd8d
|
4
|
+
data.tar.gz: a0a28978b8dcd21c631e6a1a2e0ff102c96ac22898485bce9e3877e9a558b13d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
data/lib/sidetree/cas.rb
ADDED
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
|
-
|
52
|
-
|
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:
|
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
|
-
# @
|
60
|
-
# @
|
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(
|
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.
|
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
|
-
|
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
|