Skip to content
This repository has been archived by the owner on Oct 30, 2023. It is now read-only.

Commit

Permalink
feat: added build and encode for ucans (unplugged version)
Browse files Browse the repository at this point in the history
  • Loading branch information
madclaws committed Oct 28, 2023
1 parent 6aacff3 commit c57586f
Show file tree
Hide file tree
Showing 7 changed files with 233 additions and 11 deletions.
10 changes: 10 additions & 0 deletions lib/ex_ucan/core/keymaterial.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# TODO: docs??

defprotocol ExUcan.Core.Keymaterial do
alias ExUcan.Core.Structs.UcanPayload
@spec did(struct()) :: String.t()
def did(type)

@spec sign(struct(), UcanPayload.t()) :: binary()
def sign(type, payload)
end
20 changes: 20 additions & 0 deletions lib/ex_ucan/core/plugins.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
defmodule ExUcan.Core.Plugins do
# TODO: docs

@spec verify_issuer_alg(String.t(), String.t()) :: boolean()
def verify_issuer_alg(did, jwt_alg) do

end

@spec parseDidMethod(String.t()) :: String.t()
defp parseDidMethod(did) do
parts = String.split(did, ":")
with {true, _} <- {Enum.at(parts, 0) == "did", 0},
{true, _} <- {String.length(Enum.at(parts, 1)) >=1, 1} do
{:ok, Enum.at(parts, 2)}
else
{false, 0} -> {:error, "Not a DID: #{did}"}
{false, 1} -> {:error, "No DID method included: #{did}"}
end
end
end
71 changes: 71 additions & 0 deletions lib/ex_ucan/core/structs.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
defmodule ExUcan.Core.Structs.UcanHeader do
@moduledoc """
Ucan header
"""

@type t :: %__MODULE__{
alg: String.t(),
typ: String.t()
}

@derive Jason.Encoder
defstruct(
alg: "",
typ: ""
)

end

defmodule ExUcan.Core.Structs.UcanPayload do
@moduledoc """
Ucan Payload
"""

@type t :: %__MODULE__{
ucv: String.t(),
iss: String.t(),
aud: String.t(),
nbf: integer(),
exp: integer(),
nnc: String.t(),
fct: map(),
cap: map(),
prf: list(String.t())
}

@derive Jason.Encoder
defstruct(
ucv: "",
iss: "",
aud: "",
nbf: 0,
exp: nil,
nnc: "",
fct: %{},
cap: %{},
prf: []
)
end

defmodule ExUcan.Core.Structs.Ucan do
@moduledoc """
UCAN struct
"""
alias ExUcan.Core.Structs.UcanHeader
alias ExUcan.Core.Structs.UcanPayload

@type t :: %__MODULE__{
header: UcanHeader.t(),
payload: UcanPayload.t(),
signed_data: String.t(),
signature: String.t()
}

@derive Jason.Encoder
defstruct(
header: nil,
payload: nil,
signed_data: "",
signature: ""
)
end
105 changes: 105 additions & 0 deletions lib/ex_ucan/core/token.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
defmodule ExUcan.Core.Token do
@moduledoc """
Creates and manages UCAN tokens
"""
alias ExUcan.Core.Structs.UcanHeader
alias ExUcan.Core.Keymaterial
alias ExUcan.Core.Structs.Ucan
alias ExUcan.Core.Utils
alias ExUcan.Core.Structs.UcanPayload

@token_type "JWT"
@version %{major: 0, minor: 10, patch: 0}

@spec build(params::%{
issuer: struct(),
audience: String.t(),
# Add capabilities struct later
capabilities: list(),
life_time_in_seconds: number(),
expiration: number(),
not_before: number(),
# Add Facts struct later
facts: list(),
proofs: list(String.t()),
add_nonce?: boolean()
}) :: Ucan.t()
def build(params) do
{:ok, payload} = build_payload(%{params | issuer: Keymaterial.did(params.issuer)})
sign_with_payload(payload, params.issuer)
end

# TODO: docs
@spec build_payload(
params :: %{
issuer: String.t(),
audience: String.t(),
# Add capabilities struct later
capabilities: list(),
life_time_in_seconds: number(),
expiration: number(),
not_before: number(),
# Add Facts struct later
facts: list(),
proofs: list(String.t()),
add_nonce?: boolean()
}
) :: UcanPayload.t()
def build_payload(params) do
with {:iss, true} <- {:iss, String.starts_with?(params.issuer, "did")},
{:aud, true} <- {:aud, String.starts_with?(params.audience, "did")} do
current_time_in_seconds = DateTime.utc_now() |> DateTime.to_unix()
exp = params.expiration || current_time_in_seconds + params.life_time_in_seconds

{:ok,
%{
ucv: "#{@version.major}.#{@version.minor}.#{@version.patch}",
iss: params.issuer,
aud: params.audience,
nbf: params[:not_before] || nil,
exp: exp,
nnc: add_nonce(params[:add_nonce] || false),
fct: params[:facts] || [],
cap: params[:capabilities] || [],
prf: params[:proofs] || []
}}
else
{:iss, false} -> {:error, "The issuer must be a DID"}
{:aud, false} -> {:error, "The audience must be a DID"}
end
end

@spec encode(Ucan.t()) :: String.t()
def encode(%Ucan{} = ucan) do
"#{ucan.signed_data}.#{ucan.signature}"
end

defp add_nonce(true), do: Utils.generate_nonce()
defp add_nonce(false), do: nil

@spec sign_with_payload(payload :: UcanPayload.t(), keypair :: struct()) :: Ucan.t()
defp sign_with_payload(payload, keypair) do

# TODO ExUcan.Core.Plugins.verify_issuer_alg
header = %UcanHeader{alg: keypair.jwt_alg, typ: @token_type}
encoded_header = encode_ucan_parts(header)
encoded_payload = encode_ucan_parts(payload)

signed_data = "#{encoded_header}.#{encoded_payload}"
signature = Keymaterial.sign(keypair, signed_data)
%Ucan{
header: header,
payload: payload,
signed_data: signed_data,
signature: Base.url_encode64(signature, padding: false)
}
end

@spec encode_ucan_parts(UcanHeader.t() | UcanPayload.t()) :: String.t()
defp encode_ucan_parts(data) do
data
|> Jason.encode!()
|> Base.url_encode64(padding: false)
end

end
15 changes: 15 additions & 0 deletions lib/ex_ucan/core/utils.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
defmodule ExUcan.Core.Utils do
@moduledoc """
Core utils
"""
@chars "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"

# TODO: docs
def generate_nonce(len \\ 6)

def generate_nonce(len) do
Enum.reduce(1..len, "", fn _, nonce ->
nonce <> String.at(@chars, :rand.uniform(String.length(@chars) - 1))
end)
end
end
18 changes: 12 additions & 6 deletions lib/ex_ucan/plugins/ed25519/keypair.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ defmodule ExUcan.Plugins.Ed25519.Keypair do
@moduledoc """
Encapsulates Ed25519 Keypair generation and other utilities
"""
alias ExUcan.Core.Keymaterial
alias ExUcan.Plugins.Ed25519.Crypto
@behaviour Keymaterial
# TODO: more doc..

# TODO: Need type doc
Expand All @@ -25,17 +27,21 @@ defmodule ExUcan.Plugins.Ed25519.Keypair do

def create(exportable?) do
{pub, priv} = :crypto.generate_key(:eddsa, :ed25519)

__MODULE__.__struct__(
%__MODULE__{
jwt_alg: "EdDSA",
secret_key: priv,
public_key: pub,
exportable: exportable?
)
}
end

@spec did(__MODULE__.t()) :: String.t()
def did(keypair) do
Crypto.publickey_to_did(keypair.public_key)
defimpl Keymaterial do
def did(keypair) do
Crypto.publickey_to_did(keypair.public_key)
end

def sign(keypair, payload) do
:public_key.sign(payload, :ignored, {:ed_pri, :ed25519, keypair.public_key, keypair.secret_key}, [])
end
end
end
5 changes: 0 additions & 5 deletions lib/ex_ucan/plugins/protocols.ex

This file was deleted.

0 comments on commit c57586f

Please sign in to comment.