class Oydid
Constants
- DEFAULT_LOCATION
- LOCATION_PREFIX
Public Class Methods
add_hash(log)
click to toggle source
log functions —————————–
# File lib/oydid/log.rb, line 5 def self.add_hash(log) log.map do |item| i = item.dup i.delete("previous") item["entry-hash"] = hash(canonical(item)) if item.transform_keys(&:to_s)["op"] == 1 item["sub-entry-hash"] = hash(canonical(i)) end item end end
canonical(message)
click to toggle source
# File lib/oydid/basic.rb, line 18 def self.canonical(message) if message.is_a? String message = JSON.parse(message) rescue message else message = JSON.parse(message.to_json) rescue message end message.to_json_c14n end
clone(did, options)
click to toggle source
# File lib/oydid.rb, line 616 def self.clone(did, options) # check if locations differ target_location = options[:doc_location] if target_location.to_s == "" target_location = DEFAULT_LOCATION end if did.include?(LOCATION_PREFIX) hash_split = did.split(LOCATION_PREFIX) did = hash_split[0] source_location = hash_split[1] end if source_location.to_s == "" source_location = DEFAULT_LOCATION end if target_location == source_location return [nil, "cannot clone to same location (" + target_location.to_s + ")"] end # get original did info options[:doc_location] = source_location options[:log_location] = source_location source_did, msg = read(did, options) if source_did.nil? return [nil, "cannot resolve DID (on cloning DID)"] end if source_did["error"] != 0 return [nil, source_did["message"].to_s] end if source_did["doc_log_id"].nil? return [nil, "cannot parse DID log"] end source_log = source_did["log"].first(source_did["doc_log_id"] + 1).last.to_json # write did to new location options[:doc_location] = target_location options[:log_location] = target_location options[:previous_clone] = hash(canonical(source_log)) + LOCATION_PREFIX + source_location options[:source_location] = source_location options[:source_did] = source_did["did"] retVal, msg = write(source_did["doc"]["doc"], nil, "clone", options) return [retVal, msg] end
create(content, options)
click to toggle source
# File lib/oydid.rb, line 131 def self.create(content, options) return write(content, nil, "create", options) end
dag2array(dag, log_array, index, result, options)
click to toggle source
# File lib/oydid/log.rb, line 128 def self.dag2array(dag, log_array, index, result, options) if options.transform_keys(&:to_s)["trace"] if options[:silent].nil? || !options[:silent] 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 end end result << log_array[index] dag.vertices[index].successors.each do |s| # check if successor has predecessor that is not self (i.e. REVOKE with TERMINATE) s.predecessors.each do |p| if p[:id] != index if options.transform_keys(&:to_s)["trace"] if options[:silent].nil? || !options[:silent] 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 end end result << log_array[p[:id]] end end unless s.predecessors.length < 2 dag2array(dag, log_array, s[:id], result, options) end unless dag.vertices[index].successors.count == 0 result end
dag_did(logs, options)
click to toggle source
# File lib/oydid/log.rb, line 62 def self.dag_did(logs, options) dag = DAG.new dag_log = [] log_hash = [] # calculate hash values for each entry and build vertices i = 0 create_entries = 0 create_index = nil terminate_indices = [] logs.each do |el| if el["op"].to_i == 2 create_entries += 1 create_index = i end if el["op"].to_i == 0 terminate_indices << i end log_hash << Oydid.hash(Oydid.canonical(el)) dag_log << dag.add_vertex(id: i) i += 1 end unless logs.nil? if create_entries != 1 return [nil, nil, nil, "wrong number of CREATE entries (" + create_entries.to_s + ") in log" ] end if terminate_indices.length == 0 return [nil, nil, nil, "missing TERMINATE entries" ] end # create edges between vertices i = 0 logs.each do |el| el["previous"].each do |p| position = log_hash.find_index(p) if !position.nil? dag.add_edge from: dag_log[position], to: dag_log[i] end end unless el["previous"] == [] i += 1 end unless logs.nil? # identify tangling TERMINATE entry i = 0 terminate_entries = 0 terminate_overall = 0 terminate_index = nil logs.each do |el| if el["op"].to_i == 0 if dag.vertices[i].successors.length == 0 terminate_entries += 1 terminate_index = i end terminate_overall += 1 end i += 1 end unless logs.nil? if terminate_entries != 1 && !options[:log_complete] if options[:silent].nil? || !options[:silent] return [nil, nil, nil, "cannot resolve DID" ] end end return [dag, create_index, terminate_index, ""] end
dag_update(currentDID, options)
click to toggle source
# File lib/oydid/log.rb, line 152 def self.dag_update(currentDID, options) i = 0 initial_did = currentDID["did"].to_s initial_did = initial_did.delete_prefix("did:oyd:") initial_did = initial_did.split("@").first current_public_doc_key = "" verification_output = false currentDID["log"].each do |el| case el["op"] when 2,3 # CREATE, UPDATE currentDID["doc_log_id"] = i doc_did = el["doc"] doc_location = get_location(doc_did) did_hash = doc_did.delete_prefix("did:oyd:") did_hash = did_hash.split("@").first did10 = did_hash[0,10] doc = retrieve_document_raw(doc_did, did10 + ".doc", doc_location, {}) if doc.first.nil? currentDID["error"] = 2 msg = doc.last.to_s if msg == "" msg = "cannot retrieve " + doc_did.to_s end currentDID["message"] = msg return currentDID end doc = doc.first["doc"] if el["op"] == 2 # CREATE if !match_log_did?(el, doc) currentDID["error"] = 1 currentDID["message"] = "Signatures in log don't match" return currentDID end end currentDID["did"] = doc_did currentDID["doc"] = doc # since hash is guaranteed during retrieve_document this check is not necessary # if hash(canonical(doc)) != did_hash # currentDID["error"] = 1 # currentDID["message"] = "DID identifier and DID document don't match" # if did_hash == initial_did # verification_output = true # end # if verification_output # currentDID["verification"] += "identifier: " + did_hash.to_s + "\n" # currentDID["verification"] += "⛔ does not match DID Document:" + "\n" # currentDID["verification"] += JSON.pretty_generate(doc) + "\n" # currentDID["verification"] += "(Details: https://ownyourdata.github.io/oydid/#calculate_hash)" + "\n\n" # end # return currentDID # end if did_hash == initial_did verification_output = true end if verification_output currentDID["verification"] += "identifier: " + did_hash.to_s + "\n" currentDID["verification"] += "✅ is hash of DID Document:" + "\n" currentDID["verification"] += JSON.pretty_generate(doc) + "\n" currentDID["verification"] += "(Details: https://ownyourdata.github.io/oydid/#calculate_hash)" + "\n\n" end current_public_doc_key = currentDID["doc"]["key"].split(":").first rescue "" when 0 # TERMINATE currentDID["termination_log_id"] = i doc_did = currentDID["did"] doc_location = get_location(doc_did) did_hash = doc_did.delete_prefix("did:oyd:") did_hash = did_hash.split("@").first did10 = did_hash[0,10] doc = retrieve_document_raw(doc_did, did10 + ".doc", doc_location, {}) # since it retrieves a DID that previously existed, this test is not necessary # if doc.first.nil? # currentDID["error"] = 2 # currentDID["message"] = doc.last.to_s # return currentDID # end doc = doc.first["doc"] term = doc["log"] log_location = term.split("@")[1] rescue "" if log_location.to_s == "" log_location = DEFAULT_LOCATION end term = term.split("@").first if hash(canonical(el)) != term currentDID["error"] = 1 currentDID["message"] = "Log reference and record don't match" if verification_output currentDID["verification"] += "'log' reference in DID Document: " + term.to_s + "\n" currentDID["verification"] += "⛔ does not match TERMINATE log record:" + "\n" currentDID["verification"] += JSON.pretty_generate(el) + "\n" currentDID["verification"] += "(Details: https://ownyourdata.github.io/oydid/#calculate_hash)" + "\n\n" end return currentDID end if verification_output currentDID["verification"] += "'log' reference in DID Document: " + term.to_s + "\n" currentDID["verification"] += "✅ is hash of TERMINATE log record:" + "\n" currentDID["verification"] += JSON.pretty_generate(el) + "\n" currentDID["verification"] += "(Details: https://ownyourdata.github.io/oydid/#calculate_hash)" + "\n\n" end # check if there is a revocation entry revocation_record = {} revoc_term = el["doc"] revoc_term = revoc_term.split("@").first revoc_term_found = false log_array, msg = retrieve_log(did_hash, did10 + ".log", log_location, options) log_array.each do |log_el| log_el_structure = log_el.dup if log_el["op"].to_i == 1 # TERMINATE log_el_structure.delete("previous") end if hash(canonical(log_el_structure)) == revoc_term revoc_term_found = true revocation_record = log_el.dup if verification_output currentDID["verification"] += "'doc' reference in TERMINATE log record: " + revoc_term.to_s + "\n" currentDID["verification"] += "✅ is hash of REVOCATION log record (without 'previous' attribute):" + "\n" currentDID["verification"] += JSON.pretty_generate(log_el) + "\n" currentDID["verification"] += "(Details: https://ownyourdata.github.io/oydid/#calculate_hash)" + "\n\n" end break end end unless log_array.nil? # this should actually be covered by retrieve_log in the block above # (actually I wasn't able to craft a test case covering this part...) # if !options.transform_keys(&:to_s)["log_location"].nil? # log_array, msg = retrieve_log(revoc_term, did10 + ".log", options.transform_keys(&:to_s)["log_location"], options) # log_array.each do |log_el| # if log_el["op"] == 1 # TERMINATE # log_el_structure = log_el.delete("previous") # else # log_el_structure = log_el # end # if hash(canonical(log_el_structure)) == revoc_term # revoc_term_found = true # revocation_record = log_el.dup # if verification_output # currentDID["verification"] += "'doc' reference in TERMINATE log record: " + revoc_term.to_s + "\n" # currentDID["verification"] += "✅ is hash of REVOCATION log record (without 'previous' attribute):" + "\n" # currentDID["verification"] += JSON.pretty_generate(log_el) + "\n" # currentDID["verification"] += "(Details: https://ownyourdata.github.io/oydid/#calculate_hash)" + "\n\n" # end # break # end # end # end if revoc_term_found update_term_found = false log_array.each do |log_el| if log_el["op"].to_i == 3 if log_el["previous"].include?(hash(canonical(revocation_record))) update_term_found = true message = log_el["doc"].to_s signature = log_el["sig"] public_key = current_public_doc_key.to_s signature_verification = verify(message, signature, public_key).first if signature_verification if verification_output currentDID["verification"] += "found UPDATE log record:" + "\n" currentDID["verification"] += JSON.pretty_generate(log_el) + "\n" currentDID["verification"] += "✅ public key from last DID Document: " + current_public_doc_key.to_s + "\n" currentDID["verification"] += "verifies 'doc' reference of new DID Document: " + log_el["doc"].to_s + "\n" currentDID["verification"] += log_el["sig"].to_s + "\n" currentDID["verification"] += "of next DID Document (Details: https://ownyourdata.github.io/oydid/#verify_signature)" + "\n" next_doc_did = log_el["doc"].to_s next_doc_location = get_location(next_doc_did) next_did_hash = next_doc_did.delete_prefix("did:oyd:") next_did_hash = next_did_hash.split("@").first next_did10 = next_did_hash[0,10] next_doc = retrieve_document_raw(next_doc_did, next_did10 + ".doc", next_doc_location, {}) if next_doc.first.nil? currentDID["error"] = 2 currentDID["message"] = next_doc.last return currentDID end next_doc = next_doc.first["doc"] if public_key == next_doc["key"].split(":").first currentDID["verification"] += "⚠️ no key rotation in updated DID Document" + "\n" end currentDID["verification"] += "\n" end else currentDID["error"] = 1 currentDID["message"] = "Signature does not match" if verification_output new_doc_did = log_el["doc"].to_s new_doc_location = get_location(new_doc_did) new_did_hash = new_doc_did.delete_prefix("did:oyd:") new_did_hash = new_did_hash.split("@").first new_did10 = new_did_hash[0,10] new_doc = retrieve_document(new_doc_did, new_did10 + ".doc", new_doc_location, {}).first currentDID["verification"] += "found UPDATE log record:" + "\n" currentDID["verification"] += JSON.pretty_generate(log_el) + "\n" currentDID["verification"] += "⛔ public key from last DID Document: " + current_public_doc_key.to_s + "\n" currentDID["verification"] += "does not verify 'doc' reference of new DID Document: " + log_el["doc"].to_s + "\n" currentDID["verification"] += log_el["sig"].to_s + "\n" currentDID["verification"] += "next DID Document (Details: https://ownyourdata.github.io/oydid/#verify_signature)" + "\n" currentDID["verification"] += JSON.pretty_generate(new_doc) + "\n\n" end return currentDID end break end end end else if verification_output currentDID["verification"] += "Revocation reference in log record: " + revoc_term.to_s + "\n" currentDID["verification"] += "✅ cannot find revocation record searching at" + "\n" currentDID["verification"] += "- " + log_location + "\n" if !options.transform_keys(&:to_s)["log_location"].nil? currentDID["verification"] += "- " + options.transform_keys(&:to_s)["log_location"].to_s + "\n" end currentDID["verification"] += "(Details: https://ownyourdata.github.io/oydid/#retrieve_log)" + "\n\n" end break end when 1 # revocation log entry # do nothing else currentDID["error"] = 2 currentDID["message"] = "FATAL ERROR: op code '" + el["op"].to_s + "' not implemented" return currentDID end i += 1 end unless currentDID["log"].nil? return currentDID end
decode(message)
click to toggle source
# File lib/oydid/basic.rb, line 10 def self.decode(message) Multibases.unpack(message).decode.to_s('ASCII-8BIT') end
decode_private_key(key_encoded)
click to toggle source
# File lib/oydid/basic.rb, line 104 def self.decode_private_key(key_encoded) begin code, length, digest = decode(key_encoded).unpack('SCa*') case Multicodecs[code].name when 'ed25519-priv' private_key = Ed25519::SigningKey.new(digest).to_bytes else return [nil, "unsupported key codec"] end length = private_key.bytesize return [Oydid.encode([code, length, private_key].pack("SCa#{length}")), ""] rescue return [nil, "invalid key"] end end
encode(message, method = "base58btc")
click to toggle source
basic functions —————————
# File lib/oydid/basic.rb, line 6 def self.encode(message, method = "base58btc") Multibases.pack(method, message).to_s end
fromW3C(didDocument, options)
click to toggle source
# File lib/oydid.rb, line 721 def self.fromW3C(didDocument, options) didDocument = didDocument.transform_keys(&:to_s) if didDocument["@context"].to_s == "https://www.w3.org/ns/did/v1" didDocument.delete("@context") end didDocument end
generate_base(content, did, mode, options)
click to toggle source
# File lib/oydid.rb, line 139 def self.generate_base(content, did, mode, options) # input validation did_doc = JSON.parse(content.to_json) rescue nil if did_doc.nil? return [nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, "invalid payload"] end did_old = nil log_old = nil prev_hash = [] revoc_log = nil doc_location = options[:doc_location] if options[:ts].nil? ts = Time.now.to_i else ts = options[:ts] end if mode == "create" || mode == "clone" operation_mode = 2 # CREATE if options[:doc_key].nil? if options[:doc_enc].nil? privateKey, msg = generate_private_key(options[:doc_pwd].to_s, 'ed25519-priv') else privateKey, msg = decode_private_key(options[:doc_enc].to_s) end else privateKey, msg = read_private_key(options[:doc_key].to_s) if privateKey.nil? return [nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, "private document key not found"] end end if options[:rev_key].nil? if options[:rev_enc].nil? revocationKey, msg = generate_private_key(options[:rev_pwd].to_s, 'ed25519-priv') else revocationKey, msg = decode_private_key(options[:rev_enc].to_s) end else revocationKey, msg = read_private_key(options[:rev_key].to_s) if revocationKey.nil? return [nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, "private revocation key not found"] end end else # mode == "update" => read information did_info, msg = read(did, options) if did_info.nil? return [nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, "cannot resolve DID (on updating DID)"] end if did_info["error"] != 0 return [nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, did_info["message"].to_s] end did = did_info["did"] did_hash = did.delete_prefix("did:oyd:") did10 = did_hash[0,10] if doc_location.to_s == "" if did_hash.include?(LOCATION_PREFIX) hash_split = did_hash.split(LOCATION_PREFIX) did_hash = hash_split[0] doc_location = hash_split[1] end end operation_mode = 3 # UPDATE # collect relevant information from previous did did_old = did.dup did10_old = did10.dup log_old = did_info["log"] if options[:old_doc_key].nil? if options[:old_doc_enc].nil? if options[:old_doc_pwd].nil? privateKey_old = read_private_storage(did10_old + "_private_key.b58") else privateKey_old, msg = generate_private_key(options[:old_doc_pwd].to_s, 'ed25519-priv') end else privateKey_old, msg = decode_private_key(options[:old_doc_enc].to_s) end else privateKey_old, msg = read_private_key(options[:old_doc_key].to_s) end if privateKey_old.nil? return [nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, "invalid or missing old private document key"] end if options[:old_rev_key].nil? if options[:old_rev_enc].nil? if options[:old_rev_pwd].nil? revocationKey_old = read_private_storage(did10_old + "_revocation_key.b58") else revocationKey_old, msg = generate_private_key(options[:old_rev_pwd].to_s, 'ed25519-priv') end else revocationKey_old, msg = decode_private_key(options[:old_rev_enc].to_s) end else revocationKey_old, msg = read_private_key(options[:old_rev_key].to_s) end if revocationKey_old.nil? return [nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, "invalid or missing old private revocation key"] end # key management if options[:doc_key].nil? if options[:doc_enc].nil? privateKey, msg = generate_private_key(options[:doc_pwd].to_s, 'ed25519-priv') else privateKey, msg = decode_private_key(options[:doc_enc].to_s) end else privateKey, msg = read_private_key(options[:doc_key].to_s) end # if options[:rev_key].nil? && options[:rev_pwd].nil? && options[:rev_enc].nil? # revocationLog = read_private_storage(did10 + "_revocation.json") # if revocationLog.nil? # return [nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, "invalid or missing old revocation log"] # end # else if options[:rev_key].nil? if options[:rev_enc].nil? if options[:rev_pwd].nil? revocationKey, msg = generate_private_key("", 'ed25519-priv') else revocationKey, msg = generate_private_key(options[:rev_pwd].to_s, 'ed25519-priv') end else revocationKey, msg = decode_private_key(options[:rev_enc].to_s) end else revocationKey, msg = read_private_key(options[:rev_key].to_s) end # re-build revocation document did_old_doc = did_info["doc"]["doc"] ts_old = did_info["log"].last["ts"] publicKey_old = public_key(privateKey_old).first pubRevoKey_old = public_key(revocationKey_old).first did_key_old = publicKey_old + ":" + pubRevoKey_old subDid = {"doc": did_old_doc, "key": did_key_old}.to_json subDidHash = hash(subDid) signedSubDidHash = sign(subDidHash, revocationKey_old).first revocationLog = { "ts": ts_old, "op": 1, # REVOKE "doc": subDidHash, "sig": signedSubDidHash }.transform_keys(&:to_s).to_json # end revoc_log = JSON.parse(revocationLog) revoc_log["previous"] = [ hash(canonical(log_old[did_info["doc_log_id"].to_i])), hash(canonical(log_old[did_info["termination_log_id"].to_i])) ] prev_hash = [hash(canonical(revoc_log))] end publicKey = public_key(privateKey).first pubRevoKey = public_key(revocationKey).first did_key = publicKey + ":" + pubRevoKey # build new revocation document subDid = {"doc": did_doc, "key": did_key}.to_json subDidHash = hash(canonical(subDid)) signedSubDidHash = sign(subDidHash, revocationKey).first r1 = { "ts": ts, "op": 1, # REVOKE "doc": subDidHash, "sig": signedSubDidHash }.transform_keys(&:to_s) # build termination log entry l2_doc = hash(canonical(r1)) if !doc_location.nil? l2_doc += LOCATION_PREFIX + doc_location.to_s end l2 = { "ts": ts, "op": 0, # TERMINATE "doc": l2_doc, "sig": sign(l2_doc, privateKey).first, "previous": [] }.transform_keys(&:to_s) # build actual DID document log_str = hash(canonical(l2)) if !doc_location.nil? log_str += LOCATION_PREFIX + doc_location.to_s end didDocument = { "doc": did_doc, "key": did_key, "log": log_str }.transform_keys(&:to_s) # create DID l1_doc = hash(canonical(didDocument)) if !doc_location.nil? l1_doc += LOCATION_PREFIX + doc_location.to_s end did = "did:oyd:" + l1_doc did10 = l1_doc[0,10] if mode == "clone" # create log entry for source DID new_log = { "ts": ts, "op": 4, # CLONE "doc": l1_doc, "sig": sign(l1_doc, privateKey).first, "previous": [options[:previous_clone].to_s] } retVal = HTTParty.post(options[:source_location] + "/log/" + options[:source_did], headers: { 'Content-Type' => 'application/json' }, body: {"log": new_log}.to_json ) prev_hash = [hash(canonical(new_log))] end # build creation log entry if operation_mode == 3 # UPDATE l1 = { "ts": ts, "op": operation_mode, # UPDATE "doc": l1_doc, "sig": sign(l1_doc, privateKey_old).first, "previous": prev_hash }.transform_keys(&:to_s) else l1 = { "ts": ts, "op": operation_mode, # CREATE "doc": l1_doc, "sig": sign(l1_doc, privateKey).first, "previous": prev_hash }.transform_keys(&:to_s) end return [did, didDocument, revoc_log, l1, l2, r1, privateKey, revocationKey, did_old, log_old, ""] end
generate_private_key(input, method = "ed25519-priv")
click to toggle source
key management —————————-
# File lib/oydid/basic.rb, line 28 def self.generate_private_key(input, method = "ed25519-priv") begin omc = Multicodecs[method].code rescue return [nil, "unknown key codec"] end case Multicodecs[method].name when 'ed25519-priv' if input != "" raw_key = Ed25519::SigningKey.new(RbNaCl::Hash.sha256(input)).to_bytes else raw_key = Ed25519::SigningKey.generate.to_bytes end else return [nil, "unsupported key codec"] end length = raw_key.bytesize return [encode([omc, length, raw_key].pack("SCa#{length}")), ""] end
get_location(id)
click to toggle source
# File lib/oydid/basic.rb, line 133 def self.get_location(id) if id.include?(LOCATION_PREFIX) id_split = id.split(LOCATION_PREFIX) return id_split[1] else if id.include?(CGI.escape(LOCATION_PREFIX)) id_split = id.split(CGI.escape(LOCATION_PREFIX)) return id_split[1] else return DEFAULT_LOCATION end end end
hash(message)
click to toggle source
# File lib/oydid/basic.rb, line 14 def self.hash(message) encode(Multihashes.encode(RbNaCl::Hash.sha256(message), "sha2-256").unpack('C*')) end
match_log_did?(log, doc)
click to toggle source
check if signature matches current document check if signature in log is correct
# File lib/oydid/log.rb, line 19 def self.match_log_did?(log, doc) message = log["doc"].to_s signature = log["sig"].to_s public_keys = doc["key"].to_s public_key = public_keys.split(":")[0] rescue "" return verify(message, signature, public_key).first end
public_key(private_key)
click to toggle source
# File lib/oydid/basic.rb, line 49 def self.public_key(private_key) code, length, digest = decode(private_key).unpack('SCa*') case Multicodecs[code].name when 'ed25519-priv' public_key = Ed25519::SigningKey.new(digest).verify_key length = public_key.to_bytes.bytesize return [encode([Multicodecs['ed25519-pub'].code, length, public_key].pack("CCa#{length}")), ""] else return [nil, "unsupported key codec"] end end
publish(did, didDocument, logs, options)
click to toggle source
# File lib/oydid.rb, line 367 def self.publish(did, didDocument, logs, options) did_hash = did.delete_prefix("did:oyd:") did10 = did_hash[0,10] doc_location = options[:doc_location] if doc_location.to_s == "" if did_hash.include?(LOCATION_PREFIX) hash_split = did_hash.split(LOCATION_PREFIX) did_hash = hash_split[0] doc_location = hash_split[1] else doc_location = DEFAULT_LOCATION end end # wirte data based on location case doc_location.to_s when /^http/ # build object to post did_data = { "did": did, "did-document": didDocument, "logs": logs } oydid_url = doc_location.to_s + "/doc" retVal = HTTParty.post(oydid_url, headers: { 'Content-Type' => 'application/json' }, body: did_data.to_json ) if retVal.code != 200 err_msg = retVal.parsed_response("error").to_s rescue "invalid response from " + doc_location.to_s + "/doc" return [false, err_msg] end else # write files to disk write_private_storage(logs.to_json, did10 + ".log") write_private_storage(didDocument.to_json, did10 + ".doc") write_private_storage(did, did10 + ".did") end return [true, ""] end
read(did, options)
click to toggle source
expected DID format: did:oyd:123
# File lib/oydid.rb, line 21 def self.read(did, options) # setup currentDID = { "did": did, "doc": "", "log": [], "doc_log_id": nil, "termination_log_id": nil, "error": 0, "message": "", "verification": "" }.transform_keys(&:to_s) did_hash = did.delete_prefix("did:oyd:") did10 = did_hash[0,10] # get did location did_location = "" if !options[:doc_location].nil? did_location = options[:doc_location] end if did_location.to_s == "" if !options[:location].nil? did_location = options[:location] end end if did_location.to_s == "" if did.include?(LOCATION_PREFIX) tmp = did.split(LOCATION_PREFIX) did = tmp[0] did_location = tmp[1] end end if did_location == "" did_location = DEFAULT_LOCATION end # retrieve DID document did_document = retrieve_document(did, did10 + ".doc", did_location, options) if did_document.first.nil? return [nil, did_document.last] end did_document = did_document.first currentDID["doc"] = did_document if options[:trace] puts " .. DID document retrieved" end # get log location log_hash = did_document["log"] log_location = "" if !options[:log_location].nil? log_location = options[:log_location] end if log_location.to_s == "" if !options[:location].nil? log_location = options[:location] end end if log_location.to_s == "" if log_hash.include?(LOCATION_PREFIX) hash_split = log_hash.split(LOCATION_PREFIX) log_hash = hash_split[0] log_location = hash_split[1] end end if log_location == "" log_location = DEFAULT_LOCATION end # retrieve and traverse log to get current DID state log_array, msg = retrieve_log(log_hash, did10 + ".log", log_location, options) if log_array.nil? return [nil, msg] else if options[:trace] puts " .. Log retrieved" end dag, create_index, terminate_index, msg = dag_did(log_array, options) if dag.nil? return [nil, msg] end if options[:trace] puts " .. DAG with " + dag.vertices.length.to_s + " vertices and " + dag.edges.length.to_s + " edges, CREATE index: " + create_index.to_s end ordered_log_array = dag2array(dag, log_array, create_index, [], options) ordered_log_array << log_array[terminate_index] currentDID["log"] = ordered_log_array if options[:trace] if options[:silent].nil? || !options[:silent] 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 end end currentDID["log"] = ordered_log_array if options[:trace] if options[:silent].nil? || !options[:silent] dag.edges.each do |e| puts " edge " + e.origin[:id].to_s + " <- " + e.destination[:id].to_s end end end currentDID = dag_update(currentDID, options) if options[:log_complete] currentDID["log"] = log_array end return [currentDID, ""] end end
read_private_key(filename)
click to toggle source
# File lib/oydid/basic.rb, line 93 def self.read_private_key(filename) begin f = File.open(filename) key_encoded = f.read f.close rescue return [nil, "cannot read file"] end decode_private_key(key_encoded) end
read_private_storage(filename)
click to toggle source
# File lib/oydid/basic.rb, line 125 def self.read_private_storage(filename) begin File.open(filename, 'r') { |f| f.read } rescue nil end end
retrieve_document(doc_hash, doc_file, doc_location, options)
click to toggle source
# File lib/oydid/basic.rb, line 147 def self.retrieve_document(doc_hash, doc_file, doc_location, options) if doc_location == "" doc_location = DEFAULT_LOCATION end if !(doc_location == "" || doc_location == "local") if !doc_location.start_with?("http") doc_location = "https://" + doc_location end end case doc_location when /^http/ retVal = HTTParty.get(doc_location + "/doc/" + doc_hash) if retVal.code != 200 msg = retVal.parsed_response("error").to_s rescue "invalid response from " + doc_location.to_s + "/doc/" + doc_hash.to_s return [nil, msg] end if options.transform_keys(&:to_s)["trace"] if options[:silent].nil? || !options[:silent] puts "GET " + doc_hash + " from " + doc_location end end return [retVal.parsed_response, ""] when "", "local" doc = JSON.parse(read_private_storage(doc_file)) rescue {} if doc == {} return [nil, "cannot read file"] else return [doc, ""] end end end
retrieve_document_raw(doc_hash, doc_file, doc_location, options)
click to toggle source
# File lib/oydid/basic.rb, line 180 def self.retrieve_document_raw(doc_hash, doc_file, doc_location, options) if doc_location == "" doc_location = DEFAULT_LOCATION end if !(doc_location == "" || doc_location == "local") if !doc_location.start_with?("http") doc_location = "https://" + doc_location end end case doc_location when /^http/ retVal = HTTParty.get(doc_location + "/doc_raw/" + doc_hash) if retVal.code != 200 msg = retVal.parsed_response("error").to_s rescue "invalid response from " + doc_location.to_s + "/doc/" + doc_hash.to_s return [nil, msg] end if options.transform_keys(&:to_s)["trace"] if options[:silent].nil? || !options[:silent] puts "GET " + doc_hash + " from " + doc_location end end return [retVal.parsed_response, ""] when "", "local" doc = JSON.parse(read_private_storage(doc_file)) rescue {} log = JSON.parse(read_private_storage(doc_file.sub(".doc", ".log"))) rescue {} if doc == {} return [nil, "cannot read file"] else obj = {"doc" => doc, "log" => log} return [obj, ""] end end end
retrieve_log(did_hash, log_file, log_location, options)
click to toggle source
# File lib/oydid/log.rb, line 27 def self.retrieve_log(did_hash, log_file, log_location, options) if log_location == "" log_location = DEFAULT_LOCATION end if !(log_location == "" || log_location == "local") if !log_location.start_with?("http") log_location = "https://" + log_location end end case log_location when /^http/ retVal = HTTParty.get(log_location + "/log/" + did_hash) if retVal.code != 200 msg = retVal.parsed_response("error").to_s rescue "invalid response from " + log_location.to_s + "/log/" + did_hash.to_s return [nil, msg] end if options.transform_keys(&:to_s)["trace"] if options[:silent].nil? || !options[:silent] puts "GET log for " + did_hash + " from " + log_location end end retVal = JSON.parse(retVal.to_s) rescue nil return [retVal, ""] when "", "local" doc = JSON.parse(read_private_storage(log_file)) rescue {} if doc == {} return [nil, "cannot read file '" + log_file + "'"] end return [doc, ""] end end
revoke(did, options)
click to toggle source
# File lib/oydid.rb, line 608 def self.revoke(did, options) revoc_log, msg = revoke_base(did, options) if revoc_log.nil? return [nil, msg] end success, msg = revoke_publish(did, revoc_log, options) end
revoke_base(did, options)
click to toggle source
# File lib/oydid.rb, line 469 def self.revoke_base(did, options) did_orig = did.dup doc_location = options[:doc_location] if options[:ts].nil? ts = Time.now.to_i else ts = options[:ts] end did_info, msg = read(did, options) if did_info.nil? return [nil, "cannot resolve DID (on revoking DID)"] end if did_info["error"] != 0 return [nil, did_info["message"].to_s] end did = did_info["did"] did_hash = did.delete_prefix("did:oyd:") did10 = did_hash[0,10] if doc_location.to_s == "" if did_hash.include?(LOCATION_PREFIX) hash_split = did_hash.split(LOCATION_PREFIX) did_hash = hash_split[0] doc_location = hash_split[1] end end # collect relevant information from previous did did_old = did.dup did10_old = did10.dup log_old = did_info["log"] if options[:old_doc_key].nil? if options[:old_doc_enc].nil? if options[:old_doc_pwd].nil? privateKey_old = read_private_storage(did10_old + "_private_key.b58") else privateKey_old, msg = generate_private_key(options[:old_doc_pwd].to_s, 'ed25519-priv') end else privateKey_old, msg = decode_private_key(options[:old_doc_enc].to_s) end else privateKey_old, msg = read_private_key(options[:old_doc_key].to_s) end if privateKey_old.nil? return [nil, "invalid or missing old private document key"] end if options[:old_rev_key].nil? if options[:old_rev_enc].nil? if options[:old_rev_pwd].nil? revocationKey_old = read_private_storage(did10_old + "_revocation_key.b58") else revocationKey_old, msg = generate_private_key(options[:old_rev_pwd].to_s, 'ed25519-priv') end else revocationKey_old, msg = decode_private_key(options[:old_rev_enc].to_s) end else revocationKey_old, msg = read_private_key(options[:old_rev_key].to_s) end if revocationKey_old.nil? return [nil, "invalid or missing old private revocation key"] end if options[:rev_key].nil? && options[:rev_pwd].nil? && options[:rev_enc].nil? revocationKey, msg = read_private_key(did10 + "_revocation_key.b58") revocationLog = read_private_storage(did10 + "_revocation.json") else if options[:rev_pwd].nil? if options[:rev_enc].nil? revocationKey, msg = read_private_key(options[:rev_key].to_s) else revocationKey, msg = decode_private_key(options[:rev_enc].to_s) end else revocationKey, msg = generate_private_key(options[:rev_pwd].to_s, 'ed25519-priv') end # re-build revocation document did_old_doc = did_info["doc"]["doc"] ts_old = did_info["log"].last["ts"] publicKey_old = public_key(privateKey_old).first pubRevoKey_old = public_key(revocationKey_old).first did_key_old = publicKey_old + ":" + pubRevoKey_old subDid = {"doc": did_old_doc, "key": did_key_old}.to_json subDidHash = hash(subDid) signedSubDidHash = sign(subDidHash, revocationKey_old).first revocationLog = { "ts": ts_old, "op": 1, # REVOKE "doc": subDidHash, "sig": signedSubDidHash }.transform_keys(&:to_s).to_json end if revocationLog.nil? return [nil, "private revocation key not found"] end revoc_log = JSON.parse(revocationLog) revoc_log["previous"] = [ hash(canonical(log_old[did_info["doc_log_id"].to_i])), hash(canonical(log_old[did_info["termination_log_id"].to_i])) ] return [revoc_log, ""] end
revoke_publish(did, revoc_log, options)
click to toggle source
# File lib/oydid.rb, line 575 def self.revoke_publish(did, revoc_log, options) did_hash = did.delete_prefix("did:oyd:") did10 = did_hash[0,10] doc_location = options[:doc_location] if did_hash.include?(LOCATION_PREFIX) hash_split = did_hash.split(LOCATION_PREFIX) did_hash = hash_split[0] doc_location = hash_split[1] end if doc_location.to_s == "" doc_location = DEFAULT_LOCATION end # publish revocation log based on location case doc_location.to_s when /^http/ retVal = HTTParty.post(doc_location.to_s + "/log/" + did_hash.to_s, headers: { 'Content-Type' => 'application/json' }, body: {"log": revoc_log}.to_json ) if retVal.code != 200 msg = retVal.parsed_response("error").to_s rescue "invalid response from " + doc_location.to_s + "/log/" + did_hash.to_s return [nil, msg] end else File.write(did10 + ".log", revoc_log.to_json) if !did_old.nil? File.write(did10_old + ".log", revoc_log.to_json) end end return [did, ""] end
sign(message, private_key)
click to toggle source
# File lib/oydid/basic.rb, line 61 def self.sign(message, private_key) code, length, digest = decode(private_key).unpack('SCa*') case Multicodecs[code].name when 'ed25519-priv' return [encode(Ed25519::SigningKey.new(digest).sign(message)), ""] else return [nil, "unsupported key codec"] end end
update(content, did, options)
click to toggle source
# File lib/oydid.rb, line 135 def self.update(content, did, options) return write(content, did, "update", options) end
verify(message, signature, public_key)
click to toggle source
# File lib/oydid/basic.rb, line 71 def self.verify(message, signature, public_key) begin code, length, digest = decode(public_key).unpack('CCa*') case Multicodecs[code].name when 'ed25519-pub' verify_key = Ed25519::VerifyKey.new(digest) signature_verification = false begin verify_key.verify(decode(signature), message) signature_verification = true rescue Ed25519::VerifyError signature_verification = false end return [signature_verification, ""] else return [nil, "unsupported key codec"] end rescue return [nil, "unknown key codec"] end end
w3c(did_info, options)
click to toggle source
# File lib/oydid.rb, line 659 def self.w3c(did_info, options) did = did_info["did"] if !did.start_with?("did:oyd:") did = "did:oyd:" + did end didDoc = did_info.transform_keys(&:to_s)["doc"] pubDocKey = didDoc["key"].split(":")[0] rescue "" pubRevKey = didDoc["key"].split(":")[1] rescue "" wd = {} wd["@context"] = "https://www.w3.org/ns/did/v1" wd["id"] = did wd["verificationMethod"] = [{ "id": did, "type": "Ed25519VerificationKey2020", "controller": did, "publicKeyBase58": pubDocKey }] wd["keyAgreement"] = [{ "id": did, "type": "Ed25519VerificationKey2020", "controller": did, "publicKeyBase58": pubRevKey }] if didDoc["@context"].to_s == "https://www.w3.org/ns/did/v1" didDoc.delete("@context") end if didDoc["doc"].to_s != "" didDoc = didDoc["doc"] end newDidDoc = [] if didDoc.is_a?(Hash) if didDoc["authentication"].to_s != "" wd["authentication"] = didDoc["authentication"] didDoc.delete("authentication") end if didDoc["service"].to_s != "" if didDoc["service"].is_a?(Array) newDidDoc = didDoc.dup newDidDoc.delete("service") if newDidDoc == {} newDidDoc = [] else if !newDidDoc.is_a?(Array) newDidDoc=[newDidDoc] end end newDidDoc << didDoc["service"] newDidDoc = newDidDoc.flatten end else newDidDoc = didDoc end else newDidDoc = didDoc end wd["service"] = newDidDoc return wd end
write(content, did, mode, options)
click to toggle source
# File lib/oydid.rb, line 409 def self.write(content, did, mode, options) did, didDocument, revoc_log, l1, l2, r1, privateKey, revocationKey, did_old, log_old, msg = generate_base(content, did, mode, options) if msg != "" return [nil, msg] end did_hash = did.delete_prefix("did:oyd:") did10 = did_hash[0,10] did_old_hash = did_old.delete_prefix("did:oyd:") rescue nil did10_old = did_old_hash[0,10] rescue nil doc_location = options[:doc_location] if doc_location.to_s == "" if did_hash.include?(LOCATION_PREFIX) hash_split = did_hash.split(LOCATION_PREFIX) did_hash = hash_split[0] doc_location = hash_split[1] else doc_location = DEFAULT_LOCATION end end case doc_location.to_s when /^http/ logs = [revoc_log, l1, l2].flatten.compact else logs = [log_old, revoc_log, l1, l2].flatten.compact if !did_old.nil? write_private_storage([log_old, revoc_log, l1, l2].flatten.compact.to_json, did10_old + ".log") end end success, msg = publish(did, didDocument, logs, options) if success w3c_input = { "did" => did, "doc" => didDocument } retVal = { "did" => did, "doc" => didDocument, "doc_w3c" => w3c(w3c_input, options), "log" => logs } if options[:return_secrets] retVal["private_key"] = privateKey retVal["revocation_key"] = revocationKey retVal["revocation_log"] = r1 else write_private_storage(privateKey, did10 + "_private_key.b58") write_private_storage(revocationKey, did10 + "_revocation_key.b58") write_private_storage(r1.to_json, did10 + "_revocation.json") end return [retVal, ""] else return [nil, msg] end end
write_private_storage(payload, filename)
click to toggle source
storage functions —————————–
# File lib/oydid/basic.rb, line 121 def self.write_private_storage(payload, filename) File.open(filename, 'w') {|f| f.write(payload)} end