pplcdid 1.2.3

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.
Files changed (123) hide show
  1. checksums.yaml +7 -0
  2. data/AUTHORS +1 -0
  3. data/LICENSE +201 -0
  4. data/README.md +2 -0
  5. data/VERSION +1 -0
  6. data/lib/ppldid/basic.rb +278 -0
  7. data/lib/ppldid/didcomm.rb +120 -0
  8. data/lib/ppldid/log.rb +394 -0
  9. data/lib/ppldid.rb +739 -0
  10. data/spec/input/basic/arrays.json +8 -0
  11. data/spec/input/basic/french.json +6 -0
  12. data/spec/input/basic/sample2_get_location.doc +1 -0
  13. data/spec/input/basic/sample2_retrieve_document.doc +1 -0
  14. data/spec/input/basic/sample3_retrieve_document.doc +1 -0
  15. data/spec/input/basic/sample4_retrieve_document.doc +1 -0
  16. data/spec/input/basic/sample5_retrieve_document.doc +1 -0
  17. data/spec/input/basic/sample_dec.doc +1 -0
  18. data/spec/input/basic/sample_enc.doc +1 -0
  19. data/spec/input/basic/sample_get_location.doc +1 -0
  20. data/spec/input/basic/sample_hash.doc +1 -0
  21. data/spec/input/basic/sample_invalid2_readkey.doc +1 -0
  22. data/spec/input/basic/sample_invalid2_verify.doc +1 -0
  23. data/spec/input/basic/sample_invalid3_readkey.doc +1 -0
  24. data/spec/input/basic/sample_invalid3_verify.doc +1 -0
  25. data/spec/input/basic/sample_invalid_privkey.doc +1 -0
  26. data/spec/input/basic/sample_invalid_readkey.doc +1 -0
  27. data/spec/input/basic/sample_invalid_sign.doc +1 -0
  28. data/spec/input/basic/sample_invalid_verify.doc +1 -0
  29. data/spec/input/basic/sample_key.doc +1 -0
  30. data/spec/input/basic/sample_readkey.doc +1 -0
  31. data/spec/input/basic/sample_retrieve_document.doc +1 -0
  32. data/spec/input/basic/sample_sign.doc +1 -0
  33. data/spec/input/basic/sample_valid_privkey.doc +1 -0
  34. data/spec/input/basic/sample_verify.doc +1 -0
  35. data/spec/input/basic/structures.json +8 -0
  36. data/spec/input/basic/unicode.json +3 -0
  37. data/spec/input/basic/values.json +5 -0
  38. data/spec/input/basic/wierd.json +11 -0
  39. data/spec/input/basic/zQmaBZTghn.doc +1 -0
  40. data/spec/input/log/sample0_dag2array.doc +1 -0
  41. data/spec/input/log/sample0_dag_did.doc +1 -0
  42. data/spec/input/log/sample1_dag_did.doc +1 -0
  43. data/spec/input/log/sample1_dag_update.doc +1 -0
  44. data/spec/input/log/sample2_dag_did.doc +1 -0
  45. data/spec/input/log/sample2_dag_update.doc +1 -0
  46. data/spec/input/log/sample2_retrieve_log.doc +1 -0
  47. data/spec/input/log/sample3_dag_did.doc +1 -0
  48. data/spec/input/log/sample3_dag_update.doc +1 -0
  49. data/spec/input/log/sample3_retrieve_log.doc +1 -0
  50. data/spec/input/log/sample4_dag_did.doc +1 -0
  51. data/spec/input/log/sample4_dag_update.doc +1 -0
  52. data/spec/input/log/sample4_retrieve_log.doc +1 -0
  53. data/spec/input/log/sample5_dag_update.doc +1 -0
  54. data/spec/input/log/sample5_retrieve_log.doc +1 -0
  55. data/spec/input/log/sample6_dag_update.doc +1 -0
  56. data/spec/input/log/sample6_retrieve_log.doc +1 -0
  57. data/spec/input/log/sample7_dag_update.doc +1 -0
  58. data/spec/input/log/sample7_retrieve_log.doc +1 -0
  59. data/spec/input/log/sample8_dag_update.doc +1 -0
  60. data/spec/input/log/sample_addhash.doc +1 -0
  61. data/spec/input/log/sample_dag_update.doc +1 -0
  62. data/spec/input/log/sample_match_log.doc +1 -0
  63. data/spec/input/log/sample_op1_addhash.doc +1 -0
  64. data/spec/input/log/sample_retrieve_log.doc +1 -0
  65. data/spec/input/main/sample0_read.doc +1 -0
  66. data/spec/output/basic/arrays.json +1 -0
  67. data/spec/output/basic/french.json +1 -0
  68. data/spec/output/basic/sample2_get_location.doc +1 -0
  69. data/spec/output/basic/sample2_retrieve_document.doc +1 -0
  70. data/spec/output/basic/sample3_retrieve_document.doc +1 -0
  71. data/spec/output/basic/sample4_retrieve_document.doc +1 -0
  72. data/spec/output/basic/sample5_retrieve_document.doc +1 -0
  73. data/spec/output/basic/sample_dec.doc +1 -0
  74. data/spec/output/basic/sample_enc.doc +1 -0
  75. data/spec/output/basic/sample_get_location.doc +1 -0
  76. data/spec/output/basic/sample_hash.doc +1 -0
  77. data/spec/output/basic/sample_invalid2_readkey.doc +1 -0
  78. data/spec/output/basic/sample_invalid2_verify.doc +1 -0
  79. data/spec/output/basic/sample_invalid3_readkey.doc +1 -0
  80. data/spec/output/basic/sample_invalid3_verify.doc +1 -0
  81. data/spec/output/basic/sample_invalid_privkey.doc +1 -0
  82. data/spec/output/basic/sample_invalid_readkey.doc +1 -0
  83. data/spec/output/basic/sample_invalid_sign.doc +1 -0
  84. data/spec/output/basic/sample_invalid_verify.doc +1 -0
  85. data/spec/output/basic/sample_key.doc +1 -0
  86. data/spec/output/basic/sample_readkey.doc +1 -0
  87. data/spec/output/basic/sample_retrieve_document.doc +1 -0
  88. data/spec/output/basic/sample_sign.doc +1 -0
  89. data/spec/output/basic/sample_valid_privkey.doc +1 -0
  90. data/spec/output/basic/sample_verify.doc +1 -0
  91. data/spec/output/basic/structures.json +1 -0
  92. data/spec/output/basic/unicode.json +1 -0
  93. data/spec/output/basic/values.json +1 -0
  94. data/spec/output/basic/wierd.json +1 -0
  95. data/spec/output/log/sample0_dag2array.doc +1 -0
  96. data/spec/output/log/sample0_dag_did.doc +1 -0
  97. data/spec/output/log/sample1_dag_did.doc +1 -0
  98. data/spec/output/log/sample1_dag_update.doc +1 -0
  99. data/spec/output/log/sample2_dag_did.doc +1 -0
  100. data/spec/output/log/sample2_dag_update.doc +1 -0
  101. data/spec/output/log/sample2_retrieve_log.doc +1 -0
  102. data/spec/output/log/sample3_dag_did.doc +1 -0
  103. data/spec/output/log/sample3_dag_update.doc +1 -0
  104. data/spec/output/log/sample3_retrieve_log.doc +1 -0
  105. data/spec/output/log/sample4_dag_did.doc +1 -0
  106. data/spec/output/log/sample4_dag_update.doc +1 -0
  107. data/spec/output/log/sample4_retrieve_log.doc +1 -0
  108. data/spec/output/log/sample5_dag_update.doc +1 -0
  109. data/spec/output/log/sample5_retrieve_log.doc +1 -0
  110. data/spec/output/log/sample6_dag_update.doc +1 -0
  111. data/spec/output/log/sample6_retrieve_log.doc +1 -0
  112. data/spec/output/log/sample7_dag_update.doc +1 -0
  113. data/spec/output/log/sample7_retrieve_log.doc +1 -0
  114. data/spec/output/log/sample8_dag_update.doc +1 -0
  115. data/spec/output/log/sample_addhash.doc +1 -0
  116. data/spec/output/log/sample_dag_update.doc +1 -0
  117. data/spec/output/log/sample_match_log.doc +1 -0
  118. data/spec/output/log/sample_op1_addhash.doc +1 -0
  119. data/spec/output/log/sample_retrieve_log.doc +1 -0
  120. data/spec/output/main/sample0_read.doc +1 -0
  121. data/spec/ppldid_spec.rb +170 -0
  122. data/spec/spec_helper.rb +31 -0
  123. 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