oydid 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (122) hide show
  1. checksums.yaml +7 -0
  2. data/AUTHORS +1 -0
  3. data/LICENSE +21 -0
  4. data/README.md +5 -0
  5. data/VERSION +1 -0
  6. data/lib/oydid/basic.rb +212 -0
  7. data/lib/oydid/log.rb +391 -0
  8. data/lib/oydid.rb +661 -0
  9. data/spec/input/basic/arrays.json +8 -0
  10. data/spec/input/basic/french.json +6 -0
  11. data/spec/input/basic/sample2_get_location.doc +1 -0
  12. data/spec/input/basic/sample2_retrieve_document.doc +1 -0
  13. data/spec/input/basic/sample3_retrieve_document.doc +1 -0
  14. data/spec/input/basic/sample4_retrieve_document.doc +1 -0
  15. data/spec/input/basic/sample5_retrieve_document.doc +1 -0
  16. data/spec/input/basic/sample_dec.doc +1 -0
  17. data/spec/input/basic/sample_enc.doc +1 -0
  18. data/spec/input/basic/sample_get_location.doc +1 -0
  19. data/spec/input/basic/sample_hash.doc +1 -0
  20. data/spec/input/basic/sample_invalid2_readkey.doc +1 -0
  21. data/spec/input/basic/sample_invalid2_verify.doc +1 -0
  22. data/spec/input/basic/sample_invalid3_readkey.doc +1 -0
  23. data/spec/input/basic/sample_invalid3_verify.doc +1 -0
  24. data/spec/input/basic/sample_invalid_privkey.doc +1 -0
  25. data/spec/input/basic/sample_invalid_readkey.doc +1 -0
  26. data/spec/input/basic/sample_invalid_sign.doc +1 -0
  27. data/spec/input/basic/sample_invalid_verify.doc +1 -0
  28. data/spec/input/basic/sample_key.doc +1 -0
  29. data/spec/input/basic/sample_readkey.doc +1 -0
  30. data/spec/input/basic/sample_retrieve_document.doc +1 -0
  31. data/spec/input/basic/sample_sign.doc +1 -0
  32. data/spec/input/basic/sample_valid_privkey.doc +1 -0
  33. data/spec/input/basic/sample_verify.doc +1 -0
  34. data/spec/input/basic/structures.json +8 -0
  35. data/spec/input/basic/unicode.json +3 -0
  36. data/spec/input/basic/values.json +5 -0
  37. data/spec/input/basic/wierd.json +11 -0
  38. data/spec/input/basic/zQmaBZTghn.doc +1 -0
  39. data/spec/input/log/sample0_dag2array.doc +1 -0
  40. data/spec/input/log/sample0_dag_did.doc +1 -0
  41. data/spec/input/log/sample1_dag_did.doc +1 -0
  42. data/spec/input/log/sample1_dag_update.doc +1 -0
  43. data/spec/input/log/sample2_dag_did.doc +1 -0
  44. data/spec/input/log/sample2_dag_update.doc +1 -0
  45. data/spec/input/log/sample2_retrieve_log.doc +1 -0
  46. data/spec/input/log/sample3_dag_did.doc +1 -0
  47. data/spec/input/log/sample3_dag_update.doc +1 -0
  48. data/spec/input/log/sample3_retrieve_log.doc +1 -0
  49. data/spec/input/log/sample4_dag_did.doc +1 -0
  50. data/spec/input/log/sample4_dag_update.doc +1 -0
  51. data/spec/input/log/sample4_retrieve_log.doc +1 -0
  52. data/spec/input/log/sample5_dag_update.doc +1 -0
  53. data/spec/input/log/sample5_retrieve_log.doc +1 -0
  54. data/spec/input/log/sample6_dag_update.doc +1 -0
  55. data/spec/input/log/sample6_retrieve_log.doc +1 -0
  56. data/spec/input/log/sample7_dag_update.doc +1 -0
  57. data/spec/input/log/sample7_retrieve_log.doc +1 -0
  58. data/spec/input/log/sample8_dag_update.doc +1 -0
  59. data/spec/input/log/sample_addhash.doc +1 -0
  60. data/spec/input/log/sample_dag_update.doc +1 -0
  61. data/spec/input/log/sample_match_log.doc +1 -0
  62. data/spec/input/log/sample_op1_addhash.doc +1 -0
  63. data/spec/input/log/sample_retrieve_log.doc +1 -0
  64. data/spec/input/main/sample0_read.doc +1 -0
  65. data/spec/output/basic/arrays.json +1 -0
  66. data/spec/output/basic/french.json +1 -0
  67. data/spec/output/basic/sample2_get_location.doc +1 -0
  68. data/spec/output/basic/sample2_retrieve_document.doc +1 -0
  69. data/spec/output/basic/sample3_retrieve_document.doc +1 -0
  70. data/spec/output/basic/sample4_retrieve_document.doc +1 -0
  71. data/spec/output/basic/sample5_retrieve_document.doc +1 -0
  72. data/spec/output/basic/sample_dec.doc +1 -0
  73. data/spec/output/basic/sample_enc.doc +1 -0
  74. data/spec/output/basic/sample_get_location.doc +1 -0
  75. data/spec/output/basic/sample_hash.doc +1 -0
  76. data/spec/output/basic/sample_invalid2_readkey.doc +1 -0
  77. data/spec/output/basic/sample_invalid2_verify.doc +1 -0
  78. data/spec/output/basic/sample_invalid3_readkey.doc +1 -0
  79. data/spec/output/basic/sample_invalid3_verify.doc +1 -0
  80. data/spec/output/basic/sample_invalid_privkey.doc +1 -0
  81. data/spec/output/basic/sample_invalid_readkey.doc +1 -0
  82. data/spec/output/basic/sample_invalid_sign.doc +1 -0
  83. data/spec/output/basic/sample_invalid_verify.doc +1 -0
  84. data/spec/output/basic/sample_key.doc +1 -0
  85. data/spec/output/basic/sample_readkey.doc +1 -0
  86. data/spec/output/basic/sample_retrieve_document.doc +1 -0
  87. data/spec/output/basic/sample_sign.doc +1 -0
  88. data/spec/output/basic/sample_valid_privkey.doc +1 -0
  89. data/spec/output/basic/sample_verify.doc +1 -0
  90. data/spec/output/basic/structures.json +1 -0
  91. data/spec/output/basic/unicode.json +1 -0
  92. data/spec/output/basic/values.json +1 -0
  93. data/spec/output/basic/wierd.json +1 -0
  94. data/spec/output/log/sample0_dag2array.doc +1 -0
  95. data/spec/output/log/sample0_dag_did.doc +1 -0
  96. data/spec/output/log/sample1_dag_did.doc +1 -0
  97. data/spec/output/log/sample1_dag_update.doc +1 -0
  98. data/spec/output/log/sample2_dag_did.doc +1 -0
  99. data/spec/output/log/sample2_dag_update.doc +1 -0
  100. data/spec/output/log/sample2_retrieve_log.doc +1 -0
  101. data/spec/output/log/sample3_dag_did.doc +1 -0
  102. data/spec/output/log/sample3_dag_update.doc +1 -0
  103. data/spec/output/log/sample3_retrieve_log.doc +1 -0
  104. data/spec/output/log/sample4_dag_did.doc +1 -0
  105. data/spec/output/log/sample4_dag_update.doc +1 -0
  106. data/spec/output/log/sample4_retrieve_log.doc +1 -0
  107. data/spec/output/log/sample5_dag_update.doc +1 -0
  108. data/spec/output/log/sample5_retrieve_log.doc +1 -0
  109. data/spec/output/log/sample6_dag_update.doc +1 -0
  110. data/spec/output/log/sample6_retrieve_log.doc +1 -0
  111. data/spec/output/log/sample7_dag_update.doc +1 -0
  112. data/spec/output/log/sample7_retrieve_log.doc +1 -0
  113. data/spec/output/log/sample8_dag_update.doc +1 -0
  114. data/spec/output/log/sample_addhash.doc +1 -0
  115. data/spec/output/log/sample_dag_update.doc +1 -0
  116. data/spec/output/log/sample_match_log.doc +1 -0
  117. data/spec/output/log/sample_op1_addhash.doc +1 -0
  118. data/spec/output/log/sample_retrieve_log.doc +1 -0
  119. data/spec/output/main/sample0_read.doc +1 -0
  120. data/spec/oydid_spec.rb +170 -0
  121. data/spec/spec_helper.rb +31 -0
  122. metadata +405 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 84460bc149df8e10a4f5094879894399bd3d6670ff5ef1fc8c500811af5fb5e2
4
+ data.tar.gz: 3515b7f22cf286b3abda13299fd15a6a713f39a2048c8f1d3553f36832577e24
5
+ SHA512:
6
+ metadata.gz: 1eadd2c6dfba9ef8788e72514b3c12561e8628a20c6fc14bb21172d3777227bde42accb6dfe5cda14ccdeb027cb53415c179193cbd70bf1fa637b1bfe1208078
7
+ data.tar.gz: 9e0f47ca1316e508faf65af727afb8a61de3a89e023d789d21ce69ca9328da46f4d1c9a197ee7486aba3c460476112bebf919803c26b7f0c06896f911b1ecfa6
data/AUTHORS ADDED
@@ -0,0 +1 @@
1
+ * Christoph Fabianek <christoph@ownyourdata.eu>
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2022 OwnYourData
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,5 @@
1
+ # OYDID Gem
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/oydid.png)](http://badge.fury.io/rb/oydid)
4
+ [![Build Status](https://github.com/ownyourdata/oydid/workflows/CI/badge.svg)](https://github.com/ownyourdata/oydid/actions?query=workflow%3ACI)
5
+ [![Coverage Status](https://coveralls.io/repos/github/OwnYourData/oydid/badge.svg?branch=main)](https://coveralls.io/github/OwnYourData/oydid?branch=main)
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.2.0
@@ -0,0 +1,212 @@
1
+ # -*- encoding: utf-8 -*-
2
+ # frozen_string_literal: true
3
+
4
+ class Oydid
5
+
6
+ # basic functions ---------------------------
7
+ def self.encode(message, method = "base58btc")
8
+ Multibases.pack(method, message).to_s
9
+ end
10
+
11
+ def self.decode(message)
12
+ Multibases.unpack(message).decode.to_s('ASCII-8BIT')
13
+ end
14
+
15
+ def self.hash(message)
16
+ encode(Multihashes.encode(RbNaCl::Hash.sha256(message), "sha2-256").unpack('C*'))
17
+ end
18
+
19
+ def self.canonical(message)
20
+ if message.is_a? String
21
+ message = JSON.parse(message) rescue message
22
+ else
23
+ message = JSON.parse(message.to_json) rescue message
24
+ end
25
+ message.to_json_c14n
26
+ end
27
+
28
+ # key management ----------------------------
29
+ def self.generate_private_key(input, method = "ed25519-priv")
30
+ begin
31
+ omc = Multicodecs[method].code
32
+ rescue
33
+ return [nil, "unknown key codec"]
34
+ end
35
+
36
+ case Multicodecs[method].name
37
+ when 'ed25519-priv'
38
+ if input != ""
39
+ raw_key = Ed25519::SigningKey.new(RbNaCl::Hash.sha256(input)).to_bytes
40
+ else
41
+ raw_key = Ed25519::SigningKey.generate.to_bytes
42
+ end
43
+ else
44
+ return [nil, "unsupported key codec"]
45
+ end
46
+ length = raw_key.bytesize
47
+ return [encode([omc, length, raw_key].pack("SCa#{length}")), ""]
48
+ end
49
+
50
+ def self.public_key(private_key)
51
+ code, length, digest = decode(private_key).unpack('SCa*')
52
+ case Multicodecs[code].name
53
+ when 'ed25519-priv'
54
+ public_key = Ed25519::SigningKey.new(digest).verify_key
55
+ length = public_key.to_bytes.bytesize
56
+ return [encode([Multicodecs['ed25519-pub'].code, length, public_key].pack("CCa#{length}")), ""]
57
+ else
58
+ return [nil, "unsupported key codec"]
59
+ end
60
+ end
61
+
62
+ def self.sign(message, private_key)
63
+ code, length, digest = decode(private_key).unpack('SCa*')
64
+ case Multicodecs[code].name
65
+ when 'ed25519-priv'
66
+ return [encode(Ed25519::SigningKey.new(digest).sign(message)), ""]
67
+ else
68
+ return [nil, "unsupported key codec"]
69
+ end
70
+ end
71
+
72
+ def self.verify(message, signature, public_key)
73
+ begin
74
+ code, length, digest = decode(public_key).unpack('CCa*')
75
+ case Multicodecs[code].name
76
+ when 'ed25519-pub'
77
+ verify_key = Ed25519::VerifyKey.new(digest)
78
+ signature_verification = false
79
+ begin
80
+ verify_key.verify(decode(signature), message)
81
+ signature_verification = true
82
+ rescue Ed25519::VerifyError
83
+ signature_verification = false
84
+ end
85
+ return [signature_verification, ""]
86
+ else
87
+ return [nil, "unsupported key codec"]
88
+ end
89
+ rescue
90
+ return [nil, "unknown key codec"]
91
+ end
92
+ end
93
+
94
+ def self.read_private_key(filename)
95
+ begin
96
+ f = File.open(filename)
97
+ key_encoded = f.read
98
+ f.close
99
+ rescue
100
+ return [nil, "cannot read file"]
101
+ end
102
+ decode_private_key(key_encoded)
103
+ end
104
+
105
+ def self.decode_private_key(key_encoded)
106
+ begin
107
+ code, length, digest = decode(key_encoded).unpack('SCa*')
108
+ case Multicodecs[code].name
109
+ when 'ed25519-priv'
110
+ private_key = Ed25519::SigningKey.new(digest).to_bytes
111
+ else
112
+ return [nil, "unsupported key codec"]
113
+ end
114
+ length = private_key.bytesize
115
+ return [Oydid.encode([code, length, private_key].pack("SCa#{length}")), ""]
116
+ rescue
117
+ return [nil, "invalid key"]
118
+ end
119
+ end
120
+
121
+ # storage functions -----------------------------
122
+ def self.write_private_storage(payload, filename)
123
+ File.open(filename, 'w') {|f| f.write(payload)}
124
+ end
125
+
126
+ def self.read_private_storage(filename)
127
+ File.open(filename, 'r') { |f| f.read }
128
+ end
129
+
130
+ def self.get_location(id)
131
+ if id.include?(LOCATION_PREFIX)
132
+ id_split = id.split(LOCATION_PREFIX)
133
+ return id_split[1]
134
+ else
135
+ if id.include?(CGI.escape(LOCATION_PREFIX))
136
+ id_split = id.split(CGI.escape(LOCATION_PREFIX))
137
+ return id_split[1]
138
+ else
139
+ return DEFAULT_LOCATION
140
+ end
141
+ end
142
+ end
143
+
144
+ def self.retrieve_document(doc_hash, doc_file, doc_location, options)
145
+ if doc_location == ""
146
+ doc_location = DEFAULT_LOCATION
147
+ end
148
+ if !(doc_location == "" || doc_location == "local")
149
+ if !doc_location.start_with?("http")
150
+ doc_location = "https://" + doc_location
151
+ end
152
+ end
153
+
154
+ case doc_location
155
+ when /^http/
156
+ retVal = HTTParty.get(doc_location + "/doc/" + doc_hash)
157
+ if retVal.code != 200
158
+ msg = retVal.parsed_response("error").to_s rescue "invalid response from " + doc_location.to_s + "/doc/" + doc_hash.to_s
159
+ return [nil, msg]
160
+ end
161
+ if options.transform_keys(&:to_s)["trace"]
162
+ if options[:silent].nil? || !options[:silent]
163
+ puts "GET " + doc_hash + " from " + doc_location
164
+ end
165
+ end
166
+ return [retVal.parsed_response, ""]
167
+ when "", "local"
168
+ doc = JSON.parse(read_private_storage(doc_file)) rescue {}
169
+ if doc == {}
170
+ return [nil, "cannot read file"]
171
+ else
172
+ return [doc, ""]
173
+ end
174
+ end
175
+ end
176
+
177
+ def self.retrieve_document_raw(doc_hash, doc_file, doc_location, options)
178
+ if doc_location == ""
179
+ doc_location = DEFAULT_LOCATION
180
+ end
181
+ if !(doc_location == "" || doc_location == "local")
182
+ if !doc_location.start_with?("http")
183
+ doc_location = "https://" + doc_location
184
+ end
185
+ end
186
+
187
+ case doc_location
188
+ when /^http/
189
+ retVal = HTTParty.get(doc_location + "/doc_raw/" + doc_hash)
190
+ if retVal.code != 200
191
+ msg = retVal.parsed_response("error").to_s rescue "invalid response from " + doc_location.to_s + "/doc/" + doc_hash.to_s
192
+ return [nil, msg]
193
+ end
194
+ if options.transform_keys(&:to_s)["trace"]
195
+ if options[:silent].nil? || !options[:silent]
196
+ puts "GET " + doc_hash + " from " + doc_location
197
+ end
198
+ end
199
+ return [retVal.parsed_response, ""]
200
+ when "", "local"
201
+ doc = JSON.parse(read_private_storage(doc_file)) rescue {}
202
+ log = JSON.parse(read_private_storage(doc_file.sub(".doc", ".log"))) rescue {}
203
+ if doc == {}
204
+ return [nil, "cannot read file"]
205
+ else
206
+ obj = {"doc" => doc, "log" => log}
207
+ return [obj, ""]
208
+ end
209
+ end
210
+ end
211
+
212
+ end
data/lib/oydid/log.rb ADDED
@@ -0,0 +1,391 @@
1
+ # -*- encoding: utf-8 -*-
2
+ # frozen_string_literal: true
3
+
4
+ class Oydid
5
+ # log functions -----------------------------
6
+ def self.add_hash(log)
7
+ log.map do |item|
8
+ i = item.dup
9
+ i.delete("previous")
10
+ item["entry-hash"] = hash(canonical(item))
11
+ if item.transform_keys(&:to_s)["op"] == 1
12
+ item["sub-entry-hash"] = hash(canonical(i))
13
+ end
14
+ item
15
+ end
16
+ end
17
+
18
+ # check if signature matches current document
19
+ # check if signature in log is correct
20
+ def self.match_log_did?(log, doc)
21
+ message = log["doc"].to_s
22
+ signature = log["sig"].to_s
23
+ public_keys = doc["key"].to_s
24
+ public_key = public_keys.split(":")[0] rescue ""
25
+ return verify(message, signature, public_key).first
26
+ end
27
+
28
+ def self.retrieve_log(did_hash, log_file, log_location, options)
29
+ if log_location == ""
30
+ log_location = DEFAULT_LOCATION
31
+ end
32
+ if !(log_location == "" || log_location == "local")
33
+ if !log_location.start_with?("http")
34
+ log_location = "https://" + log_location
35
+ end
36
+ end
37
+
38
+ case log_location
39
+ when /^http/
40
+ retVal = HTTParty.get(log_location + "/log/" + did_hash)
41
+ if retVal.code != 200
42
+ msg = retVal.parsed_response("error").to_s rescue
43
+ "invalid response from " + log_location.to_s + "/log/" + did_hash.to_s
44
+
45
+ return [nil, msg]
46
+ end
47
+ if options.transform_keys(&:to_s)["trace"]
48
+ if options[:silent].nil? || !options[:silent]
49
+ puts "GET log for " + did_hash + " from " + log_location
50
+ end
51
+ end
52
+ retVal = JSON.parse(retVal.to_s) rescue nil
53
+ return [retVal, ""]
54
+ when "", "local"
55
+ doc = JSON.parse(read_private_storage(log_file)) rescue {}
56
+ if doc == {}
57
+ return [nil, "cannot read file '" + log_file + "'"]
58
+ end
59
+ return [doc, ""]
60
+ end
61
+ end
62
+
63
+ def self.dag_did(logs, options)
64
+ dag = DAG.new
65
+ dag_log = []
66
+ log_hash = []
67
+
68
+ # calculate hash values for each entry and build vertices
69
+ i = 0
70
+ create_entries = 0
71
+ create_index = nil
72
+ terminate_indices = []
73
+ logs.each do |el|
74
+ if el["op"].to_i == 2
75
+ create_entries += 1
76
+ create_index = i
77
+ end
78
+ if el["op"].to_i == 0
79
+ terminate_indices << i
80
+ end
81
+ log_hash << Oydid.hash(Oydid.canonical(el))
82
+ dag_log << dag.add_vertex(id: i)
83
+ i += 1
84
+ end unless logs.nil?
85
+
86
+ if create_entries != 1
87
+ return [nil, nil, nil, "wrong number of CREATE entries (" + create_entries.to_s + ") in log" ]
88
+ end
89
+ if terminate_indices.length == 0
90
+ return [nil, nil, nil, "missing TERMINATE entries" ]
91
+ end
92
+
93
+ # create edges between vertices
94
+ i = 0
95
+ logs.each do |el|
96
+ el["previous"].each do |p|
97
+ position = log_hash.find_index(p)
98
+ if !position.nil?
99
+ dag.add_edge from: dag_log[position], to: dag_log[i]
100
+ end
101
+ end unless el["previous"] == []
102
+ i += 1
103
+ end unless logs.nil?
104
+
105
+ # identify tangling TERMINATE entry
106
+ i = 0
107
+ terminate_entries = 0
108
+ terminate_overall = 0
109
+ terminate_index = nil
110
+ logs.each do |el|
111
+ if el["op"].to_i == 0
112
+ if dag.vertices[i].successors.length == 0
113
+ terminate_entries += 1
114
+ terminate_index = i
115
+ end
116
+ terminate_overall += 1
117
+ end
118
+ i += 1
119
+ end unless logs.nil?
120
+
121
+ if terminate_entries != 1 && !options[:log_complete]
122
+ if options[:silent].nil? || !options[:silent]
123
+ return [nil, nil, nil, "cannot resolve DID" ]
124
+ end
125
+ end
126
+ return [dag, create_index, terminate_index, ""]
127
+ end
128
+
129
+ def self.dag2array(dag, log_array, index, result, options)
130
+ if options.transform_keys(&:to_s)["trace"]
131
+ if options[:silent].nil? || !options[:silent]
132
+ puts " vertex " + index.to_s + " at " + log_array[index]["ts"].to_s + " op: " + log_array[index]["op"].to_s + " doc: " + log_array[index]["doc"].to_s
133
+ end
134
+ end
135
+ result << log_array[index]
136
+ dag.vertices[index].successors.each do |s|
137
+ # check if successor has predecessor that is not self (i.e. REVOKE with TERMINATE)
138
+ s.predecessors.each do |p|
139
+ if p[:id] != index
140
+ if options.transform_keys(&:to_s)["trace"]
141
+ if options[:silent].nil? || !options[:silent]
142
+ puts " vertex " + p[:id].to_s + " at " + log_array[p[:id]]["ts"].to_s + " op: " + log_array[p[:id]]["op"].to_s + " doc: " + log_array[p[:id]]["doc"].to_s
143
+ end
144
+ end
145
+ result << log_array[p[:id]]
146
+ end
147
+ end unless s.predecessors.length < 2
148
+ dag2array(dag, log_array, s[:id], result, options)
149
+ end unless dag.vertices[index].successors.count == 0
150
+ result
151
+ end
152
+
153
+ def self.dag_update(currentDID, options)
154
+ i = 0
155
+ initial_did = currentDID["did"].to_s
156
+ initial_did = initial_did.delete_prefix("did:oyd:")
157
+ initial_did = initial_did.split("@").first
158
+ current_public_doc_key = ""
159
+ verification_output = false
160
+ currentDID["log"].each do |el|
161
+ case el["op"]
162
+ when 2,3 # CREATE, UPDATE
163
+ currentDID["doc_log_id"] = i
164
+
165
+ doc_did = el["doc"]
166
+ doc_location = get_location(doc_did)
167
+ did_hash = doc_did.delete_prefix("did:oyd:")
168
+ did_hash = did_hash.split("@").first
169
+ did10 = did_hash[0,10]
170
+ doc = retrieve_document_raw(doc_did, did10 + ".doc", doc_location, {})
171
+ if doc.first.nil?
172
+ currentDID["error"] = 2
173
+ msg = doc.last.to_s
174
+ if msg == ""
175
+ msg = "cannot retrieve " + doc_did.to_s
176
+ end
177
+ currentDID["message"] = msg
178
+ return currentDID
179
+ end
180
+ doc = doc.first["doc"]
181
+ if el["op"] == 2 # CREATE
182
+ if !match_log_did?(el, doc)
183
+ currentDID["error"] = 1
184
+ currentDID["message"] = "Signatures in log don't match"
185
+ return currentDID
186
+ end
187
+ end
188
+ currentDID["did"] = doc_did
189
+ currentDID["doc"] = doc
190
+ # since hash is guaranteed during retrieve_document this check is not necessary
191
+ # if hash(canonical(doc)) != did_hash
192
+ # currentDID["error"] = 1
193
+ # currentDID["message"] = "DID identifier and DID document don't match"
194
+ # if did_hash == initial_did
195
+ # verification_output = true
196
+ # end
197
+ # if verification_output
198
+ # currentDID["verification"] += "identifier: " + did_hash.to_s + "\n"
199
+ # currentDID["verification"] += "⛔ does not match DID Document:" + "\n"
200
+ # currentDID["verification"] += JSON.pretty_generate(doc) + "\n"
201
+ # currentDID["verification"] += "(Details: https://ownyourdata.github.io/oydid/#calculate_hash)" + "\n\n"
202
+ # end
203
+ # return currentDID
204
+ # end
205
+ if did_hash == initial_did
206
+ verification_output = true
207
+ end
208
+ if verification_output
209
+ currentDID["verification"] += "identifier: " + did_hash.to_s + "\n"
210
+ currentDID["verification"] += "✅ is hash of DID Document:" + "\n"
211
+ currentDID["verification"] += JSON.pretty_generate(doc) + "\n"
212
+ currentDID["verification"] += "(Details: https://ownyourdata.github.io/oydid/#calculate_hash)" + "\n\n"
213
+ end
214
+ current_public_doc_key = currentDID["doc"]["key"].split(":").first rescue ""
215
+
216
+ when 0 # TERMINATE
217
+ currentDID["termination_log_id"] = i
218
+
219
+ doc_did = currentDID["did"]
220
+ doc_location = get_location(doc_did)
221
+ did_hash = doc_did.delete_prefix("did:oyd:")
222
+ did_hash = did_hash.split("@").first
223
+ did10 = did_hash[0,10]
224
+ doc = retrieve_document_raw(doc_did, did10 + ".doc", doc_location, {})
225
+ # since it retrieves a DID that previously existed, this test is not necessary
226
+ # if doc.first.nil?
227
+ # currentDID["error"] = 2
228
+ # currentDID["message"] = doc.last.to_s
229
+ # return currentDID
230
+ # end
231
+ doc = doc.first["doc"]
232
+ term = doc["log"]
233
+ log_location = term.split("@")[1] rescue ""
234
+ if log_location.to_s == ""
235
+ log_location = DEFAULT_LOCATION
236
+ end
237
+ term = term.split("@").first
238
+ if hash(canonical(el)) != term
239
+ currentDID["error"] = 1
240
+ currentDID["message"] = "Log reference and record don't match"
241
+ if verification_output
242
+ currentDID["verification"] += "'log' reference in DID Document: " + term.to_s + "\n"
243
+ currentDID["verification"] += "⛔ does not match TERMINATE log record:" + "\n"
244
+ currentDID["verification"] += JSON.pretty_generate(el) + "\n"
245
+ currentDID["verification"] += "(Details: https://ownyourdata.github.io/oydid/#calculate_hash)" + "\n\n"
246
+ end
247
+ return currentDID
248
+ end
249
+ if verification_output
250
+ currentDID["verification"] += "'log' reference in DID Document: " + term.to_s + "\n"
251
+ currentDID["verification"] += "✅ is hash of TERMINATE log record:" + "\n"
252
+ currentDID["verification"] += JSON.pretty_generate(el) + "\n"
253
+ currentDID["verification"] += "(Details: https://ownyourdata.github.io/oydid/#calculate_hash)" + "\n\n"
254
+ end
255
+
256
+ # check if there is a revocation entry
257
+ revocation_record = {}
258
+ revoc_term = el["doc"]
259
+ revoc_term = revoc_term.split("@").first
260
+ revoc_term_found = false
261
+ log_array, msg = retrieve_log(did_hash, did10 + ".log", log_location, options)
262
+ log_array.each do |log_el|
263
+ log_el_structure = log_el.dup
264
+ if log_el["op"].to_i == 1 # TERMINATE
265
+ log_el_structure.delete("previous")
266
+ end
267
+ if hash(canonical(log_el_structure)) == revoc_term
268
+ revoc_term_found = true
269
+ revocation_record = log_el.dup
270
+ if verification_output
271
+ currentDID["verification"] += "'doc' reference in TERMINATE log record: " + revoc_term.to_s + "\n"
272
+ currentDID["verification"] += "✅ is hash of REVOCATION log record (without 'previous' attribute):" + "\n"
273
+ currentDID["verification"] += JSON.pretty_generate(log_el) + "\n"
274
+ currentDID["verification"] += "(Details: https://ownyourdata.github.io/oydid/#calculate_hash)" + "\n\n"
275
+ end
276
+ break
277
+ end
278
+ end unless log_array.nil?
279
+ # this should actually be covered by retrieve_log in the block above
280
+ # (actually I wasn't able to craft a test case covering this part...)
281
+ # if !options.transform_keys(&:to_s)["log_location"].nil?
282
+ # log_array, msg = retrieve_log(revoc_term, did10 + ".log", options.transform_keys(&:to_s)["log_location"], options)
283
+ # log_array.each do |log_el|
284
+ # if log_el["op"] == 1 # TERMINATE
285
+ # log_el_structure = log_el.delete("previous")
286
+ # else
287
+ # log_el_structure = log_el
288
+ # end
289
+ # if hash(canonical(log_el_structure)) == revoc_term
290
+ # revoc_term_found = true
291
+ # revocation_record = log_el.dup
292
+ # if verification_output
293
+ # currentDID["verification"] += "'doc' reference in TERMINATE log record: " + revoc_term.to_s + "\n"
294
+ # currentDID["verification"] += "✅ is hash of REVOCATION log record (without 'previous' attribute):" + "\n"
295
+ # currentDID["verification"] += JSON.pretty_generate(log_el) + "\n"
296
+ # currentDID["verification"] += "(Details: https://ownyourdata.github.io/oydid/#calculate_hash)" + "\n\n"
297
+ # end
298
+ # break
299
+ # end
300
+ # end
301
+ # end
302
+
303
+ if revoc_term_found
304
+ update_term_found = false
305
+ log_array.each do |log_el|
306
+ if log_el["op"].to_i == 3
307
+ if log_el["previous"].include?(hash(canonical(revocation_record)))
308
+ update_term_found = true
309
+ message = log_el["doc"].to_s
310
+
311
+ signature = log_el["sig"]
312
+ public_key = current_public_doc_key.to_s
313
+ signature_verification = verify(message, signature, public_key).first
314
+ if signature_verification
315
+ if verification_output
316
+ currentDID["verification"] += "found UPDATE log record:" + "\n"
317
+ currentDID["verification"] += JSON.pretty_generate(log_el) + "\n"
318
+ currentDID["verification"] += "✅ public key from last DID Document: " + current_public_doc_key.to_s + "\n"
319
+ currentDID["verification"] += "verifies 'doc' reference of new DID Document: " + log_el["doc"].to_s + "\n"
320
+ currentDID["verification"] += log_el["sig"].to_s + "\n"
321
+ currentDID["verification"] += "of next DID Document (Details: https://ownyourdata.github.io/oydid/#verify_signature)" + "\n"
322
+
323
+ next_doc_did = log_el["doc"].to_s
324
+ next_doc_location = get_location(next_doc_did)
325
+ next_did_hash = next_doc_did.delete_prefix("did:oyd:")
326
+ next_did_hash = next_did_hash.split("@").first
327
+ next_did10 = next_did_hash[0,10]
328
+ next_doc = retrieve_document_raw(next_doc_did, next_did10 + ".doc", next_doc_location, {})
329
+ if next_doc.first.nil?
330
+ currentDID["error"] = 2
331
+ currentDID["message"] = next_doc.last
332
+ return currentDID
333
+ end
334
+ next_doc = next_doc.first["doc"]
335
+ if public_key == next_doc["key"].split(":").first
336
+ currentDID["verification"] += "⚠️ no key rotation in updated DID Document" + "\n"
337
+ end
338
+ currentDID["verification"] += "\n"
339
+ end
340
+ else
341
+ currentDID["error"] = 1
342
+ currentDID["message"] = "Signature does not match"
343
+ if verification_output
344
+ new_doc_did = log_el["doc"].to_s
345
+ new_doc_location = get_location(new_doc_did)
346
+ new_did_hash = new_doc_did.delete_prefix("did:oyd:")
347
+ new_did_hash = new_did_hash.split("@").first
348
+ new_did10 = new_did_hash[0,10]
349
+ new_doc = retrieve_document(new_doc_did, new_did10 + ".doc", new_doc_location, {}).first
350
+ currentDID["verification"] += "found UPDATE log record:" + "\n"
351
+ currentDID["verification"] += JSON.pretty_generate(log_el) + "\n"
352
+ currentDID["verification"] += "⛔ public key from last DID Document: " + current_public_doc_key.to_s + "\n"
353
+ currentDID["verification"] += "does not verify 'doc' reference of new DID Document: " + log_el["doc"].to_s + "\n"
354
+ currentDID["verification"] += log_el["sig"].to_s + "\n"
355
+ currentDID["verification"] += "next DID Document (Details: https://ownyourdata.github.io/oydid/#verify_signature)" + "\n"
356
+ currentDID["verification"] += JSON.pretty_generate(new_doc) + "\n\n"
357
+ end
358
+ return currentDID
359
+ end
360
+ break
361
+ end
362
+ end
363
+ end
364
+
365
+ else
366
+ if verification_output
367
+ currentDID["verification"] += "Revocation reference in log record: " + revoc_term.to_s + "\n"
368
+ currentDID["verification"] += "✅ cannot find revocation record searching at" + "\n"
369
+ currentDID["verification"] += "- " + log_location + "\n"
370
+ if !options.transform_keys(&:to_s)["log_location"].nil?
371
+ currentDID["verification"] += "- " + options.transform_keys(&:to_s)["log_location"].to_s + "\n"
372
+ end
373
+ currentDID["verification"] += "(Details: https://ownyourdata.github.io/oydid/#retrieve_log)" + "\n\n"
374
+ end
375
+ break
376
+ end
377
+ when 1 # revocation log entry
378
+ # do nothing
379
+ else
380
+ currentDID["error"] = 2
381
+ currentDID["message"] = "FATAL ERROR: op code '" + el["op"].to_s + "' not implemented"
382
+ return currentDID
383
+
384
+ end
385
+ i += 1
386
+ end unless currentDID["log"].nil?
387
+
388
+ return currentDID
389
+ end
390
+
391
+ end