oydid 0.5.4 → 0.5.6

Sign up to get free protection for your applications and to get access to all the features.
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