Skip to content

Commit

Permalink
Elixir contracts signing and validation checking
Browse files Browse the repository at this point in the history
  • Loading branch information
CDimonaco committed Nov 7, 2024
1 parent 3625cbd commit 73f041f
Show file tree
Hide file tree
Showing 5 changed files with 602 additions and 143 deletions.
229 changes: 162 additions & 67 deletions elixir/lib/contracts.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ defmodule Trento.Contracts do

require Logger

@default_event_validity 20

@doc """
Return the content type of contracts
"""
Expand All @@ -14,10 +16,62 @@ defmodule Trento.Contracts do
Encode and wrap an event struct to a protobuf CloudEvent.
"""
@spec to_event(struct(), Keyword.t()) :: binary()
def to_event(%mod{} = struct, opts \\ []) do
def to_event(%{} = struct, opts \\ []) do
struct
|> build_cloud_event(opts)
|> CloudEvents.CloudEvent.encode()
end

@doc """
Encode and wrap an event struct to a signed protobuf Cloudevent.
Accepts the private key as a pem file content.
Accepts the same options as to_event/2
"""
def to_signed_event(struct, pem_private_key, opts \\ []) do
struct
|> build_cloud_event(opts)
|> add_signature(pem_private_key)
|> CloudEvents.CloudEvent.encode()
end

@doc """
Decode and unwrap a protobuf CloudEvent to an event struct.
"""
@spec from_event(binary()) ::
{:ok, struct()}
| {:error, :decoding_error}
| {:error, :invalid_envelope}
| {:error, :unknown_event}
| {:error, :event_expired}
def from_event(value) do
with {:ok, type, cloud_event} <- decode_cloud_event(value),
{:ok, cloud_event} <- verify_event_validity(cloud_event) do
decode_trento_event(type, cloud_event)
end
end

@doc """
Decode and unwrap a signed protobuf CloudEvent to an event struct.
"""
@spec from_signed_event(binary(), binary()) ::
{:ok, struct()}
| {:error, :decoding_error}
| {:error, :invalid_envelope}
| {:error, :unknown_event}
| {:error, :event_expired}
| {:error, :invalid_event_signature}
def from_signed_event(value, public_key) do
with {:ok, event_type, event_data} <- decode_cloud_event(value),
{:ok, event_data} <- verify_event_signature(event_data, public_key),
{:ok, event_data} <- verify_event_validity(event_data) do
decode_trento_event(event_type, event_data)
end
end

defp build_cloud_event(%mod{} = struct, opts) do
id = Keyword.get(opts, :id, UUID.uuid4())
source = Keyword.get(opts, :source, "trento")
private_key = Keyword.get(opts, :private_key, "")
validity_in_seconds = Keyword.get(opts, :validity_in_seconds, @default_event_validity)

time =
Keyword.get(
Expand All @@ -26,36 +80,68 @@ defmodule Trento.Contracts do
DateTime.utc_now()
)

expiration = DateTime.add(time, validity_in_seconds)

time_attr = %Google.Protobuf.Timestamp{seconds: time |> DateTime.to_unix()}
expire_at_attr = %Google.Protobuf.Timestamp{seconds: expiration |> DateTime.to_unix()}

data = Protobuf.Encoder.encode(struct)

cloud_event = %CloudEvents.CloudEvent{
%CloudEvents.CloudEvent{
data: {:proto_data, %Google.Protobuf.Any{value: data, type_url: get_type(mod)}},
spec_version: "1.0",
type: get_type(mod),
id: id,
attributes: %{
"time" => %CloudEvents.CloudEventAttributeValue{attr: {:ce_timestamp, time_attr}},
"expire_at" => %CloudEvents.CloudEventAttributeValue{
attr: {:ce_timestamp, expire_at_attr}
}
},
source: source
} |> add_signature(private_key)
}
end

defp add_signature(
%CloudEvents.CloudEvent{
data: {:proto_data, %Google.Protobuf.Any{value: data}},
attributes:
%{
"time" => %CloudEvents.CloudEventAttributeValue{
attr: {:ce_timestamp, %{seconds: time}}
},
"expire_at" => %CloudEvents.CloudEventAttributeValue{
attr: {:ce_timestamp, %{seconds: expire_at}}
}
} = current_attrs
} = event,
private_key_content
) do
signing_key =
private_key_content
|> :public_key.pem_decode()
|> Enum.at(0)
|> :public_key.pem_entry_decode()

CloudEvents.CloudEvent.encode(cloud_event)
time_str = Integer.to_string(time)
signature = :public_key.sign("#{data}#{time_str}#{expire_at}", :sha256, signing_key)

%CloudEvents.CloudEvent{
event
| attributes:
Map.put(
current_attrs,
"signature",
%CloudEvents.CloudEventAttributeValue{attr: {:ce_bytes, signature}}
)
}
end

@doc """
Decode and unwrap a protobuf CloudEvent to an event struct.
"""
@spec from_event(binary()) ::
{:ok, struct()}
| {:error, :decoding_error}
| {:error, :invalid_envelope}
| {:error, :event_not_found}
def from_event(value) do
defp decode_cloud_event(data) do
try do
case CloudEvents.CloudEvent.decode(value) do
%{type: type, data: {:proto_data, %Google.Protobuf.Any{value: data}}} ->
decode(type, data)
case CloudEvents.CloudEvent.decode(data) do
%{type: type, data: {:proto_data, %Google.Protobuf.Any{value: _data}}} = cloud_event ->
{:ok, type, cloud_event}

event ->
Logger.error("Invalid cloud event: #{inspect(event)}")
Expand All @@ -70,7 +156,63 @@ defmodule Trento.Contracts do
end
end

defp decode(type, data) do
defp verify_event_signature(
%CloudEvents.CloudEvent{
data: {:proto_data, %Google.Protobuf.Any{value: data}},
attributes: %{
"time" => %CloudEvents.CloudEventAttributeValue{
attr: {:ce_timestamp, %{seconds: time}}
},
"expire_at" => %CloudEvents.CloudEventAttributeValue{
attr: {:ce_timestamp, %{seconds: expire_at}}
},
"signature" => %CloudEvents.CloudEventAttributeValue{attr: {:ce_bytes, signature}}
}
} = event,
public_key
) do
signing_key =
public_key
|> :public_key.pem_decode()
|> Enum.at(0)
|> :public_key.pem_entry_decode()

event_signature_valid? =
:public_key.verify("#{data}#{time}#{expire_at}", :sha256, signature, signing_key)

if event_signature_valid? do
{:ok, event}
else
{:error, :invalid_event_signature}
end
end

defp verify_event_signature(_, _), do: {:error, :invalid_event_signature}

defp verify_event_validity(
%CloudEvents.CloudEvent{
attributes: %{
"expire_at" => %CloudEvents.CloudEventAttributeValue{
attr: {:ce_timestamp, %{seconds: expire_at_ts}}
}
}
} = event
) do
expire_at = DateTime.from_unix!(expire_at_ts)
event_valid? = DateTime.compare(DateTime.utc_now(), expire_at) == :lt

if event_valid? do
{:ok, event}
else
{:error, :event_expired}
end
end

defp verify_event_validity(_), do: {:error, :event_expired}

defp decode_trento_event(type, %CloudEvents.CloudEvent{
data: {:proto_data, %Google.Protobuf.Any{value: data}}
}) do
try do
module_name = Macro.camelize(type)
module = Module.safe_concat([module_name])
Expand All @@ -81,58 +223,11 @@ defmodule Trento.Contracts do
end
end

defp decode_trento_event(_, _), do: {:error, :unknown_event}

defp get_type(mod) do
mod
|> Atom.to_string()
|> String.replace("Elixir.", "")
end

defp add_signature(event, ""), do: event

defp add_signature(
%CloudEvents.CloudEvent{
data: {:proto_data, %Google.Protobuf.Any{value: data}},
attributes: %{
"time" => %CloudEvents.CloudEventAttributeValue{
attr: {:ce_timestamp, %{seconds: time}}
}
} = current_attrs
} = event, private_key) do

time_str = Integer.to_string(time)
signature = :public_key.sign(data <> time_str, :sha256, private_key)

%CloudEvents.CloudEvent{
event |
attributes: Map.put(
current_attrs,
"signature",
%CloudEvents.CloudEventAttributeValue{attr: {:ce_bytes, signature}}
)
}
end

# def add_signature(event, private_key) do
# decoded_event = %{
# data: {:proto_data, %Google.Protobuf.Any{value: data}},
# attributes: %{
# "time" => %CloudEvents.CloudEventAttributeValue{attr: {:ce_timestamp, time}}
# } = current_attrs
# } = CloudEvents.CloudEvent.decode(event)

# %{seconds: seconds} = time
# time_str = Integer.to_string(seconds)
# signature = :public_key.sign(data <> time_str, :sha256, private_key)

# updated_event = %CloudEvents.CloudEvent{
# decoded_event |
# attributes: Map.put(
# current_attrs,
# "signature",
# %CloudEvents.CloudEventAttributeValue{attr: {:ce_bytes, signature}}
# )
# }

# CloudEvents.CloudEvent.encode(updated_event)
# end
end
2 changes: 1 addition & 1 deletion elixir/mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ defmodule Proto.MixProject do
# Run "mix help compile.app" to learn about applications.
def application do
[
extra_applications: [:logger]
extra_applications: [:logger, :public_key, :crypto]
]
end

Expand Down
Loading

0 comments on commit 73f041f

Please sign in to comment.