pplcdid 1.2.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/AUTHORS +1 -0
- data/LICENSE +201 -0
- data/README.md +2 -0
- data/VERSION +1 -0
- data/lib/ppldid/basic.rb +278 -0
- data/lib/ppldid/didcomm.rb +120 -0
- data/lib/ppldid/log.rb +394 -0
- data/lib/ppldid.rb +739 -0
- data/spec/input/basic/arrays.json +8 -0
- data/spec/input/basic/french.json +6 -0
- data/spec/input/basic/sample2_get_location.doc +1 -0
- data/spec/input/basic/sample2_retrieve_document.doc +1 -0
- data/spec/input/basic/sample3_retrieve_document.doc +1 -0
- data/spec/input/basic/sample4_retrieve_document.doc +1 -0
- data/spec/input/basic/sample5_retrieve_document.doc +1 -0
- data/spec/input/basic/sample_dec.doc +1 -0
- data/spec/input/basic/sample_enc.doc +1 -0
- data/spec/input/basic/sample_get_location.doc +1 -0
- data/spec/input/basic/sample_hash.doc +1 -0
- data/spec/input/basic/sample_invalid2_readkey.doc +1 -0
- data/spec/input/basic/sample_invalid2_verify.doc +1 -0
- data/spec/input/basic/sample_invalid3_readkey.doc +1 -0
- data/spec/input/basic/sample_invalid3_verify.doc +1 -0
- data/spec/input/basic/sample_invalid_privkey.doc +1 -0
- data/spec/input/basic/sample_invalid_readkey.doc +1 -0
- data/spec/input/basic/sample_invalid_sign.doc +1 -0
- data/spec/input/basic/sample_invalid_verify.doc +1 -0
- data/spec/input/basic/sample_key.doc +1 -0
- data/spec/input/basic/sample_readkey.doc +1 -0
- data/spec/input/basic/sample_retrieve_document.doc +1 -0
- data/spec/input/basic/sample_sign.doc +1 -0
- data/spec/input/basic/sample_valid_privkey.doc +1 -0
- data/spec/input/basic/sample_verify.doc +1 -0
- data/spec/input/basic/structures.json +8 -0
- data/spec/input/basic/unicode.json +3 -0
- data/spec/input/basic/values.json +5 -0
- data/spec/input/basic/wierd.json +11 -0
- data/spec/input/basic/zQmaBZTghn.doc +1 -0
- data/spec/input/log/sample0_dag2array.doc +1 -0
- data/spec/input/log/sample0_dag_did.doc +1 -0
- data/spec/input/log/sample1_dag_did.doc +1 -0
- data/spec/input/log/sample1_dag_update.doc +1 -0
- data/spec/input/log/sample2_dag_did.doc +1 -0
- data/spec/input/log/sample2_dag_update.doc +1 -0
- data/spec/input/log/sample2_retrieve_log.doc +1 -0
- data/spec/input/log/sample3_dag_did.doc +1 -0
- data/spec/input/log/sample3_dag_update.doc +1 -0
- data/spec/input/log/sample3_retrieve_log.doc +1 -0
- data/spec/input/log/sample4_dag_did.doc +1 -0
- data/spec/input/log/sample4_dag_update.doc +1 -0
- data/spec/input/log/sample4_retrieve_log.doc +1 -0
- data/spec/input/log/sample5_dag_update.doc +1 -0
- data/spec/input/log/sample5_retrieve_log.doc +1 -0
- data/spec/input/log/sample6_dag_update.doc +1 -0
- data/spec/input/log/sample6_retrieve_log.doc +1 -0
- data/spec/input/log/sample7_dag_update.doc +1 -0
- data/spec/input/log/sample7_retrieve_log.doc +1 -0
- data/spec/input/log/sample8_dag_update.doc +1 -0
- data/spec/input/log/sample_addhash.doc +1 -0
- data/spec/input/log/sample_dag_update.doc +1 -0
- data/spec/input/log/sample_match_log.doc +1 -0
- data/spec/input/log/sample_op1_addhash.doc +1 -0
- data/spec/input/log/sample_retrieve_log.doc +1 -0
- data/spec/input/main/sample0_read.doc +1 -0
- data/spec/output/basic/arrays.json +1 -0
- data/spec/output/basic/french.json +1 -0
- data/spec/output/basic/sample2_get_location.doc +1 -0
- data/spec/output/basic/sample2_retrieve_document.doc +1 -0
- data/spec/output/basic/sample3_retrieve_document.doc +1 -0
- data/spec/output/basic/sample4_retrieve_document.doc +1 -0
- data/spec/output/basic/sample5_retrieve_document.doc +1 -0
- data/spec/output/basic/sample_dec.doc +1 -0
- data/spec/output/basic/sample_enc.doc +1 -0
- data/spec/output/basic/sample_get_location.doc +1 -0
- data/spec/output/basic/sample_hash.doc +1 -0
- data/spec/output/basic/sample_invalid2_readkey.doc +1 -0
- data/spec/output/basic/sample_invalid2_verify.doc +1 -0
- data/spec/output/basic/sample_invalid3_readkey.doc +1 -0
- data/spec/output/basic/sample_invalid3_verify.doc +1 -0
- data/spec/output/basic/sample_invalid_privkey.doc +1 -0
- data/spec/output/basic/sample_invalid_readkey.doc +1 -0
- data/spec/output/basic/sample_invalid_sign.doc +1 -0
- data/spec/output/basic/sample_invalid_verify.doc +1 -0
- data/spec/output/basic/sample_key.doc +1 -0
- data/spec/output/basic/sample_readkey.doc +1 -0
- data/spec/output/basic/sample_retrieve_document.doc +1 -0
- data/spec/output/basic/sample_sign.doc +1 -0
- data/spec/output/basic/sample_valid_privkey.doc +1 -0
- data/spec/output/basic/sample_verify.doc +1 -0
- data/spec/output/basic/structures.json +1 -0
- data/spec/output/basic/unicode.json +1 -0
- data/spec/output/basic/values.json +1 -0
- data/spec/output/basic/wierd.json +1 -0
- data/spec/output/log/sample0_dag2array.doc +1 -0
- data/spec/output/log/sample0_dag_did.doc +1 -0
- data/spec/output/log/sample1_dag_did.doc +1 -0
- data/spec/output/log/sample1_dag_update.doc +1 -0
- data/spec/output/log/sample2_dag_did.doc +1 -0
- data/spec/output/log/sample2_dag_update.doc +1 -0
- data/spec/output/log/sample2_retrieve_log.doc +1 -0
- data/spec/output/log/sample3_dag_did.doc +1 -0
- data/spec/output/log/sample3_dag_update.doc +1 -0
- data/spec/output/log/sample3_retrieve_log.doc +1 -0
- data/spec/output/log/sample4_dag_did.doc +1 -0
- data/spec/output/log/sample4_dag_update.doc +1 -0
- data/spec/output/log/sample4_retrieve_log.doc +1 -0
- data/spec/output/log/sample5_dag_update.doc +1 -0
- data/spec/output/log/sample5_retrieve_log.doc +1 -0
- data/spec/output/log/sample6_dag_update.doc +1 -0
- data/spec/output/log/sample6_retrieve_log.doc +1 -0
- data/spec/output/log/sample7_dag_update.doc +1 -0
- data/spec/output/log/sample7_retrieve_log.doc +1 -0
- data/spec/output/log/sample8_dag_update.doc +1 -0
- data/spec/output/log/sample_addhash.doc +1 -0
- data/spec/output/log/sample_dag_update.doc +1 -0
- data/spec/output/log/sample_match_log.doc +1 -0
- data/spec/output/log/sample_op1_addhash.doc +1 -0
- data/spec/output/log/sample_retrieve_log.doc +1 -0
- data/spec/output/main/sample0_read.doc +1 -0
- data/spec/ppldid_spec.rb +170 -0
- data/spec/spec_helper.rb +31 -0
- metadata +420 -0
data/lib/ppldid.rb
ADDED
@@ -0,0 +1,739 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'dag'
|
5
|
+
require 'jwt'
|
6
|
+
require 'rbnacl'
|
7
|
+
require 'ed25519'
|
8
|
+
require 'httparty'
|
9
|
+
require 'multibases'
|
10
|
+
require 'multihashes'
|
11
|
+
require 'multicodecs'
|
12
|
+
require 'json/canonicalization'
|
13
|
+
require './pplcdid/basic'
|
14
|
+
require './pplcdid/log'
|
15
|
+
require './pplcdid/didcomm'
|
16
|
+
|
17
|
+
class Pplcdid
|
18
|
+
|
19
|
+
# pplcdid-base server
|
20
|
+
|
21
|
+
LOCATION_PREFIX = "@"
|
22
|
+
DEFAULT_LOCATION = "http://pplcdid.peoplecarbon.org:3000"
|
23
|
+
|
24
|
+
# expected DID format: did:pplc:123
|
25
|
+
def self.read(did, options)
|
26
|
+
# setup
|
27
|
+
currentDID = {
|
28
|
+
"did": did,
|
29
|
+
"doc": "",
|
30
|
+
"log": [],
|
31
|
+
"doc_log_id": nil,
|
32
|
+
"termination_log_id": nil,
|
33
|
+
"error": 0,
|
34
|
+
"message": "",
|
35
|
+
"verification": ""
|
36
|
+
}.transform_keys(&:to_s)
|
37
|
+
did_hash = did.delete_prefix("did:pplc:")
|
38
|
+
did10 = did_hash[0,10]
|
39
|
+
|
40
|
+
# get did location
|
41
|
+
did_location = ""
|
42
|
+
if !options[:doc_location].nil?
|
43
|
+
did_location = options[:doc_location]
|
44
|
+
end
|
45
|
+
if did_location.to_s == ""
|
46
|
+
if !options[:location].nil?
|
47
|
+
did_location = options[:location]
|
48
|
+
end
|
49
|
+
end
|
50
|
+
if did_location.to_s == ""
|
51
|
+
if did.include?(LOCATION_PREFIX)
|
52
|
+
tmp = did.split(LOCATION_PREFIX)
|
53
|
+
did = tmp[0]
|
54
|
+
did_location = tmp[1]
|
55
|
+
end
|
56
|
+
end
|
57
|
+
if did_location == ""
|
58
|
+
did_location = DEFAULT_LOCATION
|
59
|
+
end
|
60
|
+
|
61
|
+
# retrieve DID document
|
62
|
+
did_document = retrieve_document(did, did10 + ".doc", did_location, options)
|
63
|
+
if did_document.first.nil?
|
64
|
+
return [nil, did_document.last]
|
65
|
+
end
|
66
|
+
did_document = did_document.first
|
67
|
+
currentDID["doc"] = did_document
|
68
|
+
if options[:trace]
|
69
|
+
puts " .. DID document retrieved"
|
70
|
+
end
|
71
|
+
|
72
|
+
# get log location
|
73
|
+
log_hash = did_document["log"]
|
74
|
+
log_location = ""
|
75
|
+
if !options[:log_location].nil?
|
76
|
+
log_location = options[:log_location]
|
77
|
+
end
|
78
|
+
if log_location.to_s == ""
|
79
|
+
if !options[:location].nil?
|
80
|
+
log_location = options[:location]
|
81
|
+
end
|
82
|
+
end
|
83
|
+
if log_location.to_s == ""
|
84
|
+
if log_hash.include?(LOCATION_PREFIX)
|
85
|
+
hash_split = log_hash.split(LOCATION_PREFIX)
|
86
|
+
log_hash = hash_split[0]
|
87
|
+
log_location = hash_split[1]
|
88
|
+
end
|
89
|
+
end
|
90
|
+
if log_location == ""
|
91
|
+
log_location = DEFAULT_LOCATION
|
92
|
+
end
|
93
|
+
|
94
|
+
# retrieve and traverse log to get current DID state
|
95
|
+
log_array, msg = retrieve_log(log_hash, did10 + ".log", log_location, options)
|
96
|
+
if log_array.nil?
|
97
|
+
return [nil, msg]
|
98
|
+
else
|
99
|
+
if options[:trace]
|
100
|
+
puts " .. Log retrieved"
|
101
|
+
end
|
102
|
+
dag, create_index, terminate_index, msg = dag_did(log_array, options)
|
103
|
+
if dag.nil?
|
104
|
+
return [nil, msg]
|
105
|
+
end
|
106
|
+
if options[:trace]
|
107
|
+
puts " .. DAG with " + dag.vertices.length.to_s + " vertices and " + dag.edges.length.to_s + " edges, CREATE index: " + create_index.to_s
|
108
|
+
end
|
109
|
+
ordered_log_array = dag2array(dag, log_array, create_index, [], options)
|
110
|
+
ordered_log_array << log_array[terminate_index]
|
111
|
+
currentDID["log"] = ordered_log_array
|
112
|
+
if options[:trace]
|
113
|
+
if options[:silent].nil? || !options[:silent]
|
114
|
+
puts " vertex " + terminate_index.to_s + " at " + log_array[terminate_index]["ts"].to_s + " op: " + log_array[terminate_index]["op"].to_s + " doc: " + log_array[terminate_index]["doc"].to_s
|
115
|
+
end
|
116
|
+
end
|
117
|
+
currentDID["log"] = ordered_log_array
|
118
|
+
if options[:trace]
|
119
|
+
if options[:silent].nil? || !options[:silent]
|
120
|
+
dag.edges.each do |e|
|
121
|
+
puts " edge " + e.origin[:id].to_s + " <- " + e.destination[:id].to_s
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
currentDID = dag_update(currentDID, options)
|
126
|
+
if options[:log_complete]
|
127
|
+
currentDID["log"] = log_array
|
128
|
+
end
|
129
|
+
|
130
|
+
return [currentDID, ""]
|
131
|
+
end
|
132
|
+
|
133
|
+
end
|
134
|
+
|
135
|
+
def self.create(content, options)
|
136
|
+
return write(content, nil, "create", options)
|
137
|
+
end
|
138
|
+
|
139
|
+
def self.update(content, did, options)
|
140
|
+
return write(content, did, "update", options)
|
141
|
+
end
|
142
|
+
|
143
|
+
def self.simulate_did(content, did, mode, options)
|
144
|
+
user_did, didDocument, revoc_log, l1, l2, r1, privateKey, revocationKey, did_old, log_old, msg = pplcdid.generate_base(content, did, mode, options)
|
145
|
+
return [user_did, msg]
|
146
|
+
end
|
147
|
+
|
148
|
+
def self.generate_base(content, did, mode, options)
|
149
|
+
# input validation
|
150
|
+
did_doc = JSON.parse(content.to_json) rescue nil
|
151
|
+
if did_doc.nil?
|
152
|
+
return [nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, "invalid payload"]
|
153
|
+
end
|
154
|
+
did_old = nil
|
155
|
+
log_old = nil
|
156
|
+
prev_hash = []
|
157
|
+
revoc_log = nil
|
158
|
+
doc_location = options[:location]
|
159
|
+
if options[:ts].nil?
|
160
|
+
ts = Time.now.to_i
|
161
|
+
else
|
162
|
+
ts = options[:ts]
|
163
|
+
end
|
164
|
+
|
165
|
+
if mode == "create" || mode == "clone"
|
166
|
+
operation_mode = 2 # CREATE
|
167
|
+
if options[:doc_key].nil?
|
168
|
+
if options[:doc_enc].nil?
|
169
|
+
privateKey, msg = generate_private_key(options[:doc_pwd].to_s, 'ed25519-priv')
|
170
|
+
else
|
171
|
+
privateKey, msg = decode_private_key(options[:doc_enc].to_s)
|
172
|
+
end
|
173
|
+
else
|
174
|
+
privateKey, msg = read_private_key(options[:doc_key].to_s)
|
175
|
+
if privateKey.nil?
|
176
|
+
return [nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, "private document key not found"]
|
177
|
+
end
|
178
|
+
end
|
179
|
+
if options[:rev_key].nil?
|
180
|
+
if options[:rev_enc].nil?
|
181
|
+
revocationKey, msg = generate_private_key(options[:rev_pwd].to_s, 'ed25519-priv')
|
182
|
+
else
|
183
|
+
revocationKey, msg = decode_private_key(options[:rev_enc].to_s)
|
184
|
+
end
|
185
|
+
else
|
186
|
+
revocationKey, msg = read_private_key(options[:rev_key].to_s)
|
187
|
+
if revocationKey.nil?
|
188
|
+
return [nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, "private revocation key not found"]
|
189
|
+
end
|
190
|
+
end
|
191
|
+
else # mode == "update" => read information
|
192
|
+
did_info, msg = read(did, options)
|
193
|
+
if did_info.nil?
|
194
|
+
return [nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, "cannot resolve DID (on updating DID)"]
|
195
|
+
end
|
196
|
+
if did_info["error"] != 0
|
197
|
+
return [nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, did_info["message"].to_s]
|
198
|
+
end
|
199
|
+
|
200
|
+
did = did_info["did"]
|
201
|
+
did_hash = did.delete_prefix("did:pplc:")
|
202
|
+
did10 = did_hash[0,10]
|
203
|
+
if doc_location.to_s == ""
|
204
|
+
if did_hash.include?(LOCATION_PREFIX)
|
205
|
+
hash_split = did_hash.split(LOCATION_PREFIX)
|
206
|
+
did_hash = hash_split[0]
|
207
|
+
doc_location = hash_split[1]
|
208
|
+
end
|
209
|
+
end
|
210
|
+
operation_mode = 3 # UPDATE
|
211
|
+
|
212
|
+
# collect relevant information from previous did
|
213
|
+
did_old = did.dup
|
214
|
+
did10_old = did10.dup
|
215
|
+
log_old = did_info["log"]
|
216
|
+
if options[:old_doc_key].nil?
|
217
|
+
if options[:old_doc_enc].nil?
|
218
|
+
if options[:old_doc_pwd].nil?
|
219
|
+
privateKey_old = read_private_storage(did10_old + "_private_key.b58")
|
220
|
+
else
|
221
|
+
privateKey_old, msg = generate_private_key(options[:old_doc_pwd].to_s, 'ed25519-priv')
|
222
|
+
end
|
223
|
+
else
|
224
|
+
privateKey_old, msg = decode_private_key(options[:old_doc_enc].to_s)
|
225
|
+
end
|
226
|
+
else
|
227
|
+
privateKey_old, msg = read_private_key(options[:old_doc_key].to_s)
|
228
|
+
end
|
229
|
+
if privateKey_old.nil?
|
230
|
+
return [nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, "invalid or missing old private document key"]
|
231
|
+
end
|
232
|
+
if options[:old_rev_key].nil?
|
233
|
+
if options[:old_rev_enc].nil?
|
234
|
+
if options[:old_rev_pwd].nil?
|
235
|
+
revocationKey_old = read_private_storage(did10_old + "_revocation_key.b58")
|
236
|
+
else
|
237
|
+
revocationKey_old, msg = generate_private_key(options[:old_rev_pwd].to_s, 'ed25519-priv')
|
238
|
+
end
|
239
|
+
else
|
240
|
+
revocationKey_old, msg = decode_private_key(options[:old_rev_enc].to_s)
|
241
|
+
end
|
242
|
+
else
|
243
|
+
revocationKey_old, msg = read_private_key(options[:old_rev_key].to_s)
|
244
|
+
end
|
245
|
+
if revocationKey_old.nil?
|
246
|
+
return [nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, "invalid or missing old private revocation key"]
|
247
|
+
end
|
248
|
+
|
249
|
+
# key management
|
250
|
+
if options[:doc_key].nil?
|
251
|
+
if options[:doc_enc].nil?
|
252
|
+
privateKey, msg = generate_private_key(options[:doc_pwd].to_s, 'ed25519-priv')
|
253
|
+
else
|
254
|
+
privateKey, msg = decode_private_key(options[:doc_enc].to_s)
|
255
|
+
end
|
256
|
+
else
|
257
|
+
privateKey, msg = read_private_key(options[:doc_key].to_s)
|
258
|
+
end
|
259
|
+
# if options[:rev_key].nil? && options[:rev_pwd].nil? && options[:rev_enc].nil?
|
260
|
+
# revocationLog = read_private_storage(did10 + "_revocation.json")
|
261
|
+
# if revocationLog.nil?
|
262
|
+
# return [nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, "invalid or missing old revocation log"]
|
263
|
+
# end
|
264
|
+
# else
|
265
|
+
if options[:rev_key].nil?
|
266
|
+
if options[:rev_enc].nil?
|
267
|
+
if options[:rev_pwd].nil?
|
268
|
+
revocationKey, msg = generate_private_key("", 'ed25519-priv')
|
269
|
+
else
|
270
|
+
revocationKey, msg = generate_private_key(options[:rev_pwd].to_s, 'ed25519-priv')
|
271
|
+
end
|
272
|
+
else
|
273
|
+
revocationKey, msg = decode_private_key(options[:rev_enc].to_s)
|
274
|
+
end
|
275
|
+
else
|
276
|
+
revocationKey, msg = read_private_key(options[:rev_key].to_s)
|
277
|
+
end
|
278
|
+
|
279
|
+
# re-build revocation document
|
280
|
+
did_old_doc = did_info["doc"]["doc"]
|
281
|
+
ts_old = did_info["log"].last["ts"]
|
282
|
+
publicKey_old = public_key(privateKey_old).first
|
283
|
+
pubRevoKey_old = public_key(revocationKey_old).first
|
284
|
+
did_key_old = publicKey_old + ":" + pubRevoKey_old
|
285
|
+
subDid = {"doc": did_old_doc, "key": did_key_old}.to_json
|
286
|
+
subDidHash = hash(canonical(subDid))
|
287
|
+
signedSubDidHash = sign(subDidHash, revocationKey_old).first
|
288
|
+
revocationLog = {
|
289
|
+
"ts": ts_old,
|
290
|
+
"op": 1, # REVOKE
|
291
|
+
"doc": subDidHash,
|
292
|
+
"sig": signedSubDidHash }.transform_keys(&:to_s).to_json
|
293
|
+
# end
|
294
|
+
revoc_log = JSON.parse(revocationLog)
|
295
|
+
revoc_log["previous"] = [
|
296
|
+
hash(canonical(log_old[did_info["doc_log_id"].to_i])),
|
297
|
+
hash(canonical(log_old[did_info["termination_log_id"].to_i]))
|
298
|
+
]
|
299
|
+
prev_hash = [hash(canonical(revoc_log))]
|
300
|
+
end
|
301
|
+
|
302
|
+
publicKey = public_key(privateKey).first
|
303
|
+
pubRevoKey = public_key(revocationKey).first
|
304
|
+
did_key = publicKey + ":" + pubRevoKey
|
305
|
+
|
306
|
+
# build new revocation document
|
307
|
+
subDid = {"doc": did_doc, "key": did_key}.to_json
|
308
|
+
subDidHash = hash(canonical(subDid))
|
309
|
+
signedSubDidHash = sign(subDidHash, revocationKey).first
|
310
|
+
r1 = { "ts": ts,
|
311
|
+
"op": 1, # REVOKE
|
312
|
+
"doc": subDidHash,
|
313
|
+
"sig": signedSubDidHash }.transform_keys(&:to_s)
|
314
|
+
|
315
|
+
# build termination log entry
|
316
|
+
l2_doc = hash(canonical(r1))
|
317
|
+
if !doc_location.nil?
|
318
|
+
l2_doc += LOCATION_PREFIX + doc_location.to_s
|
319
|
+
end
|
320
|
+
l2 = { "ts": ts,
|
321
|
+
"op": 0, # TERMINATE
|
322
|
+
"doc": l2_doc,
|
323
|
+
"sig": sign(l2_doc, privateKey).first,
|
324
|
+
"previous": [] }.transform_keys(&:to_s)
|
325
|
+
|
326
|
+
# build actual DID document
|
327
|
+
log_str = hash(canonical(l2))
|
328
|
+
if !doc_location.nil?
|
329
|
+
log_str += LOCATION_PREFIX + doc_location.to_s
|
330
|
+
end
|
331
|
+
didDocument = { "doc": did_doc,
|
332
|
+
"key": did_key,
|
333
|
+
"log": log_str }.transform_keys(&:to_s)
|
334
|
+
|
335
|
+
# create DID
|
336
|
+
l1_doc = hash(canonical(didDocument))
|
337
|
+
if !doc_location.nil?
|
338
|
+
l1_doc += LOCATION_PREFIX + doc_location.to_s
|
339
|
+
end
|
340
|
+
did = "did:pplc:" + l1_doc
|
341
|
+
did10 = l1_doc[0,10]
|
342
|
+
|
343
|
+
if mode == "clone"
|
344
|
+
# create log entry for source DID
|
345
|
+
new_log = {
|
346
|
+
"ts": ts,
|
347
|
+
"op": 4, # CLONE
|
348
|
+
"doc": l1_doc,
|
349
|
+
"sig": sign(l1_doc, privateKey).first,
|
350
|
+
"previous": [options[:previous_clone].to_s]
|
351
|
+
}
|
352
|
+
retVal = HTTParty.post(options[:source_location] + "/log/" + options[:source_did],
|
353
|
+
headers: { 'Content-Type' => 'application/json' },
|
354
|
+
body: {"log": new_log}.to_json )
|
355
|
+
prev_hash = [hash(canonical(new_log))]
|
356
|
+
end
|
357
|
+
|
358
|
+
# build creation log entry
|
359
|
+
if operation_mode == 3 # UPDATE
|
360
|
+
l1 = { "ts": ts,
|
361
|
+
"op": operation_mode, # UPDATE
|
362
|
+
"doc": l1_doc,
|
363
|
+
"sig": sign(l1_doc, privateKey_old).first,
|
364
|
+
"previous": prev_hash }.transform_keys(&:to_s)
|
365
|
+
else
|
366
|
+
l1 = { "ts": ts,
|
367
|
+
"op": operation_mode, # CREATE
|
368
|
+
"doc": l1_doc,
|
369
|
+
"sig": sign(l1_doc, privateKey).first,
|
370
|
+
"previous": prev_hash }.transform_keys(&:to_s)
|
371
|
+
end
|
372
|
+
|
373
|
+
return [did, didDocument, revoc_log, l1, l2, r1, privateKey, revocationKey, did_old, log_old, ""]
|
374
|
+
end
|
375
|
+
|
376
|
+
def self.publish(did, didDocument, logs, options)
|
377
|
+
did_hash = did.delete_prefix("did:pplc:")
|
378
|
+
did10 = did_hash[0,10]
|
379
|
+
|
380
|
+
doc_location = options[:doc_location]
|
381
|
+
if doc_location.to_s == ""
|
382
|
+
if did_hash.include?(LOCATION_PREFIX)
|
383
|
+
hash_split = did_hash.split(LOCATION_PREFIX)
|
384
|
+
did_hash = hash_split[0]
|
385
|
+
doc_location = hash_split[1]
|
386
|
+
else
|
387
|
+
doc_location = DEFAULT_LOCATION
|
388
|
+
end
|
389
|
+
end
|
390
|
+
|
391
|
+
# wirte data based on location
|
392
|
+
case doc_location.to_s
|
393
|
+
when /^http/
|
394
|
+
# build object to post
|
395
|
+
did_data = {
|
396
|
+
"did": did,
|
397
|
+
"did-document": didDocument,
|
398
|
+
"logs": logs
|
399
|
+
}
|
400
|
+
ppldid_url = doc_location.to_s + "/doc"
|
401
|
+
retVal = HTTParty.post(ppldid_url,
|
402
|
+
headers: { 'Content-Type' => 'application/json' },
|
403
|
+
body: did_data.to_json )
|
404
|
+
if retVal.code != 200
|
405
|
+
err_msg = retVal.parsed_response("error").to_s rescue "invalid response from " + doc_location.to_s + "/doc"
|
406
|
+
return [false, err_msg]
|
407
|
+
end
|
408
|
+
else
|
409
|
+
# write files to disk
|
410
|
+
write_private_storage(logs.to_json, did10 + ".log")
|
411
|
+
write_private_storage(didDocument.to_json, did10 + ".doc")
|
412
|
+
write_private_storage(did, did10 + ".did")
|
413
|
+
end
|
414
|
+
return [true, ""]
|
415
|
+
|
416
|
+
end
|
417
|
+
|
418
|
+
def self.write(content, did, mode, options)
|
419
|
+
did, didDocument, revoc_log, l1, l2, r1, privateKey, revocationKey, did_old, log_old, msg = generate_base(content, did, mode, options)
|
420
|
+
if msg != ""
|
421
|
+
return [nil, msg]
|
422
|
+
end
|
423
|
+
|
424
|
+
did_hash = did.delete_prefix("did:pplc:")
|
425
|
+
did10 = did_hash[0,10]
|
426
|
+
did_old_hash = did_old.delete_prefix("did:pplc:") rescue nil
|
427
|
+
did10_old = did_old_hash[0,10] rescue nil
|
428
|
+
|
429
|
+
doc_location = options[:doc_location]
|
430
|
+
if doc_location.to_s == ""
|
431
|
+
if did_hash.include?(LOCATION_PREFIX)
|
432
|
+
hash_split = did_hash.split(LOCATION_PREFIX)
|
433
|
+
did_hash = hash_split[0]
|
434
|
+
doc_location = hash_split[1]
|
435
|
+
else
|
436
|
+
doc_location = DEFAULT_LOCATION
|
437
|
+
end
|
438
|
+
end
|
439
|
+
|
440
|
+
case doc_location.to_s
|
441
|
+
when /^http/
|
442
|
+
logs = [revoc_log, l1, l2].flatten.compact
|
443
|
+
else
|
444
|
+
logs = [log_old, revoc_log, l1, l2].flatten.compact
|
445
|
+
if !did_old.nil?
|
446
|
+
write_private_storage([log_old, revoc_log, l1, l2].flatten.compact.to_json, did10_old + ".log")
|
447
|
+
end
|
448
|
+
end
|
449
|
+
success, msg = publish(did, didDocument, logs, options)
|
450
|
+
|
451
|
+
if success
|
452
|
+
w3c_input = {
|
453
|
+
"did" => did,
|
454
|
+
"doc" => didDocument
|
455
|
+
}
|
456
|
+
retVal = {
|
457
|
+
"did" => did,
|
458
|
+
"doc" => didDocument,
|
459
|
+
"doc_w3c" => w3c(w3c_input, options),
|
460
|
+
"log" => logs
|
461
|
+
}
|
462
|
+
if options[:return_secrets]
|
463
|
+
retVal["private_key"] = privateKey
|
464
|
+
retVal["revocation_key"] = revocationKey
|
465
|
+
retVal["revocation_log"] = r1
|
466
|
+
else
|
467
|
+
write_private_storage(privateKey, did10 + "_private_key.b58")
|
468
|
+
write_private_storage(revocationKey, did10 + "_revocation_key.b58")
|
469
|
+
write_private_storage(r1.to_json, did10 + "_revocation.json")
|
470
|
+
end
|
471
|
+
|
472
|
+
return [retVal, ""]
|
473
|
+
else
|
474
|
+
return [nil, msg]
|
475
|
+
end
|
476
|
+
end
|
477
|
+
|
478
|
+
def self.revoke_base(did, options)
|
479
|
+
did_orig = did.dup
|
480
|
+
doc_location = options[:doc_location]
|
481
|
+
if options[:ts].nil?
|
482
|
+
ts = Time.now.to_i
|
483
|
+
else
|
484
|
+
ts = options[:ts]
|
485
|
+
end
|
486
|
+
did_info, msg = read(did, options)
|
487
|
+
if did_info.nil?
|
488
|
+
return [nil, "cannot resolve DID (on revoking DID)"]
|
489
|
+
end
|
490
|
+
if did_info["error"] != 0
|
491
|
+
return [nil, did_info["message"].to_s]
|
492
|
+
end
|
493
|
+
|
494
|
+
did = did_info["did"]
|
495
|
+
did_hash = did.delete_prefix("did:pplc:")
|
496
|
+
did10 = did_hash[0,10]
|
497
|
+
if doc_location.to_s == ""
|
498
|
+
if did_hash.include?(LOCATION_PREFIX)
|
499
|
+
hash_split = did_hash.split(LOCATION_PREFIX)
|
500
|
+
did_hash = hash_split[0]
|
501
|
+
doc_location = hash_split[1]
|
502
|
+
end
|
503
|
+
end
|
504
|
+
|
505
|
+
# collect relevant information from previous did
|
506
|
+
did_old = did.dup
|
507
|
+
did10_old = did10.dup
|
508
|
+
log_old = did_info["log"]
|
509
|
+
|
510
|
+
if options[:old_doc_key].nil?
|
511
|
+
if options[:old_doc_enc].nil?
|
512
|
+
if options[:old_doc_pwd].nil?
|
513
|
+
privateKey_old = read_private_storage(did10_old + "_private_key.b58")
|
514
|
+
else
|
515
|
+
privateKey_old, msg = generate_private_key(options[:old_doc_pwd].to_s, 'ed25519-priv')
|
516
|
+
end
|
517
|
+
else
|
518
|
+
privateKey_old, msg = decode_private_key(options[:old_doc_enc].to_s)
|
519
|
+
end
|
520
|
+
else
|
521
|
+
privateKey_old, msg = read_private_key(options[:old_doc_key].to_s)
|
522
|
+
end
|
523
|
+
if privateKey_old.nil?
|
524
|
+
return [nil, "invalid or missing old private document key"]
|
525
|
+
end
|
526
|
+
if options[:old_rev_key].nil?
|
527
|
+
if options[:old_rev_enc].nil?
|
528
|
+
if options[:old_rev_pwd].nil?
|
529
|
+
revocationKey_old = read_private_storage(did10_old + "_revocation_key.b58")
|
530
|
+
else
|
531
|
+
revocationKey_old, msg = generate_private_key(options[:old_rev_pwd].to_s, 'ed25519-priv')
|
532
|
+
end
|
533
|
+
else
|
534
|
+
revocationKey_old, msg = decode_private_key(options[:old_rev_enc].to_s)
|
535
|
+
end
|
536
|
+
else
|
537
|
+
revocationKey_old, msg = read_private_key(options[:old_rev_key].to_s)
|
538
|
+
end
|
539
|
+
if revocationKey_old.nil?
|
540
|
+
return [nil, "invalid or missing old private revocation key"]
|
541
|
+
end
|
542
|
+
|
543
|
+
if options[:rev_key].nil? && options[:rev_pwd].nil? && options[:rev_enc].nil?
|
544
|
+
revocationKey, msg = read_private_key(did10 + "_revocation_key.b58")
|
545
|
+
revocationLog = read_private_storage(did10 + "_revocation.json")
|
546
|
+
else
|
547
|
+
if options[:rev_pwd].nil?
|
548
|
+
if options[:rev_enc].nil?
|
549
|
+
revocationKey, msg = read_private_key(options[:rev_key].to_s)
|
550
|
+
else
|
551
|
+
revocationKey, msg = decode_private_key(options[:rev_enc].to_s)
|
552
|
+
end
|
553
|
+
else
|
554
|
+
revocationKey, msg = generate_private_key(options[:rev_pwd].to_s, 'ed25519-priv')
|
555
|
+
end
|
556
|
+
# re-build revocation document
|
557
|
+
did_old_doc = did_info["doc"]["doc"]
|
558
|
+
ts_old = did_info["log"].last["ts"]
|
559
|
+
publicKey_old = public_key(privateKey_old).first
|
560
|
+
pubRevoKey_old = public_key(revocationKey_old).first
|
561
|
+
did_key_old = publicKey_old + ":" + pubRevoKey_old
|
562
|
+
subDid = {"doc": did_old_doc, "key": did_key_old}.to_json
|
563
|
+
subDidHash = hash(canonical(subDid))
|
564
|
+
signedSubDidHash = sign(subDidHash, revocationKey_old).first
|
565
|
+
revocationLog = {
|
566
|
+
"ts": ts_old,
|
567
|
+
"op": 1, # REVOKE
|
568
|
+
"doc": subDidHash,
|
569
|
+
"sig": signedSubDidHash }.transform_keys(&:to_s).to_json
|
570
|
+
end
|
571
|
+
|
572
|
+
if revocationLog.nil?
|
573
|
+
return [nil, "private revocation key not found"]
|
574
|
+
end
|
575
|
+
|
576
|
+
revoc_log = JSON.parse(revocationLog)
|
577
|
+
revoc_log["previous"] = [
|
578
|
+
hash(canonical(log_old[did_info["doc_log_id"].to_i])),
|
579
|
+
hash(canonical(log_old[did_info["termination_log_id"].to_i]))
|
580
|
+
]
|
581
|
+
return [revoc_log, ""]
|
582
|
+
end
|
583
|
+
|
584
|
+
def self.revoke_publish(did, revoc_log, options)
|
585
|
+
did_hash = did.delete_prefix("did:pplc:")
|
586
|
+
did10 = did_hash[0,10]
|
587
|
+
doc_location = options[:doc_location]
|
588
|
+
if did_hash.include?(LOCATION_PREFIX)
|
589
|
+
hash_split = did_hash.split(LOCATION_PREFIX)
|
590
|
+
did_hash = hash_split[0]
|
591
|
+
doc_location = hash_split[1]
|
592
|
+
end
|
593
|
+
if doc_location.to_s == ""
|
594
|
+
doc_location = DEFAULT_LOCATION
|
595
|
+
end
|
596
|
+
|
597
|
+
# publish revocation log based on location
|
598
|
+
case doc_location.to_s
|
599
|
+
when /^http/
|
600
|
+
retVal = HTTParty.post(doc_location.to_s + "/log/" + did_hash.to_s,
|
601
|
+
headers: { 'Content-Type' => 'application/json' },
|
602
|
+
body: {"log": revoc_log}.to_json )
|
603
|
+
if retVal.code != 200
|
604
|
+
msg = retVal.parsed_response("error").to_s rescue "invalid response from " + doc_location.to_s + "/log/" + did_hash.to_s
|
605
|
+
return [nil, msg]
|
606
|
+
end
|
607
|
+
else
|
608
|
+
File.write(did10 + ".log", revoc_log.to_json)
|
609
|
+
if !did_old.nil?
|
610
|
+
File.write(did10_old + ".log", revoc_log.to_json)
|
611
|
+
end
|
612
|
+
end
|
613
|
+
|
614
|
+
return [did, ""]
|
615
|
+
end
|
616
|
+
|
617
|
+
def self.revoke(did, options)
|
618
|
+
revoc_log, msg = revoke_base(did, options)
|
619
|
+
if revoc_log.nil?
|
620
|
+
return [nil, msg]
|
621
|
+
end
|
622
|
+
success, msg = revoke_publish(did, revoc_log, options)
|
623
|
+
end
|
624
|
+
|
625
|
+
def self.clone(did, options)
|
626
|
+
# check if locations differ
|
627
|
+
target_location = options[:doc_location]
|
628
|
+
if target_location.to_s == ""
|
629
|
+
target_location = DEFAULT_LOCATION
|
630
|
+
end
|
631
|
+
if did.include?(LOCATION_PREFIX)
|
632
|
+
hash_split = did.split(LOCATION_PREFIX)
|
633
|
+
did = hash_split[0]
|
634
|
+
source_location = hash_split[1]
|
635
|
+
end
|
636
|
+
if source_location.to_s == ""
|
637
|
+
source_location = DEFAULT_LOCATION
|
638
|
+
end
|
639
|
+
if target_location == source_location
|
640
|
+
return [nil, "cannot clone to same location (" + target_location.to_s + ")"]
|
641
|
+
end
|
642
|
+
|
643
|
+
# get original did info
|
644
|
+
options[:doc_location] = source_location
|
645
|
+
options[:log_location] = source_location
|
646
|
+
source_did, msg = read(did, options)
|
647
|
+
if source_did.nil?
|
648
|
+
return [nil, "cannot resolve DID (on cloning DID)"]
|
649
|
+
end
|
650
|
+
if source_did["error"] != 0
|
651
|
+
return [nil, source_did["message"].to_s]
|
652
|
+
end
|
653
|
+
if source_did["doc_log_id"].nil?
|
654
|
+
return [nil, "cannot parse DID log"]
|
655
|
+
end
|
656
|
+
source_log = source_did["log"].first(source_did["doc_log_id"] + 1).last.to_json
|
657
|
+
|
658
|
+
# write did to new location
|
659
|
+
options[:doc_location] = target_location
|
660
|
+
options[:log_location] = target_location
|
661
|
+
options[:previous_clone] = hash(canonical(source_log)) + LOCATION_PREFIX + source_location
|
662
|
+
options[:source_location] = source_location
|
663
|
+
options[:source_did] = source_did["did"]
|
664
|
+
retVal, msg = write(source_did["doc"]["doc"], nil, "clone", options)
|
665
|
+
return [retVal, msg]
|
666
|
+
end
|
667
|
+
|
668
|
+
def self.w3c(did_info, options)
|
669
|
+
did = did_info["did"]
|
670
|
+
if !did.start_with?("did:pplc:")
|
671
|
+
did = "did:pplc:" + did
|
672
|
+
end
|
673
|
+
|
674
|
+
didDoc = did_info.transform_keys(&:to_s)["doc"]
|
675
|
+
pubDocKey = didDoc["key"].split(":")[0] rescue ""
|
676
|
+
pubRevKey = didDoc["key"].split(":")[1] rescue ""
|
677
|
+
|
678
|
+
wd = {}
|
679
|
+
wd["@context"] = "https://www.w3.org/ns/did/v1"
|
680
|
+
wd["id"] = did
|
681
|
+
wd["verificationMethod"] = [{
|
682
|
+
"id": did + "#key-doc",
|
683
|
+
"type": "Ed25519VerificationKey2020",
|
684
|
+
"controller": did,
|
685
|
+
"publicKeyMultibase": pubDocKey
|
686
|
+
},{
|
687
|
+
"id": did + "#key-rev",
|
688
|
+
"type": "Ed25519VerificationKey2020",
|
689
|
+
"controller": did,
|
690
|
+
"publicKeyMultibase": pubRevKey
|
691
|
+
}]
|
692
|
+
|
693
|
+
if didDoc["@context"].to_s == "https://www.w3.org/ns/did/v1"
|
694
|
+
didDoc.delete("@context")
|
695
|
+
end
|
696
|
+
if didDoc["doc"].to_s != ""
|
697
|
+
didDoc = didDoc["doc"]
|
698
|
+
end
|
699
|
+
newDidDoc = []
|
700
|
+
if didDoc.is_a?(Hash)
|
701
|
+
if didDoc["authentication"].to_s != ""
|
702
|
+
wd["authentication"] = didDoc["authentication"]
|
703
|
+
didDoc.delete("authentication")
|
704
|
+
end
|
705
|
+
if didDoc["service"].to_s != ""
|
706
|
+
if didDoc["service"].is_a?(Array)
|
707
|
+
newDidDoc = didDoc.dup
|
708
|
+
newDidDoc.delete("service")
|
709
|
+
if newDidDoc == {}
|
710
|
+
newDidDoc = []
|
711
|
+
else
|
712
|
+
if !newDidDoc.is_a?(Array)
|
713
|
+
newDidDoc=[newDidDoc]
|
714
|
+
end
|
715
|
+
end
|
716
|
+
newDidDoc << didDoc["service"]
|
717
|
+
newDidDoc = newDidDoc.flatten
|
718
|
+
else
|
719
|
+
newDidDoc = didDoc["service"]
|
720
|
+
end
|
721
|
+
else
|
722
|
+
newDidDoc = didDoc
|
723
|
+
end
|
724
|
+
else
|
725
|
+
newDidDoc = didDoc
|
726
|
+
end
|
727
|
+
wd["service"] = newDidDoc
|
728
|
+
return wd
|
729
|
+
end
|
730
|
+
|
731
|
+
def self.fromW3C(didDocument, options)
|
732
|
+
didDocument = didDocument.transform_keys(&:to_s)
|
733
|
+
if didDocument["@context"].to_s == "https://www.w3.org/ns/did/v1"
|
734
|
+
didDocument.delete("@context")
|
735
|
+
end
|
736
|
+
didDocument
|
737
|
+
end
|
738
|
+
|
739
|
+
end
|