Skip to content

Commit

Permalink
refactor verifiable credentials status tokens
Browse files Browse the repository at this point in the history
  • Loading branch information
patatoid committed Jan 18, 2025
1 parent a9f9880 commit 954161a
Show file tree
Hide file tree
Showing 2 changed files with 129 additions and 117 deletions.
200 changes: 106 additions & 94 deletions lib/boruta/openid/verifiable_credentials.ex
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,110 @@ defmodule Boruta.Openid.VerifiableCredentials do
end
end

defmodule Status do
@moduledoc """
Implements status tokens as stated in [this specification draft](https://github.com/malach-it/vc-decentralized-status/blob/main/SPECIFICATION.md) helping to annotate identity information.
"""

@status_table [
:valid,
:suspended,
:revoked
]

@spec shift(status :: atom()) :: shift :: integer()
def shift(status) do
Atom.to_string(status)
|> :binary.decode_unsigned()
end

@spec generate_status_token(secret :: String.t(), ttl :: integer(), status :: atom()) ::
status_token :: String.t()
def generate_status_token(secret, ttl, status) do
iat =
:os.system_time(:microsecond)
|> :binary.encode_unsigned()
|> :binary.bin_to_list()
|> :string.right(7, 0)

padded_ttl =
:binary.encode_unsigned(ttl)
|> :binary.bin_to_list()
|> :string.right(4, 0)

status_list =
iat ++
padded_ttl

status_information =
status_list
|> to_string()
|> Base.url_encode64(padding: false)

derived_status =
Hotp.generate_hotp(
secret,
div(:os.system_time(:seconds), ttl) + shift(status)
)

"#{status_information}~#{derived_status}"
end

@spec verify_status_token(secret :: String.t(), status_token :: String.t()) ::
status :: atom()
def verify_status_token(secret, status_token) do
[status_list, hotp] = String.split(status_token, "~")

%{ttl: ttl} =
status_list
|> Base.url_decode64!(padding: false)
|> to_charlist()
|> parse_statuslist()

Enum.reduce_while(@status_table, :expired, fn status, acc ->
case hotp ==
Hotp.generate_hotp(
secret,
div(:os.system_time(:seconds), ttl) + shift(status)
) do
true -> {:halt, status}
false -> {:cont, acc}
end
end)
rescue
_ -> :invalid
end

def parse_statuslist(statuslist) do
parse_statuslist(statuslist, {0, %{ttl: [], memory: []}})
end

def parse_statuslist([], {_index, result}), do: result

def parse_statuslist([_char | t], {index, acc}) when index < 7 do
parse_statuslist(t, {index + 1, acc})
end

def parse_statuslist([char | t], {index, acc}) when index < 10 do
acc = Map.put(acc, :memory, acc[:memory] ++ [char])
parse_statuslist(t, {index + 1, acc})
end

def parse_statuslist([char | t], {index, acc}) when index == 10 do
acc =
acc
|> Map.put(
:ttl,
(acc[:memory] ++ [char])
|> :erlang.list_to_binary()
|> :binary.decode_unsigned()
)
|> Map.put(:memory, [])

parse_statuslist(t, {index + 1, acc})
end
end

@moduledoc false

alias Boruta.Config
Expand All @@ -55,12 +159,6 @@ defmodule Boruta.Openid.VerifiableCredentials do
# RJiimmRTMXUAa49VQ9NWT7PUK2P7VbBy4Bn"
@individual_claim_default_expiration 3600 * 24 * 365 * 120

@status_table [
:valid,
:suspended,
:revoked
]

@authorization_details_schema %{
"type" => "array",
"items" => %{
Expand Down Expand Up @@ -470,7 +568,8 @@ defmodule Boruta.Openid.VerifiableCredentials do

claims_with_salt =
Enum.map(claims, fn {name, {value, status, ttl}} ->
{{name, value}, generate_sd_salt(client.private_key, ttl, String.to_atom(status))}
{{name, value},
Status.generate_status_token(client.private_key, ttl, String.to_atom(status))}
end)

disclosures =
Expand Down Expand Up @@ -518,88 +617,6 @@ defmodule Boruta.Openid.VerifiableCredentials do
defp generate_credential(_claims, _credential_configuration, _proof, _client, _format),
do: {:error, "Unkown format."}

def generate_sd_salt(secret, ttl, status) do
iat =
:os.system_time(:microsecond)
|> :binary.encode_unsigned()
|> :binary.bin_to_list()
|> :string.right(7, 0)

padded_ttl =
:binary.encode_unsigned(ttl)
|> :binary.bin_to_list()
|> :string.right(4, 0)

status_list =
iat ++
padded_ttl

salt =
status_list
|> to_string()
|> Base.url_encode64(padding: false)

hotp =
Hotp.generate_hotp(
secret,
div(:os.system_time(:seconds), ttl) + shift(status)
)

"#{salt}~#{hotp}"
end

def verify_salt(secret, salt) do
[status_list, hotp] = String.split(salt, "~")

%{ttl: ttl} =
status_list
|> Base.url_decode64!(padding: false)
|> to_charlist()
|> parse_statuslist()

Enum.reduce_while(@status_table, :expired, fn status, acc ->
case hotp ==
Hotp.generate_hotp(
secret,
div(:os.system_time(:seconds), ttl) + shift(status)
) do
true -> {:halt, status}
false -> {:cont, acc}
end
end)
rescue
_ -> :invalid
end

def parse_statuslist(statuslist) do
parse_statuslist(statuslist, {0, %{ttl: [], memory: []}})
end

def parse_statuslist([], {_index, result}), do: result

def parse_statuslist([_char | t], {index, acc}) when index < 7 do
parse_statuslist(t, {index + 1, acc})
end

def parse_statuslist([char | t], {index, acc}) when index < 10 do
acc = Map.put(acc, :memory, acc[:memory] ++ [char])
parse_statuslist(t, {index + 1, acc})
end

def parse_statuslist([char | t], {index, acc}) when index == 10 do
acc =
acc
|> Map.put(
:ttl,
(acc[:memory] ++ [char])
|> :erlang.list_to_binary()
|> :binary.decode_unsigned()
)
|> Map.put(:memory, [])

parse_statuslist(t, {index + 1, acc})
end

defp extract_credential_claims(resource_owner, credential_configuration) do
claims =
credential_configuration[:claims]
Expand Down Expand Up @@ -640,9 +657,4 @@ defmodule Boruta.Openid.VerifiableCredentials do

defp do_validate_headers([error | checks], errors),
do: do_validate_headers(checks, errors ++ [error])

def shift(status) do
Atom.to_string(status)
|> :binary.decode_unsigned()
end
end
46 changes: 23 additions & 23 deletions test/boruta/openid/verifiable_credentials_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -344,11 +344,11 @@ defmodule Boruta.Openid.VerifiableCredentialsTest do
valid_salt_key = valid_salt
|> String.split("~")
|> List.last()
VerifiableCredentials.verify_salt(token.client.private_key, valid_salt)
VerifiableCredentials.Status.verify_status_token(token.client.private_key, valid_salt)

assert valid_salt_key == VerifiableCredentials.Hotp.generate_hotp(
token.client.private_key,
div(:os.system_time(:seconds), 3600) + VerifiableCredentials.shift(:valid)
div(:os.system_time(:seconds), 3600) + VerifiableCredentials.Status.shift(:valid)
)

suspended_salt = String.split(credential, "~")
Expand All @@ -359,11 +359,11 @@ defmodule Boruta.Openid.VerifiableCredentialsTest do
suspended_salt_key = suspended_salt
|> String.split("~")
|> List.last()
VerifiableCredentials.verify_salt(token.client.private_key, suspended_salt)
VerifiableCredentials.Status.verify_status_token(token.client.private_key, suspended_salt)

assert suspended_salt_key == VerifiableCredentials.Hotp.generate_hotp(
token.client.private_key,
div(:os.system_time(:seconds), 3600) + VerifiableCredentials.shift(:suspended)
div(:os.system_time(:seconds), 3600) + VerifiableCredentials.Status.shift(:suspended)
)
end

Expand Down Expand Up @@ -419,7 +419,7 @@ defmodule Boruta.Openid.VerifiableCredentialsTest do

assert suspended_salt_key == VerifiableCredentials.Hotp.generate_hotp(
token.client.private_key,
div(:os.system_time(:seconds), 3600) + VerifiableCredentials.shift(:suspended)
div(:os.system_time(:seconds), 3600) + VerifiableCredentials.Status.shift(:suspended)
)
end

Expand Down Expand Up @@ -475,7 +475,7 @@ defmodule Boruta.Openid.VerifiableCredentialsTest do

assert revoked_salt_key == VerifiableCredentials.Hotp.generate_hotp(
token.client.private_key,
div(:os.system_time(:seconds), 3600) + VerifiableCredentials.shift(:revoked)
div(:os.system_time(:seconds), 3600) + VerifiableCredentials.Status.shift(:revoked)
)
end

Expand Down Expand Up @@ -545,32 +545,32 @@ defmodule Boruta.Openid.VerifiableCredentialsTest do
end
end

describe "generate_sd_salt/3" do
describe "Status.generate_status/3" do
test "generate a ten years valid salt" do
secret = "secret"
expiration = 3600 * 24 * 365 * 10
:binary.encode_unsigned(expiration) |> :binary.bin_to_list()
status = :valid
salt = VerifiableCredentials.generate_sd_salt(secret, expiration, status)
salt = VerifiableCredentials.Status.generate_status_token(secret, expiration, status)

assert String.split(salt, "~") |> List.last() == VerifiableCredentials.Hotp.generate_hotp(
secret,
div(:os.system_time(:seconds), expiration) + VerifiableCredentials.shift(:valid)
div(:os.system_time(:seconds), expiration) + VerifiableCredentials.Status.shift(:valid)
)
assert VerifiableCredentials.verify_salt(secret, salt) == :valid
assert VerifiableCredentials.Status.verify_status_token(secret, salt) == :valid
end

test "generate a valid salt" do
secret = "secret"
expiration = 60
status = :valid
salt = VerifiableCredentials.generate_sd_salt(secret, expiration, status)
salt = VerifiableCredentials.Status.generate_status_token(secret, expiration, status)

assert String.split(salt, "~") |> List.last() == VerifiableCredentials.Hotp.generate_hotp(
secret,
div(:os.system_time(:seconds), expiration) + VerifiableCredentials.shift(:valid)
div(:os.system_time(:seconds), expiration) + VerifiableCredentials.Status.shift(:valid)
)
assert VerifiableCredentials.verify_salt(secret, salt) == :valid
assert VerifiableCredentials.Status.verify_status_token(secret, salt) == :valid
end

test "generate a thousand salt" do
Expand All @@ -581,13 +581,13 @@ defmodule Boruta.Openid.VerifiableCredentialsTest do
status = Enum.random(statuses)
expiration = Enum.random(1..3600)

assert salt = VerifiableCredentials.generate_sd_salt(secret, expiration, status)
assert salt = VerifiableCredentials.Status.generate_status_token(secret, expiration, status)
{status, salt}
end)

Enum.map(salts, fn {status, salt} ->
:timer.tc(fn ->
assert VerifiableCredentials.verify_salt(secret, salt) == status
assert VerifiableCredentials.Status.verify_status_token(secret, salt) == status
end)
end)
end
Expand All @@ -596,32 +596,32 @@ defmodule Boruta.Openid.VerifiableCredentialsTest do
secret = "secret"
expiration = 60
status = :revoked
salt = VerifiableCredentials.generate_sd_salt(secret, expiration, status)
salt = VerifiableCredentials.Status.generate_status_token(secret, expiration, status)

assert String.split(salt, "~") |> List.last() == VerifiableCredentials.Hotp.generate_hotp(
secret,
div(:os.system_time(:seconds), expiration) + VerifiableCredentials.shift(:revoked)
div(:os.system_time(:seconds), expiration) + VerifiableCredentials.Status.shift(:revoked)
)
assert VerifiableCredentials.verify_salt(secret, salt) == :revoked
assert VerifiableCredentials.Status.verify_status_token(secret, salt) == :revoked
end

test "generate a suspended salt" do
secret = "secret"
expiration = 60
status = :suspended
salt = VerifiableCredentials.generate_sd_salt(secret, expiration, status)
salt = VerifiableCredentials.Status.generate_status_token(secret, expiration, status)

assert String.split(salt, "~") |> List.last() == VerifiableCredentials.Hotp.generate_hotp(
secret,
div(:os.system_time(:seconds), expiration) + VerifiableCredentials.shift(:suspended)
div(:os.system_time(:seconds), expiration) + VerifiableCredentials.Status.shift(:suspended)
)
assert VerifiableCredentials.verify_salt(secret, salt) == :suspended
assert VerifiableCredentials.Status.verify_status_token(secret, salt) == :suspended
end
end

describe "verify_salt/2" do
describe "Status.verify_status_token/2" do
test "returns invalid" do
assert VerifiableCredentials.verify_salt("secret", "invalid salt") == :invalid
assert VerifiableCredentials.Status.verify_status_token("secret", "invalid salt") == :invalid
end
end

Expand Down

0 comments on commit 954161a

Please sign in to comment.