~doma/do-auth

d2a28de2e48dfd3c739b40633b280c4ab4814826 — Jonn 4 months ago 3f03572
Problem: Credential isn't inserted as one piece

Solution:

 - Make a DB IO function "tx_from_keypair_credential!/2" that makes
	credential under transaction
 - Add DBUtils.now() to have PG-compatible timestamps in it
 - Add a way to get an entity by DID DB id. (!!) This might turn out to
	be an anti-feature, because, as per shower-ponderings, for high
	availability purposes and easier sharding, we should never rely on DB
	ids. Perhaps it's worth wrapping it in something like `by_did!` which
	will first find relevant DID's ID and then call `by_did_id` under it.
	Perhaps it also makes sense to make `by_did_id` private.
 - Rename functions that take B64 encoded strings to have "64" in their
	name for additional explicitness

Further work:
 - Check that if transaction fails, nothing gets inserted
M lib/do_auth/db_utils.ex => lib/do_auth/db_utils.ex +8 -0
@@ 85,6 85,14 @@ defmodule DoAuth.DBUtils do
  end

  @doc """
  PostgreSQL-compatible current UTC timestamp.
  """
  @spec now :: DateTime.t()
  def now() do
    DateTime.utc_now() |> DateTime.truncate(:second)
  end

  @doc """
  Standard changeset that's used rather often. Casts and requires the same fileds.
  """
  @spec castreq(map() | {Changeset.data(), Changeset.types()}, map(), atom() | list(atom) | atom) ::

M lib/do_auth/schema/credential.ex => lib/do_auth/schema/credential.ex +34 -1
@@ 1,5 1,7 @@
defmodule DoAuth.Credential do
  use DoAuth.DBUtils, into: __MODULE__
  alias DoAuth.DBUtils
  alias DoAuth.DID
  alias DoAuth.Entity
  alias DoAuth.Subject
  alias DoAuth.Proof


@@ 8,7 10,7 @@ defmodule DoAuth.Credential do
  alias DoAuth.CredentialContext, as: CC
  alias DoAuth.CredentialType
  alias DoAuth.CredentialCredentialType, as: CCT
  # alias Ecto.Multi
  alias Ecto.Multi

  schema "credentials" do
    belongs_to(:issuer, Entity)


@@ 19,9 21,39 @@ defmodule DoAuth.Credential do
    field(:timestamp, :utc_datetime)
  end

  def preload_entity(), do: [:issuer, [did: :key]]

  @doc """
  Makes a credential from a keypair serialisable map (claim).
  """
  def tx_from_keypair_credential!(kp = %{public: pk}, claim) do
    {:ok, {:ok, cred}} =
      Repo.transaction(fn ->
        tau0 = DBUtils.now()
        did = DID.by_pk64(pk |> Crypto.show()) |> Repo.one()
        entity = Entity.by_did_id(did.id) |> Repo.one() |> Repo.preload(preload_entity())
        {:ok, subject} = %{claim: claim} |> Subject.changeset() |> Repo.insert(returning: true)

        proofless = %__MODULE__{
          timestamp: tau0,
          issuer: entity,
          subject: subject,
          contexts: [],
          types: []
        }

        sig = proofless |> to_map(proofless: true) |> Proof.sign_map(kp)
        {:ok, proof} = Proof.from_sig(entity, sig.signature |> Crypto.show()) |> Repo.insert()
        %{proofless | proof: proof} |> Repo.insert(returning: true)
      end)

    cred
  end

  @doc """
  The part of credential used to create a proof.
  """
  @spec proofless_json(%__MODULE__{}) :: String.t()
  def proofless_json(cred = %__MODULE__{}) do
    cred |> to_map(proofless: true) |> Jason.encode!()
  end


@@ 31,6 63,7 @@ defmodule DoAuth.Credential do
  @doc """
  Verifies a proof of Jason.encode!'ed proofless part of a credential.
  """
  @spec verify(%__MODULE__{}, binary()) :: boolean()
  def verify(cred = %__MODULE__{proof: %Proof{signature: sig}}, pk) do
    proofless = proofless_json(cred)
    Crypto.verify(proofless, %{public: pk, signature: sig |> Crypto.read!()})

M lib/do_auth/schema/did.ex => lib/do_auth/schema/did.ex +17 -6
@@ 60,20 60,31 @@ defmodule DoAuth.DID do
  end

  @doc """
  Takes just the URLSAFE Base64 encoding of a public key (as opposed to a whole
  structure), and selects the DID derived from it.
  """
  @spec by_pk64(String.t()) :: Ecto.Query.t()
  def by_pk64(pk64) do
    from(d in __MODULE__,
      where: d.body == ^hash(pk64)
    )
  end

  @doc """
  Takes URLSAFE Base64 public key, inserts it and inserts a DID derived from it.
  """
  @spec from_new_pk(String.t() | Key.mp(), mp()) :: Ecto.Multi.t()
  def from_new_pk(pkparams = %{}, didparams = %{}) do
  @spec from_new_pk64(String.t() | Key.mp(), mp()) :: Ecto.Multi.t()
  def from_new_pk64(pkparams = %{}, didparams = %{}) do
    Ecto.Multi.new()
    |> Ecto.Multi.insert(:insert_key, Key.changeset(%Key{}, pkparams), mopts())
    |> Ecto.Multi.insert(:insert_did, &from_new_pk_changeset(didparams).(&1), mopts())
    |> Ecto.Multi.insert(:insert_did, &from_new_pk64_changeset(didparams).(&1), mopts())
  end

  def from_new_pk(<<pk::binary>>, didparams) do
    from_new_pk(%{public_key: pk}, didparams)
  def from_new_pk64(<<pk::binary>>, didparams) do
    from_new_pk64(%{public_key: pk}, didparams)
  end

  defp from_new_pk_changeset(didparams) do
  defp from_new_pk64_changeset(didparams) do
    fn %{insert_key: key} ->
      %__MODULE__{key_id: key.id, body: hash(key.public_key)} |> changeset(didparams)
    end

M lib/do_auth/schema/entity.ex => lib/do_auth/schema/entity.ex +7 -0
@@ 28,9 28,16 @@ defmodule DoAuth.Entity do

  def to_map(x), do: to_map(x, [])

  @spec from_did(any) :: Ecto.Changeset.t()
  def from_did(did), do: changeset(%{did: did})
  def from_issuer(issuer), do: changeset(%{issuer: issuer})

  def by_did_id(did_id) do
    from(e in __MODULE__,
      where: e.did_id == ^did_id
    )
  end

  @doc """
  Sadly, it seems like `changeset`s aren't compatible with this use-case,
  i.e. there is no function that would simply cast a preloaded instance of a

M test/db_test.exs => test/db_test.exs +20 -4
@@ 37,11 37,27 @@ defmodule DBTest do

  ############

  test "Credential.keypair_and_credential_tx inserts credentials" do
    kp = %{public: pk} = Crypto.server_keypair()
    # Make sure that stuff's there
    {:ok, %{insert_did: did}} = DID.from_new_pk64(pk |> Crypto.show(), %{}) |> Repo.transaction()
    {:ok, _entity} = Entity.from_did(did) |> Repo.insert(returning: true)

    cred = Credential.tx_from_keypair_credential!(kp, %{powo: "my love forever"})

    require Logger
    Logger.info("Here's a claim for y'all #{inspect(cred, pretty: true)}")

    assert(Credential.verify(cred, pk))
  end

  ############

  test "credentials can be stored" do
    tau0 = DateTime.utc_now() |> DateTime.truncate(:second)
    # Start making issuer of credential, which is a DID-Entity
    kp = %{public: pk} = Crypto.server_keypair()
    {:ok, %{insert_did: did}} = DID.from_new_pk(pk |> Crypto.show(), %{}) |> Repo.transaction()
    {:ok, %{insert_did: did}} = DID.from_new_pk64(pk |> Crypto.show(), %{}) |> Repo.transaction()
    {:ok, entity} = Entity.from_did(did) |> Repo.insert(returning: true)
    # End making issuer of credential, and store it in "entity"



@@ 119,7 135,7 @@ defmodule DBTest do

  test "DID has canonical representation" do
    %{public: pk} = Crypto.server_keypair()
    {:ok, %{insert_did: did}} = DID.from_new_pk(pk |> Crypto.show(), %{}) |> Repo.transaction()
    {:ok, %{insert_did: did}} = DID.from_new_pk64(pk |> Crypto.show(), %{}) |> Repo.transaction()

    assert(
      DID.show(did |> Repo.preload(:key)) ==


@@ 137,7 153,7 @@ defmodule DBTest do
      |> Crypto.derive_signing_keypair(42)
      |> Map.fetch!(:public)
      |> Crypto.show()
      |> DoAuth.DID.from_new_pk(%{})
      |> DoAuth.DID.from_new_pk64(%{})
      |> Repo.transaction()

    pk =


@@ 191,7 207,7 @@ defmodule DBTest do
      |> Crypto.derive_signing_keypair(42)
      |> Map.fetch!(:public)
      |> Crypto.show()
      |> DoAuth.DID.from_new_pk(%{})
      |> DoAuth.DID.from_new_pk64(%{})
      |> Repo.transaction()

    # Casually testing select btw