Skip to content

Commit

Permalink
WIP universal signatures adapter
Browse files Browse the repository at this point in the history
  • Loading branch information
patatoid committed Nov 17, 2024
1 parent a0b47c5 commit 4aca768
Show file tree
Hide file tree
Showing 7 changed files with 255 additions and 49 deletions.
13 changes: 7 additions & 6 deletions lib/boruta/adapters/internal/signatures.ex
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ defmodule Boruta.Internal.Signatures do

@moduledoc false

alias Boruta.Internal.Signatures.KeyPair
alias Boruta.Internal.Signatures.SigningKey
alias Boruta.Oauth.Client

@signature_algorithms [
Expand Down Expand Up @@ -135,13 +135,14 @@ defmodule Boruta.Internal.Signatures do
end
end

@spec verifiable_credential_sign(payload :: map(), client :: Client.t()) ::
@spec verifiable_credential_sign(payload :: map(), client :: Client.t(), format :: String.t()) ::
jwt :: String.t() | {:error, reason :: String.t()}
def verifiable_credential_sign(
payload,
%Client{
id_token_signature_alg: signature_alg
} = client
} = client,
_format
) do
with {:ok, signing_key} <- get_signing_key(client, :verifiable_credential) do
signer =
Expand Down Expand Up @@ -186,7 +187,7 @@ defmodule Boruta.Internal.Signatures do

defp get_signing_key(client, :id_token) do
{:ok,
%KeyPair{
%SigningKey{
type: :internal,
private_key: client.private_key,
secret: client.secret,
Expand All @@ -196,7 +197,7 @@ defmodule Boruta.Internal.Signatures do

defp get_signing_key(client, :userinfo) do
{:ok,
%KeyPair{
%SigningKey{
type: :internal,
private_key: client.private_key,
secret: client.secret,
Expand All @@ -206,7 +207,7 @@ defmodule Boruta.Internal.Signatures do

defp get_signing_key(client, :verifiable_credential) do
{:ok,
%KeyPair{
%SigningKey{
type: :internal,
private_key: client.private_key,
secret: client.secret,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
defmodule Boruta.Internal.Signatures.KeyPair do
defmodule Boruta.Internal.Signatures.SigningKey do
@moduledoc false

@enforce_keys [:type]
Expand Down
4 changes: 2 additions & 2 deletions lib/boruta/adapters/signatures.ex
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ defmodule Boruta.SignaturesAdapter do
def userinfo_sign(payload, client), do: signatures().userinfo_sign(payload, client)

@impl Boruta.Openid.Signatures
def verifiable_credential_sign(payload, client),
do: signatures().verifiable_credential_sign(payload, client)
def verifiable_credential_sign(payload, client, format),
do: signatures().verifiable_credential_sign(payload, client, format)

@impl Boruta.Oauth.Signatures
def kid_from_private_key(private_pem), do: signatures().kid_from_private_key(private_pem)
Expand Down
214 changes: 214 additions & 0 deletions lib/boruta/adapters/universal/signatures.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
defmodule Boruta.Universal.Signatures do
@behaviour Boruta.Oauth.Signatures

defmodule Token do
@moduledoc false

use Joken.Config

def token_config, do: %{}
end

@moduledoc false

import Boruta.Config, only: [universal_did_auth: 0]

alias Boruta.Internal.Signatures.SigningKey
alias Boruta.Oauth.Client

@signature_algorithms [
ES256: [type: :asymmetric, hash_algorithm: :SHA256, binary_size: 16],
ES384: [type: :asymmetric, hash_algorithm: :SHA384, binary_size: 24],
ES512: [type: :asymmetric, hash_algorithm: :SHA512, binary_size: 32],
RS256: [type: :asymmetric, hash_algorithm: :SHA256, binary_size: 16],
RS384: [type: :asymmetric, hash_algorithm: :SHA384, binary_size: 24],
RS512: [type: :asymmetric, hash_algorithm: :SHA512, binary_size: 32],
HS256: [type: :symmetric, hash_algorithm: :SHA256, binary_size: 16],
HS384: [type: :symmetric, hash_algorithm: :SHA384, binary_size: 24],
HS512: [type: :symmetric, hash_algorithm: :SHA512, binary_size: 32]
]

@spec signature_algorithms() :: list(atom())
def signature_algorithms, do: Keyword.keys(@signature_algorithms)

@spec hash_alg(Client.t()) :: hash_alg :: atom()
def hash_alg(%Client{id_token_signature_alg: signature_alg}),
do: @signature_algorithms[String.to_atom(signature_alg)][:hash_algorithm]

@spec hash_binary_size(Client.t()) :: binary_size :: integer()
def hash_binary_size(%Client{id_token_signature_alg: signature_alg}),
do: @signature_algorithms[String.to_atom(signature_alg)][:binary_size]

@spec hash(string :: String.t(), client :: Client.t()) :: hash :: String.t()
def hash(string, client) do
hash_alg(client)
|> Atom.to_string()
|> String.downcase()
|> String.to_atom()
|> :crypto.hash(string)
|> binary_part(0, hash_binary_size(client))
|> Base.url_encode64(padding: false)
end

@spec id_token_sign(payload :: map(), client :: Client.t()) ::
jwt :: String.t() | {:error, reason :: String.t()}
def id_token_sign(
payload,
%Client{
id_token_signature_alg: signature_alg
} = client
) do
with {:ok, signing_key} <- get_signing_key(client, :id_token) do
signer =
case id_token_signature_type(client) do
:symmetric ->
Joken.Signer.create(signature_alg, signing_key.secret)

:asymmetric ->
Joken.Signer.create(
signature_alg,
%{"pem" => signing_key.private_key},
%{
"kid" => signing_key.kid,
"trust_chain" => signing_key.trust_chain
}
)
end

case Token.encode_and_sign(payload, signer) do
{:ok, token, _payload} ->
token

{:error, error} ->
{:error, "Could not sign the given payload with client credentials: #{inspect(error)}"}
end
end
end

@spec verify_id_token_signature(id_token :: String.t(), jwk :: JOSE.JWK.t()) ::
:ok | {:error, reason :: String.t()}
def verify_id_token_signature(id_token, jwk) do
case Joken.peek_header(id_token) do
{:ok, %{"alg" => alg}} ->
signer = Joken.Signer.create(alg, %{"pem" => JOSE.JWK.from_map(jwk) |> JOSE.JWK.to_pem()})

case Token.verify(id_token, signer) do
{:ok, claims} -> {:ok, claims}
{:error, reason} -> {:error, inspect(reason)}
end

{:error, reason} ->
{:error, inspect(reason)}
end
end

@spec userinfo_sign(payload :: map(), client :: Client.t()) ::
jwt :: String.t() | {:error, reason :: String.t()}
def userinfo_sign(
payload,
%Client{
userinfo_signed_response_alg: signature_alg
} = client
) do
with {:ok, signing_key} <- get_signing_key(client, :userinfo) do
signer =
case userinfo_signature_type(client) do
:symmetric ->
Joken.Signer.create(signature_alg, signing_key.secret)

:asymmetric ->
Joken.Signer.create(
signature_alg,
%{"pem" => signing_key.private_key},
%{
"kid" => signing_key.kid,
"trust_chain" => signing_key.trust_chain
}
)
end

case Token.encode_and_sign(payload, signer) do
{:ok, token, _payload} ->
token

{:error, error} ->
{:error, "Could not sign the given payload with client credentials: #{inspect(error)}"}
end
end
end

@universal_format %{
"jwt_vc" => "jwt"
}
@spec verifiable_credential_sign(payload :: map(), client :: Client.t(), format :: String.t()) ::
jwt :: String.t() | {:error, reason :: String.t()}
def verifiable_credential_sign(
credential,
%Client{
id_token_signature_alg: signature_alg
} = client,
format
) do
payload = %{
"credential" => credential,
"options" => %{
"format" => @universal_format[format]
}
}
case Finch.build(:post, "https://api.godiddy.com/1.0.0/universal-issuer/credentials/issue", [
{"Authorization", "Bearer #{universal_did_auth()[:token]}"},
{"Content-Type", "application/json"}
], Jason.encode!(payload))
|> Finch.request(OpenIDHttpClient) |> dbg do
{:ok, %Finch.Response{body: body, status: 201}} ->
body
{:ok, %Finch.Response{body: body}} ->
{:error, "Could not sign verifiable credential - #{body}"}
{:error, error} ->
{:error, "Could not sign verifiable credential - #{inspect(error)}"}
end
end

@spec kid_from_private_key(private_pem :: String.t()) :: kid :: String.t()
def kid_from_private_key(private_pem) do
:crypto.hash(:md5, private_pem) |> Base.url_encode64() |> String.slice(0..16)
end

@spec userinfo_signature_type(Client.t()) :: userinfo_token_signature_type :: atom()
def userinfo_signature_type(%Client{userinfo_signed_response_alg: signature_alg}),
do: @signature_algorithms[String.to_atom(signature_alg)][:type]

@spec id_token_signature_type(Client.t()) :: id_token_signature_type :: atom()
def id_token_signature_type(%Client{id_token_signature_alg: signature_alg}),
do: @signature_algorithms[String.to_atom(signature_alg)][:type]

defp get_signing_key(client, :id_token) do
{:ok,
%SigningKey{
type: :internal,
private_key: client.private_key,
secret: client.secret,
kid: client.id_token_kid || kid_from_private_key(client.private_key)
}}
end

defp get_signing_key(client, :userinfo) do
{:ok,
%SigningKey{
type: :internal,
private_key: client.private_key,
secret: client.secret,
kid: client.id
}}
end

defp get_signing_key(client, :verifiable_credential) do
{:ok,
%SigningKey{
type: :internal,
private_key: client.private_key,
secret: client.secret,
kid: client.did || client.id_token_kid || kid_from_private_key(client.private_key)
}}
end
end
2 changes: 1 addition & 1 deletion lib/boruta/openid/contexts/signatures.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@ defmodule Boruta.Openid.Signatures do
@doc """
Signs the given payload according tot the given client and generates a verifiable credential
"""
@callback verifiable_credential_sign(payload :: map(), client :: Boruta.Oauth.Client.t()) ::
@callback verifiable_credential_sign(payload :: map(), client :: Boruta.Oauth.Client.t(), format :: String.t()) ::
jwt :: String.t() | {:error, reason :: String.t()}
end
Loading

0 comments on commit 4aca768

Please sign in to comment.