oydid 0.5.4 → 0.5.6

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.
data/lib/oydid.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  # -*- encoding: utf-8 -*-
2
2
  # frozen_string_literal: true
3
3
 
4
- require 'dag'
4
+ require 'simple_dag'
5
5
  require 'jwt'
6
6
  require 'rbnacl'
7
7
  require 'ed25519'
@@ -24,9 +24,18 @@ class Oydid
24
24
  DEFAULT_ENCODING = "base58btc"
25
25
  SUPPORTED_ENCODINGS = ["base16", "base32", "base58btc", "base64"]
26
26
  LOG_HASH_OPTIONS = {:digest => "sha2-256", :encode => "base58btc"}
27
+ DEFAULT_PUBLIC_RESOLVER = "https://dev.uniresolver.io/1.0/identifiers/"
28
+
29
+ # full Multicodecs table: https://github.com/multiformats/multicodec/blob/master/table.csv
30
+ # Multicodecs.register(code: 0x1305, name: 'rsa-priv', tag: 'key')
31
+ # Multicodecs.register(code: 0x1205, name: 'rsa-pub', tag: 'key')
27
32
 
28
33
  # expected DID format: did:oyd:123
29
34
  def self.read(did, options)
35
+ if did.to_s == ""
36
+ return [nil, "missing DID"]
37
+ end
38
+
30
39
  # setup
31
40
  currentDID = {
32
41
  "did": did,
@@ -115,15 +124,12 @@ class Oydid
115
124
  if options[:trace]
116
125
  puts " .. DAG with " + dag.vertices.length.to_s + " vertices and " + dag.edges.length.to_s + " edges, CREATE index: " + create_index.to_s
117
126
  end
118
- ordered_log_array = dag2array(dag, log_array, create_index, [], options)
119
- ordered_log_array << log_array[terminate_index]
120
- currentDID["log"] = ordered_log_array
121
- if options[:trace]
122
- if options[:silent].nil? || !options[:silent]
123
- 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
124
- end
125
- end
127
+
128
+ result = dag2array(dag, log_array, create_index, [], options)
129
+ ordered_log_array = dag2array_terminate(dag, log_array, terminate_index, result, options)
126
130
  currentDID["log"] = ordered_log_array
131
+ # !!! ugly hack to get access to all delegation keys required in dag_update
132
+ currentDID["full_log"] = log_array
127
133
  if options[:trace]
128
134
  if options[:silent].nil? || !options[:silent]
129
135
  dag.edges.each do |e|
@@ -131,14 +137,24 @@ class Oydid
131
137
  end
132
138
  end
133
139
  end
134
- currentDID = dag_update(currentDID, options)
140
+
141
+ # identify if DID Rotation was performed
142
+ rotated_DID = (currentDID.transform_keys(&:to_s)["doc"]["doc"].has_key?("@context") &&
143
+ currentDID.transform_keys(&:to_s)["doc"]["doc"].has_key?("id") &&
144
+ currentDID.transform_keys(&:to_s)["doc"]["doc"]["id"].split(":").first == "did") rescue false
145
+
146
+ if rotated_DID
147
+ doc = currentDID["doc"].dup
148
+ currentDID = dag_update(currentDID, options)
149
+ currentDID["doc"] = doc
150
+ else
151
+ currentDID = dag_update(currentDID, options)
152
+ end
135
153
  if options[:log_complete]
136
154
  currentDID["log"] = log_array
137
155
  end
138
-
139
156
  return [currentDID, ""]
140
157
  end
141
-
142
158
  end
143
159
 
144
160
  def self.create(content, options)
@@ -150,7 +166,7 @@ class Oydid
150
166
  end
151
167
 
152
168
  def self.simulate_did(content, did, mode, options)
153
- did_doc, did_key, did_log, msg = Oydid.generate_base(content, did, mode, options)
169
+ did_doc, did_key, did_log, msg = generate_base(content, did, mode, options)
154
170
  user_did = did_doc[:did]
155
171
  return [user_did, msg]
156
172
  end
@@ -169,50 +185,38 @@ class Oydid
169
185
  revoc_log = nil
170
186
  doc_location = options[:location]
171
187
  if options[:ts].nil?
172
- ts = Time.now.to_i
188
+ ts = Time.now.utc.to_i
173
189
  else
174
190
  ts = options[:ts]
175
191
  end
176
192
 
177
- if mode == "create" || mode == "clone"
178
- operation_mode = 2 # CREATE
179
- if options[:doc_key].nil?
180
- if options[:doc_enc].nil?
181
- privateKey, msg = generate_private_key(options[:doc_pwd].to_s, 'ed25519-priv', options)
182
- else
183
- privateKey, msg = decode_private_key(options[:doc_enc].to_s, options)
184
- end
185
- else
186
- privateKey, msg = read_private_key(options[:doc_key].to_s, options)
187
- if privateKey.nil?
188
- return [nil, nil, nil, "private document key not found"]
189
- end
193
+ # key management
194
+ tmp_did_hash = did.delete_prefix("did:oyd:") rescue ""
195
+ tmp_did10 = tmp_did_hash[0,10] + "_private_key.enc" rescue ""
196
+ privateKey, msg = getPrivateKey(options[:doc_enc], options[:doc_pwd], options[:doc_key], tmp_did10, options)
197
+ if privateKey.nil?
198
+ privateKey, msg = generate_private_key("", 'ed25519-priv', options)
199
+ if privateKey.nil?
200
+ return [nil, nil, nil, "private document key not found"]
190
201
  end
191
- if options[:rev_key].nil?
192
- if options[:rev_enc].nil?
193
- revocationKey, msg = generate_private_key(options[:rev_pwd].to_s, 'ed25519-priv', options)
194
- else
195
- revocationKey, msg = decode_private_key(options[:rev_enc].to_s, options)
196
- end
197
- else
198
- revocationKey, msg = read_private_key(options[:rev_key].to_s, options)
199
- if revocationKey.nil?
200
- return [nil, nil, nil, "private revocation key not found"]
201
- end
202
+ end
203
+ tmp_did10 = tmp_did_hash[0,10] + "_revocation_key.enc" rescue ""
204
+ revocationKey, msg = getPrivateKey(options[:rev_enc], options[:rev_pwd], options[:rev_key], tmp_did10, options)
205
+ if revocationKey.nil?
206
+ revocationKey, msg = generate_private_key("", 'ed25519-priv', options)
207
+ if revocationKey.nil?
208
+ return [nil, nil, nil, "private revocation key not found"]
202
209
  end
203
- else # mode == "update" => read information
204
- # if a location is provided this is only relevant for writing the DID
205
- update_location = options[:location]
206
- update_doc_location = options[:doc_location]
207
- update_log_location = options[:log_location]
208
- options[:location] = nil
209
- options[:doc_location] = nil
210
- options[:log_location] = nil
211
- did_info, msg = read(did, options)
212
- options[:location] = options[:location]
213
- options[:doc_location] = options[:doc_location]
214
- options[:log_location] = options[:log_location]
210
+ end
215
211
 
212
+ # mode-specific handling
213
+ if mode == "create" || mode == "clone"
214
+ operation_mode = 2 # CREATE
215
+
216
+ else # mode == "update" => read information first
217
+ operation_mode = 3 # UPDATE
218
+
219
+ did_info, msg = read(did, options)
216
220
  if did_info.nil?
217
221
  return [nil, nil, nil, "cannot resolve DID (on updating DID)"]
218
222
  end
@@ -230,90 +234,64 @@ class Oydid
230
234
  doc_location = hash_split[1]
231
235
  end
232
236
  end
233
- operation_mode = 3 # UPDATE
234
-
235
- # collect relevant information from previous did
236
237
  did_old = did.dup
237
238
  did10_old = did10.dup
238
239
  log_old = did_info["log"]
239
- if options[:old_doc_key].nil?
240
- if options[:old_doc_enc].nil?
241
- if options[:old_doc_pwd].nil?
242
- privateKey_old = read_private_storage(did10_old + "_private_key.enc")
243
- else
244
- privateKey_old, msg = generate_private_key(options[:old_doc_pwd].to_s, 'ed25519-priv', options)
245
- end
246
- else
247
- privateKey_old, msg = decode_private_key(options[:old_doc_enc].to_s, options)
248
- end
240
+
241
+ # check if provided old keys are native DID keys or delegates ==================
242
+ tmp_old_did10 = did10_old + "_private_key.enc" rescue ""
243
+ old_privateKey, msg = getPrivateKey(options[:old_doc_enc], options[:old_doc_pwd], options[:old_doc_key], tmp_old_did10, options)
244
+ tmp_old_did10 = did10_old + "_revocation_key.enc" rescue ""
245
+ old_revocationKey, msg = getPrivateKey(options[:old_rev_enc], options[:old_rev_pwd], options[:old_rev_key], tmp_did10, options)
246
+ old_publicDocKey = public_key(old_privateKey, {}).first
247
+ old_publicRevKey = public_key(old_revocationKey, {}).first
248
+ old_did_key = old_publicDocKey + ":" + old_publicRevKey
249
+
250
+ # compare old keys with existing DID Document & generate revocation record
251
+ if old_did_key.to_s == did_info["doc"]["key"].to_s
252
+ # provided keys are native DID keys ------------------
253
+
254
+ # re-build revocation document
255
+ old_did_doc = did_info["doc"]["doc"]
256
+ old_ts = did_info["log"].last["ts"]
257
+ old_subDid = {"doc": old_did_doc, "key": old_did_key}.to_json
258
+ old_subDidHash = multi_hash(canonical(old_subDid), LOG_HASH_OPTIONS).first
259
+ old_signedSubDidHash = sign(old_subDidHash, old_revocationKey, LOG_HASH_OPTIONS).first
260
+ revocationLog = {
261
+ "ts": old_ts,
262
+ "op": 1, # REVOKE
263
+ "doc": old_subDidHash,
264
+ "sig": old_signedSubDidHash }.transform_keys(&:to_s).to_json
249
265
  else
250
- privateKey_old, msg = read_private_key(options[:old_doc_key].to_s, options)
251
- end
266
+ # proviced keys are either delegates or invalid ------
267
+ # * check validity of key-doc delegate
268
+ pubKeys, msg = getDelegatedPubKeysFromDID(did, "doc")
269
+ if !pubKeys.include?(old_publicDocKey)
270
+ return [nil, nil, nil, "invalid or missing old private document key"]
271
+ end
252
272
 
253
- if privateKey_old.nil?
254
- return [nil, nil, nil, "invalid or missing old private document key"]
255
- end
256
- if options[:old_rev_key].nil?
257
- if options[:old_rev_enc].nil?
258
- if options[:old_rev_pwd].nil?
259
- revocationKey_old = read_private_storage(did10_old + "_revocation_key.enc")
260
- else
261
- revocationKey_old, msg = generate_private_key(options[:old_rev_pwd].to_s, 'ed25519-priv', options)
273
+ # * check validity of key-rev delegate
274
+ pubKeys, msg = getDelegatedPubKeysFromDID(did, "rev")
275
+ if !pubKeys.include?(old_publicRevKey)
276
+ return [nil, nil, nil, "invalid or missing old private revocation key"]
277
+ end
278
+
279
+ # retrieve revocationLog from previous in key-rev delegate
280
+ revoc_log = nil
281
+ log_old.each do |item|
282
+ if !item["encrypted-revocation-log"].nil?
283
+ revoc_log = item["encrypted-revocation-log"]
262
284
  end
263
- else
264
- revocationKey_old, msg = decode_private_key(options[:old_rev_enc].to_s, options)
265
285
  end
266
- else
267
- revocationKey_old, msg = read_private_key(options[:old_rev_key].to_s, options)
268
- end
269
- if revocationKey_old.nil?
270
- return [nil, nil, nil, "invalid or missing old private revocation key"]
271
- end
272
- # key management
273
- if options[:doc_key].nil?
274
- if options[:doc_enc].nil?
275
- privateKey, msg = generate_private_key(options[:doc_pwd].to_s, 'ed25519-priv', options)
276
- else
277
- privateKey, msg = decode_private_key(options[:doc_enc].to_s, options)
286
+ if revoc_log.nil?
287
+ return [nil, nil, nil, "cannot retrieve revocation log"]
278
288
  end
279
- else
280
- privateKey, msg = read_private_key(options[:doc_key].to_s, options)
281
- end
282
- # if options[:rev_key].nil? && options[:rev_pwd].nil? && options[:rev_enc].nil?
283
- # revocationLog = read_private_storage(did10 + "_revocation.json")
284
- # if revocationLog.nil?
285
- # return [nil, nil, nil, "invalid or missing old revocation log"]
286
- # end
287
- # else
288
- if options[:rev_key].nil?
289
- if options[:rev_enc].nil?
290
- if options[:rev_pwd].nil?
291
- revocationKey, msg = generate_private_key("", 'ed25519-priv', options)
292
- else
293
- revocationKey, msg = generate_private_key(options[:rev_pwd].to_s, 'ed25519-priv', options)
294
- end
295
- else
296
- revocationKey, msg = decode_private_key(options[:rev_enc].to_s, options)
297
- end
298
- else
299
- revocationKey, msg = read_private_key(options[:rev_key].to_s, options)
289
+ revocationLog, msg = decrypt(revoc_log.to_json, old_revocationKey.to_s)
290
+ if revocationLog.nil?
291
+ return [nil, nil, nil, "cannot decrypt revocation log entry: " + msg]
300
292
  end
293
+ end # compare old keys with existing DID Document
301
294
 
302
- # re-build revocation document
303
- did_old_doc = did_info["doc"]["doc"]
304
- ts_old = did_info["log"].last["ts"]
305
- publicKey_old = public_key(privateKey_old, options).first
306
- pubRevoKey_old = public_key(revocationKey_old, options).first
307
- did_key_old = publicKey_old + ":" + pubRevoKey_old
308
- subDid = {"doc": did_old_doc, "key": did_key_old}.to_json
309
- subDidHash = multi_hash(canonical(subDid), LOG_HASH_OPTIONS).first
310
- signedSubDidHash = sign(subDidHash, revocationKey_old, options).first
311
- revocationLog = {
312
- "ts": ts_old,
313
- "op": 1, # REVOKE
314
- "doc": subDidHash,
315
- "sig": signedSubDidHash }.transform_keys(&:to_s).to_json
316
- # end
317
295
  revoc_log = JSON.parse(revocationLog)
318
296
  revoc_log["previous"] = [
319
297
  multi_hash(canonical(log_old[did_info["doc_log_id"].to_i]), LOG_HASH_OPTIONS).first,
@@ -325,11 +303,25 @@ class Oydid
325
303
  pubRevoKey = public_key(revocationKey, options).first
326
304
  did_key = publicKey + ":" + pubRevoKey
327
305
 
328
- # check if pubKeys matches with existing DID Document
329
- if mode == "update"
330
- if did_key_old.to_s != did_info["doc"]["key"].to_s
331
- return [nil, nil, nil, "keys from original DID don't match"]
306
+ if options[:x25519_keyAgreement]
307
+ if did_doc.nil?
308
+ did_doc = {}
309
+ end
310
+ did_doc[:keyAgreement] = [{
311
+ "id": "#key-doc-x25519",
312
+ "type": "X25519KeyAgreementKey2019",
313
+ "publicKeyMultibase": public_key(privateKey, options, 'x25519-pub').first
314
+ }]
315
+ did_doc = did_doc.transform_keys(&:to_s)
316
+ end
317
+ if options[:authentication]
318
+ if did_doc.nil?
319
+ did_doc = {}
332
320
  end
321
+ did_doc[:authentication] = [{
322
+ "id": "#key-doc"
323
+ }]
324
+ did_doc = did_doc.transform_keys(&:to_s)
333
325
  end
334
326
 
335
327
  # build new revocation document
@@ -350,11 +342,16 @@ class Oydid
350
342
  if !doc_location.nil?
351
343
  l2_doc += LOCATION_PREFIX + doc_location.to_s
352
344
  end
345
+ if options[:confirm_logs].nil?
346
+ previous_array = []
347
+ else
348
+ previous_array = options[:confirm_logs]
349
+ end
353
350
  l2 = { "ts": ts,
354
351
  "op": 0, # TERMINATE
355
352
  "doc": l2_doc,
356
353
  "sig": sign(l2_doc, privateKey, options).first,
357
- "previous": [] }.transform_keys(&:to_s)
354
+ "previous": previous_array }.transform_keys(&:to_s)
358
355
 
359
356
  # build actual DID document
360
357
  log_str = multi_hash(canonical(l2), LOG_HASH_OPTIONS).first
@@ -389,13 +386,27 @@ class Oydid
389
386
  end
390
387
 
391
388
  # build creation log entry
389
+ log_revoke_encrypted_array = nil
392
390
  if operation_mode == 3 # UPDATE
393
391
  l1 = { "ts": ts,
394
392
  "op": operation_mode, # UPDATE
395
393
  "doc": l1_doc,
396
- "sig": sign(l1_doc, privateKey_old, options).first,
394
+ "sig": sign(l1_doc, old_privateKey, options).first,
397
395
  "previous": prev_hash }.transform_keys(&:to_s)
398
- else
396
+ options[:confirm_logs].each do |el|
397
+ # read each log entry to check if it is a revocation delegation
398
+ log_item, msg = retrieve_log_item(el, doc_location, options)
399
+ if log_item["doc"][0..3] == "rev:"
400
+ cipher, msg = encrypt(r1.to_json, log_item["encryption-key"], {})
401
+ cipher[:log] = el.to_s
402
+ if log_revoke_encrypted_array.nil?
403
+ log_revoke_encrypted_array = [cipher]
404
+ else
405
+ log_revoke_encrypted_array << cipher
406
+ end
407
+ end
408
+ end unless options[:confirm_logs].nil?
409
+ else
399
410
  l1 = { "ts": ts,
400
411
  "op": operation_mode, # CREATE
401
412
  "doc": l1_doc,
@@ -407,7 +418,6 @@ class Oydid
407
418
  # did_doc = [did, didDocument, did_old]
408
419
  # did_log = [revoc_log, l1, l2, r1, log_old]
409
420
  # did_key = [privateKey, revocationKey]
410
-
411
421
  did_doc = {
412
422
  :did => did,
413
423
  :didDocument => didDocument,
@@ -420,6 +430,10 @@ class Oydid
420
430
  :r1 => r1,
421
431
  :log_old => log_old
422
432
  }
433
+ if !log_revoke_encrypted_array.nil?
434
+ did_log[:r1_encrypted] = log_revoke_encrypted_array
435
+ end
436
+
423
437
  did_key = {
424
438
  :privateKey => privateKey,
425
439
  :revocationKey => revocationKey
@@ -482,6 +496,7 @@ class Oydid
482
496
  l1 = did_log[:l1]
483
497
  l2 = did_log[:l2]
484
498
  r1 = did_log[:r1]
499
+ r1_encrypted = did_log[:r1_encrypted]
485
500
  log_old = did_log[:log_old]
486
501
  privateKey = did_key[:privateKey]
487
502
  revocationKey = did_key[:revocationKey]
@@ -504,7 +519,7 @@ class Oydid
504
519
  end
505
520
  case doc_location.to_s
506
521
  when /^http/
507
- logs = [revoc_log, l1, l2].flatten.compact
522
+ logs = [revoc_log, l1, l2, r1_encrypted].flatten.compact
508
523
  else
509
524
  logs = [log_old, revoc_log, l1, l2].flatten.compact
510
525
  if !did_old.nil?
@@ -514,14 +529,17 @@ class Oydid
514
529
  success, msg = publish(did, didDocument, logs, options)
515
530
 
516
531
  if success
532
+ didDocumentBackup = Marshal.load(Marshal.dump(didDocument))
517
533
  w3c_input = {
518
- "did" => did,
519
- "doc" => didDocument
534
+ "did" => did.clone,
535
+ "doc" => didDocument.clone
520
536
  }
537
+ doc_w3c = Oydid.w3c(w3c_input, options)
538
+ didDocument = didDocumentBackup
521
539
  retVal = {
522
540
  "did" => did,
523
541
  "doc" => didDocument,
524
- "doc_w3c" => w3c(w3c_input, options),
542
+ "doc_w3c" => doc_w3c,
525
543
  "log" => logs
526
544
  }
527
545
  if options[:return_secrets]
@@ -540,11 +558,88 @@ class Oydid
540
558
  end
541
559
  end
542
560
 
561
+ def self.write_log(did, log, options = {})
562
+ # validate log
563
+ if !log.is_a?(Hash)
564
+ return [nil, "invalid log input"]
565
+ end
566
+ log = log.transform_keys(&:to_s)
567
+ if log["ts"].nil?
568
+ return [nil, "missing timestamp in log"]
569
+ end
570
+ if log["op"].nil?
571
+ return [nil, "missing operation in log"]
572
+ end
573
+ if log["doc"].nil?
574
+ return [nil, "missing doc entry in log"]
575
+ end
576
+ if log["sig"].nil?
577
+ return [nil, "missing signature in log"]
578
+ end
579
+
580
+ # validate did
581
+ if did.include?(LOCATION_PREFIX)
582
+ tmp = did.split(LOCATION_PREFIX)
583
+ did = tmp[0]
584
+ source_location = tmp[1]
585
+ log_location = tmp[1]
586
+ end
587
+ if did.include?(CGI.escape LOCATION_PREFIX)
588
+ tmp = did.split(CGI.escape LOCATION_PREFIX)
589
+ did = tmp[0]
590
+ source_location = tmp[1]
591
+ log_location = tmp[1]
592
+ end
593
+
594
+ if source_location.to_s == ""
595
+ if options[:doc_location].nil?
596
+ source_location = DEFAULT_LOCATION
597
+ else
598
+ source_location = options[:doc_location]
599
+ end
600
+ if options[:log_location].nil?
601
+ log_location = DEFAULT_LOCATION
602
+ else
603
+ log_location = options[:log_location]
604
+ end
605
+ end
606
+ options[:doc_location] = source_location
607
+ options[:log_location] = log_location
608
+ source_did, msg = read(did, options)
609
+ if source_did.nil?
610
+ return [nil, "cannot resolve DID (on writing logs)"]
611
+ end
612
+ if source_did["error"] != 0
613
+ return [nil, source_did["message"].to_s]
614
+ end
615
+ if source_did["doc_log_id"].nil?
616
+ return [nil, "cannot parse DID log"]
617
+ end
618
+
619
+ # write log
620
+ source_location = source_location.gsub("%3A",":")
621
+ source_location = source_location.gsub("%2F%2F","//")
622
+ retVal = HTTParty.post(source_location + "/log/" + did,
623
+ headers: { 'Content-Type' => 'application/json' },
624
+ body: {"log": log}.to_json )
625
+ code = retVal.code rescue 500
626
+ if code != 200
627
+ err_msg = retVal.parsed_response["error"].to_s rescue "invalid response from " + source_location.to_s + "/log"
628
+ return ["", err_msg]
629
+ end
630
+ log_hash = retVal.parsed_response["log"] rescue ""
631
+ if log_hash == ""
632
+ err_msg = "missing log hash from " + source_location.to_s + "/log"
633
+ return ["", err_msg]
634
+ end
635
+ return [log_hash, nil]
636
+ end
637
+
543
638
  def self.revoke_base(did, options)
544
639
  did_orig = did.dup
545
640
  doc_location = options[:doc_location]
546
641
  if options[:ts].nil?
547
- ts = Time.now.to_i
642
+ ts = Time.now.utc.to_i
548
643
  else
549
644
  ts = options[:ts]
550
645
  end
@@ -606,38 +701,111 @@ class Oydid
606
701
  end
607
702
 
608
703
  if options[:rev_key].nil? && options[:rev_pwd].nil? && options[:rev_enc].nil?
609
- revocationKey, msg = read_private_key(did10 + "_revocation_key.enc", options)
704
+ # revocationKey, msg = read_private_key(did10 + "_revocation_key.enc", options)
610
705
  revocationLog = read_private_storage(did10 + "_revocation.json")
611
706
  else
612
- if options[:rev_pwd].nil?
707
+
708
+ # check if provided old keys are native DID keys or delegates ==================
709
+ if options[:doc_key].nil?
710
+ if options[:doc_enc].nil?
711
+ old_privateKey, msg = generate_private_key(options[:old_doc_pwd].to_s, 'ed25519-priv', options)
712
+ else
713
+ old_privateKey, msg = decode_private_key(options[:old_doc_enc].to_s, options)
714
+ end
715
+ else
716
+ old_privateKey, msg = read_private_key(options[:old_doc_key].to_s, options)
717
+ end
718
+ if options[:rev_key].nil?
613
719
  if options[:rev_enc].nil?
614
- revocationKey, msg = read_private_key(options[:rev_key].to_s, options)
720
+ old_revocationKey, msg = generate_private_key(options[:old_rev_pwd].to_s, 'ed25519-priv', options)
615
721
  else
616
- revocationKey, msg = decode_private_key(options[:rev_enc].to_s, options)
722
+ old_revocationKey, msg = decode_private_key(options[:old_rev_enc].to_s, options)
617
723
  end
618
724
  else
619
- revocationKey, msg = generate_private_key(options[:rev_pwd].to_s, 'ed25519-priv', options)
620
- end
621
- # re-build revocation document
622
- did_old_doc = did_info["doc"]["doc"]
623
- ts_old = did_info["log"].last["ts"]
624
- publicKey_old = public_key(privateKey_old, options).first
625
- pubRevoKey_old = public_key(revocationKey_old, options).first
626
- did_key_old = publicKey_old + ":" + pubRevoKey_old
627
- subDid = {"doc": did_old_doc, "key": did_key_old}.to_json
628
- subDidHash = multi_hash(canonical(subDid), LOG_HASH_OPTIONS).first
629
- signedSubDidHash = sign(subDidHash, revocationKey_old, options).first
630
- revocationLog = {
631
- "ts": ts_old,
632
- "op": 1, # REVOKE
633
- "doc": subDidHash,
634
- "sig": signedSubDidHash }.transform_keys(&:to_s).to_json
725
+ old_revocationKey, msg = read_private_key(options[:old_rev_key].to_s, options)
726
+ end
727
+ old_publicDocKey = public_key(old_privateKey, {}).first
728
+ old_publicRevKey = public_key(old_revocationKey, {}).first
729
+ old_did_key = old_publicDocKey + ":" + old_publicRevKey
730
+
731
+ # compare old keys with existing DID Document & generate revocation record
732
+ if old_did_key.to_s == did_info["doc"]["key"].to_s
733
+ # provided keys are native DID keys ------------------
734
+
735
+ # re-build revocation document
736
+ old_did_doc = did_info["doc"]["doc"]
737
+ old_ts = did_info["log"].last["ts"]
738
+ old_subDid = {"doc": old_did_doc, "key": old_did_key}.to_json
739
+ old_subDidHash = multi_hash(canonical(old_subDid), LOG_HASH_OPTIONS).first
740
+ old_signedSubDidHash = sign(old_subDidHash, old_revocationKey, LOG_HASH_OPTIONS).first
741
+ revocationLog = {
742
+ "ts": old_ts,
743
+ "op": 1, # REVOKE
744
+ "doc": old_subDidHash,
745
+ "sig": old_signedSubDidHash }.transform_keys(&:to_s).to_json
746
+ else
747
+ # proviced keys are either delegates or invalid ------
748
+ # * check validity of key-doc delegate
749
+ pubKeys, msg = getDelegatedPubKeysFromDID(did, "doc")
750
+ if !pubKeys.include?(old_publicDocKey)
751
+ return [nil, "invalid or missing private document key"]
752
+ end
753
+
754
+ # * check validity of key-rev delegate
755
+ pubKeys, msg = getDelegatedPubKeysFromDID(did, "rev")
756
+ if !pubKeys.include?(old_publicRevKey)
757
+ return [nil, "invalid or missing private revocation key"]
758
+ end
759
+
760
+ # retrieve revocationLog from previous in key-rev delegate
761
+ revoc_log = nil
762
+ log_old.each do |item|
763
+ if !item["encrypted-revocation-log"].nil?
764
+ revoc_log = item["encrypted-revocation-log"]
765
+ end
766
+ end
767
+ if revoc_log.nil?
768
+ return [nil, "cannot retrieve revocation log"]
769
+ end
770
+ revocationLog, msg = decrypt(revoc_log.to_json, old_revocationKey.to_s)
771
+ if revocationLog.nil?
772
+ return [nil, "cannot decrypt revocation log entry: " + msg]
773
+ end
774
+ end # compare old keys with existing DID Document
775
+
776
+ # if options[:rev_pwd].nil?
777
+ # if options[:rev_enc].nil?
778
+ # revocationKey, msg = read_private_key(options[:rev_key].to_s, options)
779
+ # else
780
+ # revocationKey, msg = decode_private_key(options[:rev_enc].to_s, options)
781
+ # end
782
+ # else
783
+ # revocationKey, msg = generate_private_key(options[:rev_pwd].to_s, 'ed25519-priv', options)
784
+ # end
785
+ # # re-build revocation document
786
+ # did_old_doc = did_info["doc"]["doc"]
787
+ # ts_old = did_info["log"].last["ts"]
788
+ # publicKey_old = public_key(privateKey_old, options).first
789
+ # pubRevoKey_old = public_key(revocationKey_old, options).first
790
+ # did_key_old = publicKey_old + ":" + pubRevoKey_old
791
+ # subDid = {"doc": did_old_doc, "key": did_key_old}.to_json
792
+ # subDidHash = multi_hash(canonical(subDid), LOG_HASH_OPTIONS).first
793
+ # signedSubDidHash = sign(subDidHash, revocationKey_old, options).first
794
+ # revocationLog = {
795
+ # "ts": ts_old,
796
+ # "op": 1, # REVOKE
797
+ # "doc": subDidHash,
798
+ # "sig": signedSubDidHash }.transform_keys(&:to_s).to_json
635
799
  end
636
800
 
637
801
  if revocationLog.nil?
638
802
  return [nil, "private revocation key not found"]
639
803
  end
640
804
 
805
+ # check if REVOCATION hash matches hash in TERMINATION
806
+ if did_info["log"][did_info["termination_log_id"]]["doc"] != multi_hash(canonical(revocationLog), LOG_HASH_OPTIONS).first
807
+ return [nil, "invalid revocation information"]
808
+ end
641
809
  revoc_log = JSON.parse(revocationLog)
642
810
  revoc_log["previous"] = [
643
811
  multi_hash(canonical(log_old[did_info["doc_log_id"].to_i]), LOG_HASH_OPTIONS).first,
@@ -736,15 +904,98 @@ class Oydid
736
904
  return [retVal, msg]
737
905
  end
738
906
 
739
- def self.w3c(did_info, options)
907
+ def self.delegate(did, options)
908
+ # check location
909
+ location = options[:doc_location]
910
+ if location.to_s == ""
911
+ location = DEFAULT_LOCATION
912
+ end
913
+ if did.include?(LOCATION_PREFIX)
914
+ tmp = did.split(LOCATION_PREFIX)
915
+ did = tmp[0]
916
+ location = tmp[1]
917
+ end
918
+ if did.include?(CGI.escape LOCATION_PREFIX)
919
+ tmp = did.split(CGI.escape LOCATION_PREFIX)
920
+ did = tmp[0]
921
+ location = tmp[1]
922
+ end
923
+ options[:doc_location] = location
924
+ options[:log_location] = location
925
+
926
+ if options[:ts].nil?
927
+ ts = Time.now.utc.to_i
928
+ else
929
+ ts = options[:ts]
930
+ end
931
+
932
+ # build log record
933
+ log = {}
934
+ log["ts"] = ts
935
+ log["op"] = 5 # DELEGATE
936
+ pwd = false
937
+ doc_privateKey, msg = getPrivateKey(options[:doc_enc], options[:doc_pwd], options[:doc_key], "", options)
938
+ rev_privateKey, msg = getPrivateKey(options[:rev_enc], options[:rev_pwd], options[:rev_key], "", options)
939
+ if !doc_privateKey.nil?
940
+ pwd="doc"
941
+ privateKey = doc_privateKey
942
+ end
943
+ if !rev_privateKey.nil?
944
+ pwd="rev"
945
+ privateKey = rev_privateKey
946
+ end
947
+ if !pwd || privateKey.to_s == ""
948
+ return [nil, "missing or invalid delegate key"]
949
+ end
950
+ log["doc"] = pwd + ":" + public_key(privateKey, options).first.to_s
951
+ log["sig"] = sign(privateKey, privateKey, options).first
952
+ log["previous"] = [did] # DID in previous cannot be resolved in the DAG but guarantees unique log hash
953
+
954
+ # revocation delegate keys need to specify a public key for encrypting the revocation record
955
+ if pwd == "rev"
956
+ publicEncryptionKey, msg = public_key(privateKey, {}, 'x25519-pub')
957
+ log["encryption-key"] = publicEncryptionKey
958
+ end
959
+ log_hash, msg = write_log(did, log, options)
960
+ if log_hash.nil?
961
+ return [nil, msg]
962
+ else
963
+ return [{"log": log_hash}, ""]
964
+ end
965
+ end
966
+
967
+ def self.w3c(did_info, options)
968
+ # check if doc is already W3C DID
969
+ is_already_w3c_did = (did_info.transform_keys(&:to_s)["doc"]["doc"].has_key?("@context") &&
970
+ did_info.transform_keys(&:to_s)["doc"]["doc"].has_key?("id") &&
971
+ did_info.transform_keys(&:to_s)["doc"]["doc"]["id"].split(":").first == "did") rescue false
972
+ if is_already_w3c_did
973
+ return did_info.transform_keys(&:to_s)["doc"]["doc"]
974
+ end
740
975
  did = percent_encode(did_info["did"])
741
976
  if !did.start_with?("did:oyd:")
742
977
  did = "did:oyd:" + did
743
978
  end
744
979
 
745
- didDoc = did_info.transform_keys(&:to_s)["doc"]
980
+ didDoc = did_info.dup.transform_keys(&:to_s)["doc"]
746
981
  pubDocKey = didDoc["key"].split(":")[0] rescue ""
747
982
  pubRevKey = didDoc["key"].split(":")[1] rescue ""
983
+ delegateDocKeys = getDelegatedPubKeysFromDID(did, "doc").first - [pubDocKey] rescue []
984
+ if delegateDocKeys.is_a?(String)
985
+ if delegateDocKeys == pubDocKey
986
+ delegateDocKeys = nil
987
+ else
988
+ delegateDocKeys = [delegateDocKeys]
989
+ end
990
+ end
991
+ delegateRevKeys = getDelegatedPubKeysFromDID(did, "rev").first - [pubRevKey] rescue []
992
+ if delegateRevKeys.is_a?(String)
993
+ if delegateRevKeys == pubRevKey
994
+ delegateRevKeys = nil
995
+ else
996
+ delegateRevKeys = [delegateRevKeys]
997
+ end
998
+ end
748
999
 
749
1000
  wd = {}
750
1001
  if didDoc["doc"].is_a?(Hash)
@@ -752,9 +1003,9 @@ class Oydid
752
1003
  wd["@context"] = ["https://www.w3.org/ns/did/v1", "https://w3id.org/security/suites/ed25519-2020/v1"]
753
1004
  else
754
1005
  if didDoc["doc"]["@context"].is_a?(Array)
755
- wd["@context"] = ["https://www.w3.org/ns/did/v1", "https://w3id.org/security/suites/ed25519-2020/v1"] + didDoc["doc"]["@context"]
1006
+ wd["@context"] = (["https://www.w3.org/ns/did/v1", "https://w3id.org/security/suites/ed25519-2020/v1"] + didDoc["doc"]["@context"]).uniq
756
1007
  else
757
- wd["@context"] = ["https://www.w3.org/ns/did/v1", "https://w3id.org/security/suites/ed25519-2020/v1", didDoc["doc"]["@context"]]
1008
+ wd["@context"] = (["https://www.w3.org/ns/did/v1", "https://w3id.org/security/suites/ed25519-2020/v1", didDoc["doc"]["@context"]]).uniq
758
1009
  end
759
1010
  didDoc["doc"].delete("@context")
760
1011
  end
@@ -773,6 +1024,36 @@ class Oydid
773
1024
  "controller": did,
774
1025
  "publicKeyMultibase": pubRevKey
775
1026
  }]
1027
+ if !delegateDocKeys.nil? && delegateDocKeys.count > 0
1028
+ i = 0
1029
+ wd["capabilityDelegation"] = []
1030
+ delegateDocKeys.each do |key|
1031
+ i += 1
1032
+ delegaton_object = {
1033
+ "id": did + "#key-delegate-doc-" + i.to_s,
1034
+ "type": "Ed25519VerificationKey2020",
1035
+ "controller": did,
1036
+ "publicKeyMultibase": key
1037
+ }
1038
+ wd["capabilityDelegation"] << delegaton_object
1039
+ end
1040
+ end
1041
+ if !delegateRevKeys.nil? && delegateRevKeys.count > 0
1042
+ i = 0
1043
+ if wd["capabilityDelegation"].nil?
1044
+ wd["capabilityDelegation"] = []
1045
+ end
1046
+ delegateRevKeys.each do |key|
1047
+ i += 1
1048
+ delegaton_object = {
1049
+ "id": did + "#key-delegate-rev-" + i.to_s,
1050
+ "type": "Ed25519VerificationKey2020",
1051
+ "controller": did,
1052
+ "publicKeyMultibase": key
1053
+ }
1054
+ wd["capabilityDelegation"] << delegaton_object
1055
+ end
1056
+ end
776
1057
 
777
1058
  equivalentIds = []
778
1059
  did_info["log"].each do |log|
@@ -784,7 +1065,7 @@ class Oydid
784
1065
  end
785
1066
  end unless did_info["log"].nil?
786
1067
  if equivalentIds.length > 0
787
- wd[:alsoKnownAs] = equivalentIds
1068
+ wd["alsoKnownAs"] = equivalentIds
788
1069
  end
789
1070
 
790
1071
  if didDoc["doc"].is_a?(Hash) && !didDoc["doc"]["service"].nil?
@@ -807,28 +1088,62 @@ class Oydid
807
1088
  else
808
1089
  payload = nil
809
1090
  if didDoc["doc"].is_a?(Hash)
810
- didDoc = didDoc["doc"]
811
- if didDoc["authentication"].to_s != ""
812
- wd["authentication"] = didDoc["authentication"]
813
- didDoc.delete("authentication")
814
- end
815
- if didDoc["assertionMethod"].to_s != ""
816
- wd["assertionMethod"] = didDoc["assertionMethod"]
817
- didDoc.delete("assertionMethod")
818
- end
819
- if didDoc["keyAgreement"].to_s != ""
820
- wd["keyAgreement"] = didDoc["keyAgreement"]
821
- didDoc.delete("keyAgreement")
822
- end
823
- if didDoc["capabilityInvocation"].to_s != ""
824
- wd["capabilityInvocation"] = didDoc["capabilityInvocation"]
825
- didDoc.delete("capabilityInvocation")
826
- end
827
- if didDoc["capabilityDelegation"].to_s != ""
828
- wd["capabilityDelegation"] = didDoc["capabilityDelegation"]
829
- didDoc.delete("capabilityDelegation")
1091
+ if didDoc["doc"] != {}
1092
+ didDoc = didDoc["doc"]
1093
+ if didDoc["authentication"].to_s != ""
1094
+ new_authentication = []
1095
+ didDoc["authentication"].each do |el|
1096
+ new_el = el.transform_keys(&:to_s)
1097
+ new_el["id"] = percent_encode(did) + new_el["id"]
1098
+ new_authentication << new_el
1099
+ end unless didDoc["authentication"].nil?
1100
+ if new_authentication.length > 0
1101
+ wd["authentication"] = new_authentication
1102
+ didDoc.delete("authentication")
1103
+ end
1104
+ end
1105
+ if didDoc["assertionMethod"].to_s != ""
1106
+ wd["assertionMethod"] = didDoc["assertionMethod"]
1107
+ didDoc.delete("assertionMethod")
1108
+ end
1109
+ if didDoc["keyAgreement"].to_s != ""
1110
+ new_keyAgreement = []
1111
+ didDoc["keyAgreement"].each do |el|
1112
+ new_el = el.transform_keys(&:to_s)
1113
+ new_el["id"] = percent_encode(did) + new_el["id"]
1114
+ new_keyAgreement << new_el
1115
+ end unless didDoc["keyAgreement"].nil?
1116
+ if new_keyAgreement.length > 0
1117
+ wd["keyAgreement"] = new_keyAgreement
1118
+ didDoc.delete("keyAgreement")
1119
+ end
1120
+ end
1121
+ if didDoc["capabilityInvocation"].to_s != ""
1122
+ wd["capabilityInvocation"] = didDoc["capabilityInvocation"]
1123
+ didDoc.delete("capabilityInvocation")
1124
+ end
1125
+ if didDoc["capabilityDelegation"].to_s != ""
1126
+ wd["capabilityDelegation"] = didDoc["capabilityDelegation"]
1127
+ didDoc.delete("capabilityDelegation")
1128
+ end
1129
+ if didDoc["alsoKnownAs"].to_s != ""
1130
+ if didDoc["alsoKnownAs"].is_a?(Array)
1131
+ dda = didDoc["alsoKnownAs"]
1132
+ else
1133
+ dda = [didDoc["alsoKnownAs"]]
1134
+ end
1135
+ if wd["alsoKnownAs"].nil?
1136
+ wd["alsoKnownAs"] = dda
1137
+ else
1138
+ wd["alsoKnownAs"] += dda
1139
+ end
1140
+ didDoc.delete("alsoKnownAs")
1141
+ end
1142
+ payload = didDoc
1143
+ if payload == {}
1144
+ payload = nil
1145
+ end
830
1146
  end
831
- payload = didDoc
832
1147
  else
833
1148
  payload = didDoc["doc"]
834
1149
  end
@@ -858,7 +1173,7 @@ class Oydid
858
1173
  end
859
1174
 
860
1175
 
861
- def self.w3c_legacy(did_info, options)
1176
+ def self.w3c_legacy(did_info, options)
862
1177
  did = did_info["did"]
863
1178
  if !did.start_with?("did:oyd:")
864
1179
  did = "did:oyd:" + did