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