commit 174aaa38d2f2bfe246c519d1aada62d2d652c0f6
parent 5f51520c3f00c52cdba047cfd403839216d19003
Author: Alberto Lerda <30939098+albertolerda@users.noreply.github.com>
Date: Mon, 9 Jan 2023 14:00:46 +0100
Merge pull request #41 from interfacerproject/alberto/did
feat: ifacer did creation and resolution
Diffstat:
18 files changed, 246 insertions(+), 44 deletions(-)
diff --git a/conf/.env.templ b/conf/.env.templ
@@ -27,8 +27,7 @@
#export DB_PORT=5432
## restroom
-#export ROOM_HOST=
-#export ROOM_PORT=
+#export ROOM_URI=
export ROOM_SALT=@ROOM_SALT
## admin
@@ -38,3 +37,6 @@ export ADMIN_KEY=@ADMIN_KEY
export GQL_AUTH_CALLS=true
export GQL_DEF_PAGE_SIZE=50
export GQL_MAX_PAGE_SIZE=100
+
+#export DID_KEYRING
+#export DID_URI
diff --git a/conf/runtime.exs b/conf/runtime.exs
@@ -27,6 +27,15 @@ get_env_int = fn varname, int ->
end
end
+get_env_url = fn varname, default ->
+ with {:ok, %{scheme: scheme, host: host, port: port}} when not is_nil(host) and scheme in ["http", "https"]
+ <- URI.new(get_env(varname, default)) do
+ %{scheme: :"#{scheme}", host: host, port: port}
+ else
+ err -> raise err
+ end
+end
+
#
# database
#
@@ -68,12 +77,21 @@ config :zenflows, Zenflows.DB.Repo, db_conf
#
# restroom
#
+room_uri = get_env_url.("ROOM_URI", "http://localhost")
config :zenflows, Zenflows.Restroom,
- room_host: get_env("ROOM_HOST", "localhost"),
- room_port: get_env_int.("ROOM_PORT", 3000),
+ room_uri: room_uri,
room_salt: fetch_env!("ROOM_SALT")
#
+# did
+#
+did_keyring = Base.decode64!(get_env("DID_KEYRING", ""))
+did_uri = get_env_url.("DID_URI", "https://did.dyne.org")
+config :zenflows, Zenflows.DID,
+ did_uri: did_uri,
+ did_keyring: if(did_keyring == "", do: nil, else: Jason.decode!(did_keyring))
+
+#
# admin
#
admin_key = fetch_env!("ADMIN_KEY") |> Base.decode16!(case: :lower)
diff --git a/docs/configuration-guide.md b/docs/configuration-guide.md
@@ -36,12 +36,14 @@ also see the [Required Options](#required-options).
This option should be used if extended configuration is desired (using the
options mention in the link above).
-* `ROOM_HOST`: The hostname or IP address of the Restroom instance. Defaults to `localhost`.
-* `ROOM_PORT`: The port number of the Restroom instance. It must be an integer
- between `0` and `65535`, inclusive. Defaults to `3000`.
+* `ROOM_URI`: The URI of the Restroom instance. Defaults to `http://localhost`.
* `ROOM_SALT`: The base64-encoded salt to be used with Restroom's
keypairoomServer call.
+* `DID_URI`: The URI of the DID controller instance. Defaults to `https://did.dyne.org`.
+* `DID_KEYRING`: Keyring (identity) of the server, it is not defined
+ communication with DID controller is disabled.
+
* `ADMIN_KEY`: A 64-octect long, lowercase-base16-encoded string used for the
authenticating calls from the administrators. Can be generated with
`openssl rand -hex 64`. It is automatically generated when you run
diff --git a/priv/repo/migrations/20211111175352_fill_vf_agent.exs b/priv/repo/migrations/20211111175352_fill_vf_agent.exs
@@ -34,7 +34,7 @@ OR
AND eddsa_public_key IS NULL
AND ethereum_address IS NULL
AND reflow_public_key IS NULL
- AND schnorr_public_key IS NULL
+ AND bitcoin_public_key IS NULL
)
"""
@@ -55,7 +55,7 @@ def change() do
add :eddsa_public_key, :text
add :ethereum_address, :text
add :reflow_public_key, :text
- add :schnorr_public_key, :text
+ add :bitcoin_public_key, :text
# organization
add :classified_as, {:array, :text}
diff --git a/src/zenflows/application.ex b/src/zenflows/application.ex
@@ -29,6 +29,7 @@ def start(_type, _args) do
Zenflows.DB.Repo,
Zenflows.InstVars.Domain,
Zenflows.Restroom,
+ Zenflows.DID,
{Plug.Cowboy, scheme: :http, plug: Zenflows.Web.Router, options: [port: 8000]},
]
diff --git a/src/zenflows/did.ex b/src/zenflows/did.ex
@@ -0,0 +1,143 @@
+# Zenflows is designed to implement the Valueflows vocabulary,
+# written and maintained by srfsh <info@dyne.org>.
+# Copyright (C) 2021-2023 Dyne.org foundation <foundation@dyne.org>.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+defmodule Zenflows.DID do
+@moduledoc """
+A module to interact with the did controller instances over HTTPS.
+"""
+
+alias Zenflows.VF.Person
+
+def child_spec(_) do
+ Supervisor.child_spec(
+ {Zenflows.HTTPC,
+ name: __MODULE__,
+ scheme: scheme(),
+ host: host(),
+ port: port(),
+ },
+ id: __MODULE__)
+end
+
+# Execute a Zencode specified by `name` with JSON data `data`.
+@spec exec(String.t(), map()) :: {:ok, map()} | {:error, term()}
+defp exec(name, post_data) do
+ Zenflows.Restroom.request(&Zenflows.HTTPC.request(__MODULE__, &1, &2, &3, &4),
+ "/v1/sandbox/#{name}", post_data)
+end
+
+@spec did_id(Person.t()) :: String.t()
+defp did_id(person) do
+ "did:dyne:ifacer:#{person.eddsa_public_key}"
+end
+
+@spec get_did(Person.t()) :: {:ok, map()} | {:error, term()}
+def get_did(person) do
+ with {:ok, %{status: stat, data: body}} when stat == 200 <-
+ Zenflows.HTTPC.request(__MODULE__, "GET",
+ "/dids/#{did_id(person)}") do
+ case Jason.decode(body) do
+ {:ok, data} -> {:ok, %{"created" => false, "did" => data}}
+ {:error, err} -> {:error, err}
+ end
+ else
+ _err -> {:error, "DID not found"}
+ end
+end
+
+@spec request_new_did(Person.t()) :: {:ok, map()} | {:error, term()}
+def request_new_did(person) do
+ did_request = %{
+ "proof" => %{
+ "type" => "EcdsaSecp256k1Signature2019",
+ "proofPurpose" => "assertionMethod"
+ },
+ "@context" => [
+ "https://www.w3.org/ns/did/v1",
+ "https://w3id.org/security/suites/ed25519-2018/v1",
+ "https://w3id.org/security/suites/secp256k1-2019/v1",
+ "https://w3id.org/security/suites/secp256k1-2020/v1",
+ "https://dyne.github.io/W3C-DID/specs/ReflowBLS12381.json",
+ %{
+ "description" => "https://schema.org/description",
+ "identifier" => "https://schema.org/identifier"
+ }
+ ],
+ "did_spec" => "ifacer",
+ "signer_did_spec" => "ifacer.A",
+ "identity" => "Ifacer user test",
+ "ifacer_id" => %{"identifier" => person.id},
+ "bitcoin_public_key" => person.bitcoin_public_key,
+ "ecdh_public_key" => person.ecdh_public_key,
+ "eddsa_public_key" => person.eddsa_public_key,
+ "ethereum_address" => person.ethereum_address,
+ "reflow_public_key" => person.reflow_public_key,
+ "timestamp" =>
+ DateTime.utc_now() |> DateTime.to_unix(:millisecond) |> to_string
+ }
+
+ with {:ok, did} <-
+ Zenflows.Restroom.exec("pubkeys-request-signed",
+ Map.merge(did_request, keyring())),
+ {:ok, did_signed} <- exec("pubkeys-accept.chain", did)
+ do
+ {:ok, %{"created" => true, "did" => did_signed}}
+ end
+end
+
+@spec claim(Ecto.Repo.t(), %{person: Person.t()}) :: {:ok, map()} | {:error, term()}
+def claim(_repo, %{person: person}) do
+ if keyring() == nil do
+ {:error, "DID Controller not configured"}
+ else
+ case get_did(person) do
+ {:ok, did} -> {:ok, did}
+ _ -> request_new_did(person)
+ end
+ end
+end
+
+# Return the scheme of did from the configs.
+@spec scheme() :: :http | :https
+defp scheme() do
+ Keyword.fetch!(conf(), :did_uri).scheme
+end
+
+# Return the hostname of did from the configs.
+@spec host() :: String.t()
+defp host() do
+ Keyword.fetch!(conf(), :did_uri).host
+end
+
+# Return the port of did from the configs.
+@spec port() :: non_neg_integer()
+defp port() do
+ Keyword.fetch!(conf(), :did_uri).port
+end
+
+# Return the private keyring of the server from the configs.
+@spec keyring() :: nil | map()
+defp keyring() do
+ Keyword.fetch!(conf(), :did_keyring)
+end
+
+# Return the application configurations of this module.
+@spec conf() :: Keyword.t()
+defp conf() do
+ Application.fetch_env!(:zenflows, __MODULE__)
+end
+end
diff --git a/src/zenflows/httpc.ex b/src/zenflows/httpc.ex
@@ -18,6 +18,8 @@ def start_link(opts) do
GenServer.start_link(__MODULE__, {scheme, host, port}, name: name)
end
+@spec request(term(), term(), term(), term())
+ :: {:ok, term()} | {:error, term()}
def request(name, method, path, headers \\ [], body \\ nil, max \\ 5) do
headers =
case :lists.keyfind("user-agent", 1, headers) do
diff --git a/src/zenflows/restroom.ex b/src/zenflows/restroom.ex
@@ -24,7 +24,7 @@ def child_spec(_) do
Supervisor.child_spec(
{Zenflows.HTTPC,
name: __MODULE__,
- scheme: :http,
+ scheme: scheme(),
host: host(),
port: port(),
},
@@ -77,13 +77,23 @@ end
# Execute a Zencode specified by `name` with JSON data `data`.
@spec exec(String.t(), map()) :: {:ok, map()} | {:error, term()}
-defp exec(name, post_data) do
+def exec(name, post_data) do
+ request(&Zenflows.HTTPC.request(__MODULE__, &1, &2, &3, &4),
+ name, post_data)
+end
+
+@doc """
+Given the request function (wrapper of Zenflows.HTTPC.request), the path
+and the data to post, it makes the request and parse the result.
+"""
+@spec request(fun(), String.t(), map()) :: {:ok, map()} | {:error, term()}
+def request(request_fn, path, post_data) do
hdrs = [{"content-type", "application/json"}]
with {:ok, post_body} <- Jason.encode(%{data: post_data}),
- {:ok, %{status: stat, data: body}} when stat == 200 or stat == 500 <-
- request("POST", "/api/#{name}", hdrs, post_body),
- {:ok, data} <- Jason.decode(body) do
+ {:ok, %{status: stat, data: body}} when stat == 200 or stat == 500 <-
+ request_fn.("POST", "/api/#{path}", hdrs, post_body),
+ {:ok, data} <- Jason.decode(body) do
if stat == 200 do
{:ok, data}
else
@@ -91,32 +101,34 @@ defp exec(name, post_data) do
end
else
{:ok, %{status: stat, data: body}} ->
- {:error, "the http call result in non-200 status code #{stat}: #{inspect(body)}"}
+ {:error, "the http call result in non-200 status code #{stat}: #{inspect(body)}"}
other -> other
end
end
-defp request(method, path, headers, body) do
- Zenflows.HTTPC.request(__MODULE__, method, path, headers, body)
-end
-
# Return the salt from the configs.
@spec salt() :: String.t()
defp salt() do
Keyword.fetch!(conf(), :room_salt)
end
+# Return the scheme of restroom from the configs.
+@spec scheme() :: :http | :https
+defp scheme() do
+ Keyword.fetch!(conf(), :room_uri).scheme
+end
+
# Return the hostname of restroom from the configs.
@spec host() :: String.t()
defp host() do
- Keyword.fetch!(conf(), :room_host)
+ Keyword.fetch!(conf(), :room_uri).host
end
# Return the port of restroom from the configs.
@spec port() :: non_neg_integer()
defp port() do
- Keyword.fetch!(conf(), :room_port)
+ Keyword.fetch!(conf(), :room_uri).port
end
# Return the application configurations of this module.
diff --git a/src/zenflows/vf/agent.ex b/src/zenflows/vf/agent.ex
@@ -40,7 +40,7 @@ alias Zenflows.VF.SpatialThing
eddsa_public_key: String.t() | nil,
ethereum_address: String.t() | nil,
reflow_public_key: String.t() | nil,
- schnorr_public_key: String.t() | nil,
+ bitcoin_public_key: String.t() | nil,
# organization
classified_as: [String.t()] | nil,
@@ -62,7 +62,7 @@ schema "vf_agent" do
field :eddsa_public_key, :string
field :ethereum_address, :string
field :reflow_public_key, :string
- field :schnorr_public_key, :string
+ field :bitcoin_public_key, :string
# organization
field :classified_as, {:array, :string}
diff --git a/src/zenflows/vf/economic_resource/domain.ex b/src/zenflows/vf/economic_resource/domain.ex
@@ -301,7 +301,7 @@ def trace_dpp_ee_before_recurse(%EconomicResource{} = item, visited, depth) do
trace_dpp_er_before(item, visited, depth)
end
def trace_dpp_ee_before_recurse(%EconomicEvent{} = item, visited, depth) do
- trace_dpp_pr_before(item, MapSet.put(visited, {item.__struct__, item.id}), depth)
+ trace_dpp_ee_before(item, MapSet.put(visited, {item.__struct__, item.id}), depth)
end
def trace_dpp_ee_before_recurse(%Process{} = item, visited, depth) do
trace_dpp_pr_before(item, MapSet.put(visited, {item.__struct__, item.id}), depth)
diff --git a/src/zenflows/vf/person.ex b/src/zenflows/vf/person.ex
@@ -37,7 +37,7 @@ alias Zenflows.VF.SpatialThing
eddsa_public_key: String.t() | nil,
ethereum_address: String.t() | nil,
reflow_public_key: String.t() | nil,
- schnorr_public_key: String.t() | nil,
+ bitcoin_public_key: String.t() | nil,
}
schema "vf_agent" do
@@ -52,7 +52,7 @@ schema "vf_agent" do
field :eddsa_public_key, :string
field :ethereum_address, :string
field :reflow_public_key, :string
- field :schnorr_public_key, :string
+ field :bitcoin_public_key, :string
timestamps()
end
@@ -63,7 +63,7 @@ end
eddsa_public_key
ethereum_address
reflow_public_key
- schnorr_public_key
+ bitcoin_public_key
]a
# TODO: Maybe add email to @update_cast as well?
# TODO: Maybe add the pubkeys to @update_cast as well?
@@ -85,7 +85,7 @@ def changeset(params) do
|> Validate.key(:eddsa_public_key)
|> Validate.key(:ethereum_address)
|> Validate.key(:reflow_public_key)
- |> Validate.key(:schnorr_public_key)
+ |> Validate.key(:bitcoin_public_key)
|> Validate.email(:email)
|> Changeset.unique_constraint(:user)
|> Changeset.unique_constraint(:name)
diff --git a/src/zenflows/vf/person/domain.ex b/src/zenflows/vf/person/domain.ex
@@ -119,6 +119,19 @@ def delete(id) do
end
end
+@spec claim(Schema.id()) ::
+ {:ok, Person.t()} | {:error, String.t() | Changeset.t()}
+def claim(id) do
+ Multi.new()
+ |> multi_one(id)
+ |> Multi.run(:claim_id, &Zenflows.DID.claim/2)
+ |> Repo.transaction()
+ |> case do
+ {:ok, %{claim_id: value}} -> {:ok, value}
+ {:error, _, reason, _} -> {:error, reason}
+ end
+end
+
@spec delete!(Schema.id()) :: Person.t()
def delete!(id) do
{:ok, value} = delete(id)
diff --git a/src/zenflows/vf/person/resolv.ex b/src/zenflows/vf/person/resolv.ex
@@ -66,6 +66,10 @@ def delete_person(%{id: id}, _) do
end
end
+def claim_person(%{id: id}, _) do
+ Domain.claim(id)
+end
+
def images(per, _, _) do
per = Domain.preload(per, :images)
{:ok, per.images}
diff --git a/src/zenflows/vf/person/type.ex b/src/zenflows/vf/person/type.ex
@@ -40,7 +40,7 @@ who have no physical location.
@eddsa_public_key "eddsa public key, encoded by zenroom"
@ethereum_address "ethereum address, encoded by zenroom"
@reflow_public_key "reflow public key, encoded by zenroom"
-@schnorr_public_key "schnorr public key, encoded by zenroom"
+@bitcoin_public_key "bitcoin public key, encoded by zenroom"
@desc "A natural person."
object :person do
@@ -79,8 +79,8 @@ object :person do
@desc @reflow_public_key
field :reflow_public_key, :string
- @desc @schnorr_public_key
- field :schnorr_public_key, :string
+ @desc @bitcoin_public_key
+ field :bitcoin_public_key, :string
end
@desc "Person eddsa public key"
@@ -120,8 +120,8 @@ input_object :person_create_params do
@desc @reflow_public_key
field :reflow_public_key, :string
- @desc @schnorr_public_key
- field :schnorr_public_key, :string
+ @desc @bitcoin_public_key
+ field :bitcoin_public_key, :string
end
input_object :person_update_params do
@@ -227,5 +227,10 @@ object :mutation_person do
arg :id, non_null(:id)
resolve &Resolv.delete_person/2
end
+
+ field :claim_person, non_null(:json) do
+ arg :id, non_null(:id)
+ resolve &Resolv.claim_person/2
+ end
end
end
diff --git a/test/help/factory.ex b/test/help/factory.ex
@@ -273,7 +273,7 @@ def build(:person) do
eddsa_public_key: Base.encode64("some eddsa_public_key"),
ethereum_address: Base.encode64("some ethereum_address"),
reflow_public_key: Base.encode64("some reflow_public_key"),
- schnorr_public_key: Base.encode64("some schnorr_public_key"),
+ bitcoin_public_key: Base.encode64("some bitcoin_public_key"),
}
end
diff --git a/test/vf/agent/type.test.exs b/test/vf/agent/type.test.exs
@@ -98,7 +98,7 @@ describe "Query agent()" do
eddsaPublicKey
ethereumAddress
reflowPublicKey
- schnorrPublicKey
+ bitcoinPublicKey
}
}
}
@@ -115,7 +115,7 @@ describe "Query agent()" do
assert data["eddsaPublicKey"] == per.eddsa_public_key
assert data["ethereumAddress"] == per.ethereum_address
assert data["reflowPublicKey"] == per.reflow_public_key
- assert data["schnorrPublicKey"] == per.schnorr_public_key
+ assert data["bitcoinPublicKey"] == per.bitcoin_public_key
end
test "as Organization", %{org: org} do
diff --git a/test/vf/person/domain.test.exs b/test/vf/person/domain.test.exs
@@ -33,7 +33,7 @@ setup do
eddsa_public_key: Base.encode64("eddsa_public_key"),
ethereum_address: Base.encode64("ethereum_address"),
reflow_public_key: Base.encode64("reflow_public_key"),
- schnorr_public_key: Base.encode64("schnorr_public_key"),
+ bitcoin_public_key: Base.encode64("bitcoin_public_key"),
},
inserted: Factory.insert!(:person),
}
@@ -67,7 +67,7 @@ describe "create/1" do
assert new.eddsa_public_key == params.eddsa_public_key
assert new.ethereum_address == params.ethereum_address
assert new.reflow_public_key == params.reflow_public_key
- assert new.schnorr_public_key == params.schnorr_public_key
+ assert new.bitcoin_public_key == params.bitcoin_public_key
end
test "with bad params: doesn't create a Person" do
@@ -87,7 +87,7 @@ describe "update/2" do
assert new.eddsa_public_key == old.eddsa_public_key
assert new.ethereum_address == old.ethereum_address
assert new.reflow_public_key == old.reflow_public_key
- assert new.schnorr_public_key == old.schnorr_public_key
+ assert new.bitcoin_public_key == old.bitcoin_public_key
end
test "with bad params: doesn't update the Person", %{inserted: old} do
@@ -101,7 +101,7 @@ describe "update/2" do
assert new.eddsa_public_key == old.eddsa_public_key
assert new.ethereum_address == old.ethereum_address
assert new.reflow_public_key == old.reflow_public_key
- assert new.schnorr_public_key == old.schnorr_public_key
+ assert new.bitcoin_public_key == old.bitcoin_public_key
end
end
diff --git a/test/vf/person/type.test.exs b/test/vf/person/type.test.exs
@@ -30,7 +30,7 @@ setup do
"eddsaPublicKey" => Base.encode64("eddsa_public_key"),
"ethereumAddress" => Base.encode64("ethereum_address"),
"reflowPublicKey" => Base.encode64("reflow_public_key"),
- "schnorrPublicKey" => Base.encode64("schnorr_public_key"),
+ "bitcoinPublicKey" => Base.encode64("bitcoin_public_key"),
},
inserted: Factory.insert!(:person),
}
@@ -48,7 +48,7 @@ fragment person on Person {
eddsaPublicKey
ethereumAddress
reflowPublicKey
- schnorrPublicKey
+ bitcoinPublicKey
}
"""
@@ -73,7 +73,7 @@ describe "Query" do
assert data["eddsaPublicKey"] == new.eddsa_public_key
assert data["ethereumAddress"] == new.ethereum_address
assert data["reflowPublicKey"] == new.reflow_public_key
- assert data["schnorrPublicKey"] == new.schnorr_public_key
+ assert data["bitcoinPublicKey"] == new.bitcoin_public_key
end
end
@@ -139,7 +139,7 @@ describe "Mutation" do
assert data["eddsaPublicKey"] == old.eddsa_public_key
assert data["ethereumAddress"] == old.ethereum_address
assert data["reflowPublicKey"] == old.reflow_public_key
- assert data["schnorrPublicKey"] == old.schnorr_public_key
+ assert data["bitcoinPublicKey"] == old.bitcoin_public_key
end
test "deletePerson: doesn't delete the person without the admin key", %{inserted: new} do