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

Commit

Permalink
feat: builder flow done, tests + docs needed
Browse files Browse the repository at this point in the history
  • Loading branch information
madclaws committed Oct 29, 2023
1 parent cd54885 commit 45e91e8
Show file tree
Hide file tree
Showing 6 changed files with 195 additions and 95 deletions.
12 changes: 4 additions & 8 deletions lib/ex_ucan.ex
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,10 @@ defmodule ExUcan do
Keypair.create()
end

# TODO: to be removed
@spec build(struct(), map()) :: {:ok, Ucan.t()} | {:error, String.t()}
def build(keypair, _params) do
Token.build(%{
issuer: keypair,
audience: "did:key:z6MkwDK3M4PxU1FqcSt4quXghquH1MoWXGzTrNkNWTSy2NLD",
expiration: 86400
})
# TODO: docs
@spec sign(payload :: UcanPayload.t(), keypair :: struct()) :: Ucan.t()
def sign(payload, keypair) do
Token.sign_with_payload(payload, keypair)
end

# TODO: to be removed
Expand Down
162 changes: 162 additions & 0 deletions lib/ex_ucan/builder.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
defmodule ExUcan.Builder do
@moduledoc """
Builder functions for UCAN tokens
"""
alias ExUcan.Core.Structs.UcanPayload
alias ExUcan.Core.Token
alias ExUcan.Keymaterial.Ed25519.Keypair
alias ExUcan.Core.Capability

@type t :: %__MODULE__{
issuer: Keypair,
audience: String.t(),
capabilities: list(Capability),
lifetime: number(),
expiration: number(),
not_before: number(),
# Add Facts struct later
facts: map(),
proofs: list(String.t()),
add_nonce?: boolean()
}
defstruct [
:issuer,
:audience,
:capabilities,
:lifetime,
:expiration,
:not_before,
:facts,
:proofs,
:add_nonce?
]

@doc """
Create an empty builder.
Before finalising the builder, we need to at least call:
- `issued_by`
- `to_audience` and one of
- `with_lifetime` or `with_expiration`.
To finalise the builder, call its `build` or `build_parts` method.
"""
@spec default() :: __MODULE__.t()
def default() do
%__MODULE__{
issuer: nil,
audience: nil,
capabilities: [],
lifetime: nil,
expiration: nil,
not_before: nil,
facts: %{},
proofs: [],
add_nonce?: false
}
end

@doc """
The UCAN must be signed with the private key of the issuer to be valid.
"""
@spec issued_by(__MODULE__.t(), Keypair) :: __MODULE__.t()
def issued_by(%__MODULE__{} = builder, keypair) do
%{builder | issuer: keypair}
end

@doc """
This is the identity this UCAN transfers rights to.
It could e.g. be the DID of a service you're posting this UCAN as a JWT to,
or it could be the DID of something that'll use this UCAN as a proof to
continue the UCAN chain as an issuer.
"""
@spec for_audience(__MODULE__.t(), String.t()) :: __MODULE__.t()
def for_audience(builder, audience) do
%{builder | audience: audience}
end

@doc """
The number of seconds into the future (relative to when build() is
invoked) to set the expiration. This is ignored if an explicit expiration
is set.
"""
@spec with_lifetime(__MODULE__.t(), integer()) :: __MODULE__.t()
def with_lifetime(builder, seconds) do
%{builder | lifetime: seconds}
end

@doc """
Set the POSIX timestamp (in seconds) for when the UCAN should expire.
Setting this value overrides a configured lifetime value.
"""
@spec with_expiration(__MODULE__.t(), integer()) :: __MODULE__.t()
def with_expiration(builder, timestamp) do
%{builder | expiration: timestamp}
end

@doc """
Set the POSIX timestamp (in seconds) of when the UCAN becomes active.
"""
@spec not_before(__MODULE__.t(), integer()) :: __MODULE__.t()
def not_before(builder, timestamp) do
%{builder | not_before: timestamp}
end

@doc """
Add a fact or proof of knowledge to this UCAN.
"""
@spec with_fact(__MODULE__.t(), String.t(), any()) :: __MODULE__.t()
def with_fact(builder, key, fact) do
%{builder | facts: Map.put(builder.facts, key, fact)}
end

@doc """
Will ensure that the built UCAN includes a number used once.
"""
@spec with_nonce(__MODULE__.t()) :: __MODULE__.t()
def with_nonce(builder) do
%{builder | add_nonce?: true}
end

# TODO: try to do this function
@doc """
Includes a UCAN in the list of proofs for the UCAN to be built.
Note that the proof's audience must match this UCAN's issuer
or else the proof chain will be invalidated!
The proof is encoded into a [Cid], hashed via the [UcanBuilder::default_hasher()]
algorithm, unless one is provided.
"""
@spec witnessed_by(__MODULE__.t()) :: __MODULE__.t()
def witnessed_by(builder) do
builder
end

@doc """
Claim a capability by inheritance (from an authorizing proof) or
implicitly by ownership of the resource by this UCAN's issuer
"""
@spec claiming_capability(__MODULE__.t(), Capability) :: __MODULE__.t()
def claiming_capability(builder, capability) do
%{builder | capabilities: builder.capabilities ++ [capability]}
end

def delegating_from(builder) do
builder
end

# TODO: docs
@doc """
"""
@spec build!(__MODULE__.t()) :: String.t()
def build!(builder) do
case Token.build_payload(builder) do
{:ok, payload} -> payload
{:error, err} -> raise err
end
end

@spec build(__MODULE__.t()) :: {:ok, UcanPayload.t()} | {:error, String.t()}
def build(builder) do
Token.build_payload(builder)
end
end
21 changes: 0 additions & 21 deletions lib/ex_ucan/core/plugins.ex

This file was deleted.

92 changes: 28 additions & 64 deletions lib/ex_ucan/core/token.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,69 +2,41 @@ defmodule ExUcan.Core.Token do
@moduledoc """
Creates and manages UCAN tokens
"""
alias ExUcan.Builder
alias ExUcan.Keymaterial.Ed25519.Crypto
alias ExUcan.Core.Structs.UcanHeader
alias ExUcan.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()
@spec build_payload(params :: Builder) :: {:ok, UcanPayload.t()} | {:error, String.t()}
def build_payload(%Builder{issuer: nil}), do: {:error, "must call issued_by/2"}
def build_payload(%Builder{audience: nil}), do: {:error, "must call for_audience/2"}
def build_payload(%Builder{lifetime: nil}), do: {:error, "must call with_lifetime/2"}

def build_payload(params) do
with {:iss, true} <- {:iss, String.starts_with?(params.issuer, "did")},
did = Keymaterial.get_did(params.issuer)

with {:iss, true} <- {:iss, String.starts_with?(did, "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
exp = params.expiration || current_time_in_seconds + params.lifetime

{:ok,
%{
ucv: "#{@version.major}.#{@version.minor}.#{@version.patch}",
iss: params.issuer,
iss: did,
aud: params.audience,
nbf: params[:not_before] || nil,
nbf: params.not_before,
exp: exp,
nnc: add_nonce(params[:add_nonce] || false),
fct: params[:facts] || [],
cap: params[:capabilities] || [],
prf: params[:proofs] || []
nnc: params.add_nonce?,
fct: params.facts,
cap: params.capabilities,
prf: params.proofs
}}
else
{:iss, false} -> {:error, "The issuer must be a DID"}
Expand All @@ -85,28 +57,27 @@ defmodule ExUcan.Core.Token do
@spec validate(String.t()) :: :ok | {:error, String.t() | map()}
def validate(encoded_ucan) do
with {:ok, {_header, payload}} <- parse_encoded_ucan(encoded_ucan),
:ok <- is_expired?(payload),
:ok <- is_too_early?(payload) do
{false, _} <- {is_expired?(payload), :expired},
{false, _} <- {is_too_early?(payload), :early} do
[encoded_header, encoded_payload, encoded_sign] = String.split(encoded_ucan, ".")
{:ok, signature} = Base.url_decode64(encoded_sign, padding: false)
data = "#{encoded_header}.#{encoded_payload}"
verify_signature(payload.iss, data, signature)
else
{true, :expired} -> {:error, "Ucan token is already expired"}
{true, :early} -> {:error, "Ucan token is not yet active"}
err -> err
end
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
def sign_with_payload(payload, keypair) do
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)
IO.inspect(signature)

%Ucan{
header: header,
Expand All @@ -123,22 +94,15 @@ defmodule ExUcan.Core.Token do
|> Base.url_encode64(padding: false)
end

@spec is_expired?(UcanPayload.t()) :: :ok | {:error, String.t()}
@spec is_expired?(UcanPayload.t()) :: boolean()
defp is_expired?(%UcanPayload{} = ucan_payload) do
if ucan_payload.exp < DateTime.utc_now() |> DateTime.to_unix() do
:ok
else
{:error, "Ucan token is already expired"}
end
ucan_payload.exp < DateTime.utc_now() |> DateTime.to_unix()
end

@spec is_too_early?(UcanPayload.t()) :: :ok | {:error, String.t()}
@spec is_too_early?(UcanPayload.t()) :: boolean()
defp is_too_early?(%UcanPayload{nbf: nil}), do: false
defp is_too_early?(%UcanPayload{nbf: nbf}) do
if nbf > DateTime.utc_now() |> DateTime.to_unix() do
:ok
else
{:error, "Ucan token is not yet active"}
end
nbf > DateTime.utc_now() |> DateTime.to_unix()
end

@spec parse_encoded_ucan(String.t()) ::
Expand Down
1 change: 1 addition & 0 deletions lib/ex_ucan/keymaterial/ed25519/keypair.ex
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ defmodule ExUcan.Keymaterial.Ed25519.Keypair do
public_key: binary()
}

@derive Jason.Encoder
defstruct [:jwt_alg, :secret_key, :public_key]

@doc """
Expand Down
2 changes: 0 additions & 2 deletions lib/ex_ucan/keymaterial/protocol.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@ defprotocol ExUcan.Keymaterial do
This protocol requires four functions to be implemented, `get_jwt_algorithm_name/1`,
`get_did/1`, `sign/2` and `verify/3`
"""
alias ExUcan.Core.Structs.UcanPayload

@doc """
Returns the Jwt algorithm used by the Keypair to create Ucan
Expand Down

0 comments on commit 45e91e8

Please sign in to comment.