Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
181 changes: 181 additions & 0 deletions lib/TrustSpanningProtocol/endpoint.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
defmodule TSP.Endpoint do
alias Cesr.CountCodeKERIv2.CD_A_GenericGroup
alias Cesr.CountCodeKERIv2.CD_dashE_BigESSRWrapperGroup
alias Cesr.CountCodeKERIv2.CD_dashJ_BigGenericListGroup
alias Cesr.CountCodeKERIv2.CD_dashZ_BigESSRPayloadGroup
alias Cesr.Primitive.CesrBytes
alias TSP.Endpoint
alias TSP.Keystore
alias TSP.Message
alias TSP.Relationship

use GenServer
@enforce_keys [:relationships]
defstruct [:relationships]

@impl true
def init(:empty) do
{:ok, %Endpoint{relationships: []}}
end

@impl true
def handle_call({:add_vid, our_vid}, _from, %Endpoint{} = state) do
case Enum.any?(state.relationships, fn r -> r.our_vid == our_vid end) do
true -> {:reply, :error_vid_already_exists, state} # could also be a no-op
false -> {:reply, :ok, %{state | :relationships => state.relationships ++ [Relationship.new(our_vid)]}}
end
end

def handle_call({:oobi, our_vid, their_vid, their_pid}, _from, %Endpoint{} = state) do
case Enum.find(state.relationships, fn r -> r.our_vid == our_vid end) do
:nil -> {:reply, :error_vid_not_created_yet, state}
old_relationship -> {:ok, their_keystore} = GenServer.call(their_pid, {:public_keys, their_vid})
new_relationship = %{old_relationship | their_vid: their_vid, their_pid: their_pid, their_keystore: their_keystore}
other_relationships = Enum.filter(state.relationships, fn r -> r.our_vid != our_vid end)
{:reply, :ok, %{state | :relationships => other_relationships ++ [new_relationship]}}
end
end

def handle_call({:public_keys, our_vid}, _from, %Endpoint{} = state) do
case Enum.find(state.relationships, fn r -> r.our_vid == our_vid end) do
:nil -> {:reply, :error_keystore_not_found, state}
old_relationship -> public_keystore = %{Keystore.new() | crypto_public_key: old_relationship.our_keystore.crypto_public_key,
sig_public_key: old_relationship.our_keystore.sig_public_key}
{:reply, {:ok, public_keystore}, state}
end
end

def handle_call({:create_message, our_vid, their_vid, route, payload}, _from, %Endpoint{} = state) do
case Enum.find(state.relationships, fn r -> r.our_vid == our_vid end) do
:nil -> {:reply, :error_keystore_not_found, state}
old_relationship -> message = Message.new(our_vid, their_vid, route, payload,
old_relationship.their_keystore.crypto_public_key,
old_relationship.our_keystore.sig_secret_key)
{:reply, {:ok, message}, state}
end
end

# routed message: verify it, unseal it, reseal for next recipient, sign and pass it on
def handle_call({:route_message, %Message{
envelope: %CD_dashE_BigESSRWrapperGroup{cesr_elements:
[_tsp_version, %CesrBytes{code: _code_1, payload: vid_sender},
%CesrBytes{code: _code_2, payload: vid_receiver}]},
payload: %CD_dashZ_BigESSRPayloadGroup{cesr_elements:
[_payload_tag, %CesrBytes{code: _code_3, payload: vid_sender_payload},
%CD_dashJ_BigGenericListGroup{cesr_elements: [
%CesrBytes{code: _code_4, payload: first_hop},
%CesrBytes{code: _code_5, payload: next_hop} | rest_of_hops]}, _payload_group]},
signature: _signature} = message}, _from, %Endpoint{} = state) do
with :ok <- (if vid_sender == vid_sender_payload, do: :ok, else: :error_vid_mismatch),
:ok <- (if vid_receiver == first_hop, do: :ok, else: :error_wrong_recipient),
%Relationship{} = incoming_relationship <- Enum.find(state.relationships,
fn r -> r.our_vid == vid_receiver end),
%Relationship{} = outgoing_relationship <- Enum.find(state.relationships,
fn r -> r.their_vid == next_hop end),
:ok <- (if :true == Message.verify_signature(message, incoming_relationship.their_keystore.sig_public_key),
do: :ok, else: :error_sig_not_verified),
unsealed_payload <- Message.unseal_payload(message, incoming_relationship.our_keystore.crypto_public_key,
incoming_relationship.our_keystore.crypto_secret_key),
next_hops_string_list <- [next_hop | Enum.map(rest_of_hops, fn %CesrBytes{code: _code, payload: payload} -> payload end)],
%Message{} = outgoing_message <- Message.new(outgoing_relationship.our_vid,
next_hop, next_hops_string_list, unsealed_payload,
outgoing_relationship.their_keystore.crypto_public_key,
outgoing_relationship.our_keystore.sig_secret_key) do
{:reply, {:next_route, outgoing_relationship.their_pid, outgoing_message}, state}
else
:nil -> {:reply, :error_relationship_not_found, state}
error -> {:reply, error, state}
end
end
# terminating condition: one entry in hop list
def handle_call({:route_message, %Message{
envelope: %CD_dashE_BigESSRWrapperGroup{cesr_elements:
[_tsp_version, %CesrBytes{code: _code_1, payload: vid_sender},
%CesrBytes{code: _code_2, payload: vid_receiver}]},
payload: %CD_dashZ_BigESSRPayloadGroup{cesr_elements:
[_payload_tag, %CesrBytes{code: _code_3, payload: vid_sender_payload},
%CD_dashJ_BigGenericListGroup{cesr_elements: [
%CesrBytes{code: _code_4, payload: final_vid}]}, _payload_group]},
signature: _signature} = message}, _from, %Endpoint{} = state) do
with :ok <- (if vid_sender == vid_sender_payload, do: :ok, else: :error_vid_mismatch),
:ok <- (if vid_receiver == final_vid, do: :ok, else: :error_wrong_recipient),
%Relationship{} = relationship <- Enum.find(state.relationships,
fn r -> r.our_vid == vid_receiver end),
:ok <- (if :true == Message.verify_signature(message, relationship.their_keystore.sig_public_key),
do: :ok, else: :error_sig_not_verified),
unsealed_payload <- Message.unseal_payload(message, relationship.our_keystore.crypto_public_key,
relationship.our_keystore.crypto_secret_key) do

{:reply, {:route_done, unsealed_payload}, state}
else
:nil -> {:reply, :error_relationship_not_found, state}
error -> {:reply, error, state}
end
end
# invalid case: tried to route a message with an empty hop list
def handle_call({:route_message, %Message{
envelope: _envelope,
payload: %CD_dashZ_BigESSRPayloadGroup{cesr_elements:
[_payload_tag, _vid_sender_payload, %CD_dashJ_BigGenericListGroup{cesr_elements: []},
%CD_A_GenericGroup{cesr_elements: [_payload_primitive]}]},
signature: _signature}}, _from, %Endpoint{} = state) do
{:reply, :error_empty_hop_list, state}
end

def handle_call({:unseal_payload, our_vid, message}, _from, %Endpoint{} = state) do
case Enum.find(state.relationships, fn r -> r.our_vid == our_vid end) do
:nil -> {:reply, :error_keystore_not_found, state}
old_relationship -> payload = Message.unseal_payload(message,
old_relationship.our_keystore.crypto_public_key,
old_relationship.our_keystore.crypto_secret_key)
{:reply, payload, state}
end
end

def handle_call({:verify_signature, vid_sender, message}, _from, %Endpoint{} = state) do
# verify from either endpoint for convenience (the sig key is public knowledge)
reply = case Enum.find(state.relationships, fn r -> r.our_vid == vid_sender end) do
:nil -> case Enum.find(state.relationships, fn r -> r.their_vid == vid_sender end) do
:nil -> :error_keystore_not_found
relationship -> Message.verify_signature(message, relationship.their_keystore.sig_public_key)
end
relationship -> Message.verify_signature(message, relationship.our_keystore.sig_public_key)
end
{:reply, reply, state}
end

# everything below this is just to verify that endpoints control keys
# (https://trustoverip.github.io/tswg-tsp-specification/#verification)

def handle_call({:verify_crypto_key, our_vid, encrypted_payload}, _from, %Endpoint{} = state) do
case Enum.find(state.relationships, fn r -> r.our_vid == our_vid end) do
:nil -> {:reply, :error_crypto_key_not_found, state}
relationship -> decrypted_payload = :libsodium_crypto_box.seal_open(encrypted_payload,
relationship.our_keystore.crypto_public_key,
relationship.our_keystore.crypto_secret_key)
{:reply, {:ok, decrypted_payload}, state}
end
end

def handle_call({:verify_sig_key, our_vid, payload}, _from, %Endpoint{} = state) do
case Enum.find(state.relationships, fn r -> r.our_vid == our_vid end) do
:nil -> {:reply, :error_public_key_not_found, state}
relationship -> encrypted_payload = :libsodium_crypto_sign_ed25519.crypto_sign_ed25519(
payload, relationship.our_keystore.sig_secret_key)
{:reply, {:ok, encrypted_payload}, state}
end
end

def verify_keys(their_vid, their_pid) do
with {:ok, their_keystore} <- GenServer.call(their_pid, {:public_keys, their_vid}),
payload <- :base64.encode(:crypto.strong_rand_bytes(8)),
encrypted_payload <- :libsodium_crypto_box.seal(payload, their_keystore.crypto_public_key),
{:ok, decrypted_payload} <- GenServer.call(their_pid, {:verify_crypto_key, their_vid, encrypted_payload}),
crypto_verified <- payload == decrypted_payload,
{:ok, signed_payload} <- GenServer.call(their_pid, {:verify_sig_key, their_vid, payload}),
sig_verified <- payload == :libsodium_crypto_sign_ed25519.open(signed_payload, their_keystore.sig_public_key)
do
if crypto_verified and sig_verified, do: :ok, else: :error_verification_failed
end
end
end
29 changes: 29 additions & 0 deletions lib/TrustSpanningProtocol/keystore.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
defmodule TSP.Keystore do
alias TSP.Keystore

@enforce_keys [:sig_public_key, :sig_secret_key, :crypto_public_key, :crypto_secret_key]
defstruct [:sig_public_key, :sig_secret_key, :crypto_public_key, :crypto_secret_key]

def new() do
%Keystore{
sig_public_key: :nil,
sig_secret_key: :nil,
crypto_public_key: :nil,
crypto_secret_key: :nil
}
end

def random() do
# converting as per https://libsodium.gitbook.io/doc/advanced/ed25519-curve25519
# can also use two different keypairs
with {sig_public_key, sig_secret_key} <- :libsodium_crypto_sign_ed25519.keypair()
do
%Keystore{
sig_public_key: sig_public_key,
sig_secret_key: sig_secret_key,
crypto_public_key: :libsodium_crypto_sign_ed25519.pk_to_curve25519(sig_public_key),
crypto_secret_key: :libsodium_crypto_sign_ed25519.sk_to_curve25519(sig_secret_key)
}
end
end
end
119 changes: 119 additions & 0 deletions lib/TrustSpanningProtocol/message.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
defmodule TSP.Message do
alias Cesr
alias Cesr.CountCodeKERIv2.CD_A_GenericGroup
alias Cesr.CountCodeKERIv2.CD_dashC_BigAttachmentGroup
alias Cesr.CountCodeKERIv2.CD_dashE_BigESSRWrapperGroup
alias Cesr.CountCodeKERIv2.CD_dashJ_BigGenericListGroup
alias Cesr.CountCodeKERIv2.CD_dashK_BigControllerIdxSigs
alias Cesr.CountCodeKERIv2.CD_dashZ_BigESSRPayloadGroup
alias Cesr.Primitive.CD_0B_Ed25519_signature
alias Cesr.Primitive.CD_X_Tag3
alias Cesr.Primitive.CD_Y_Tag7
alias Cesr.Primitive.CesrBytes
alias TSP.Message

@enforce_keys [:envelope, :payload, :signature]
defstruct [:envelope, :payload, :signature]

# 3. Messages (https://trustoverip.github.io/tswg-tsp-specification/#messages)
# TSP_Message = {TSP_Envelope, TSP_Payload, TSP_Signature}
def new(sender, receiver, route, plaintext, receiver_crypto_public_key, sender_sig_secret_key) do
with {:ok, envelope} <- make_envelope(sender, receiver),
{:ok, payload} <- make_payload(sender, route, plaintext, receiver_crypto_public_key),
{:ok, signature} <- make_signature(envelope, payload, sender_sig_secret_key) do
%Message{envelope: envelope, payload: payload, signature: signature}
end
end

defp make_envelope(sender, receiver) do
# 3.1 TSP Envelope (https://trustoverip.github.io/tswg-tsp-specification/#tsp-envelope)
# TSP_Envelope = {TSP_Tag, TSP_Version, VID_sndr, VID_rcvr | NULL}
with {{:ok, tsp_version}, ""} = CD_Y_Tag7.from_b64("YTSP-AAB"),
{:ok, vid_sender} = CesrBytes.new(sender),
{:ok, vid_receiver} = CesrBytes.new(receiver) do
# 9.1 TSP Envelope Encoding (https://trustoverip.github.io/tswg-tsp-specification/#tsp-envelope-encoding)
# tsp_tag: wrap everything in a BigESSRWrapperGroup (-E) (indicates it's an envelope)
# tsp_version: first version is YTSP-AAB (Y indicates a 7-letter tag primitive,
# TSP == Trust Spanning Protocol, AAB == 001 in base64 indices)
# vid_sender: encode as variable-length primitive (4A/5A/etc depending on padding)
# vid_receiver: optional, same encoding as receiver (we'll use :nil if it's missing)
CD_dashE_BigESSRWrapperGroup.new([tsp_version, vid_sender, vid_receiver])
end
end

defp make_cesr_bytes!(string) do
{:ok, cesr} = CesrBytes.new(string)
cesr
end

defp make_payload(sender, route, plaintext, receiver_crypto_public_key) do
# 9.2 TSP Payload Encoding (https://trustoverip.github.io/tswg-tsp-specification/#tsp-payload-encoding)
# The cesr stream looks something like this, "|" meaning "or":
# -Z## | -0Z####, XSCS, VID_sndr, Padding_field, -A## | -0A####, higher-layer-interleaved-payload-stream
# -Z: wrap everything in a BigESSRPayloadGroup (Encrypt Sender, Sign Receiver)
# XSCS: put "SCS" in a 3-letter tag (payload is a Sniffable CESR Stream)
# VID_sndr: encode as a variable-length primitive (the "encrypt sender" part of ESSR)
# hop_list: list of vids to route the message to (empty if it's a direct message)
# padding: skipping this since our cesr implementation can calculate padding properly
# -A: wrap the payload in a GenericGroup
# higher-layer-interleaved-payload-stream: encrypted payload (or nested TSP message)
with {{:ok, payload_tag}, ""} <- CD_X_Tag3.from_b64("XSCS"),
{:ok, vid_sender} <- CesrBytes.new(sender),
{:ok, hop_list} <- CD_dashJ_BigGenericListGroup.new(Enum.map(route, &make_cesr_bytes!/1)),
# {:ok, padding_field} <- CesrBytes.new(""),
encrypted_payload <- :libsodium_crypto_box.seal(plaintext, receiver_crypto_public_key),
{:ok, payload_cesr} <- CesrBytes.new(encrypted_payload) do
CD_dashZ_BigESSRPayloadGroup.new([payload_tag, vid_sender, hop_list, CD_A_GenericGroup.new([payload_cesr])])
end
end

defp make_signature(envelope, payload, sender_sig_secret_key) do
# 3.3 TSP Signature (https://trustoverip.github.io/tswg-tsp-specification/#tsp-signature)
# TSP_Signature = TSP_SIGN({TSP_Envelope, TSP_Payload})
with text <- Cesr.produce_text_stream([envelope, payload]),
signature <- :libsodium_crypto_sign_ed25519.detached(text, sender_sig_secret_key),
{:ok, sig_cesr} <- CD_0B_Ed25519_signature.new(signature) do
# 9.3 TSP Signature Encoding (https://trustoverip.github.io/tswg-tsp-specification/#tsp-signature-encoding)
# 1. wrap everything in a BigAttachmentGroup (-C) so we know it's signatures
# 2. inside that, wrap signatures & indexes in a BigControllerIdxSigs (-K)
# 3. signature types match the keys, we used ed25519 so Ed25519_signature (0B)
CD_dashC_BigAttachmentGroup.new([CD_dashK_BigControllerIdxSigs.new([sig_cesr])])
end
end

def unseal_payload(%Message{envelope: _envelope,
payload: %CD_dashZ_BigESSRPayloadGroup{cesr_elements:
[_payload_tag, _vid_sender, _hop_list, %CD_A_GenericGroup{cesr_elements: [payload_primitive]}]},
signature: _signature}, receiver_crypto_public_key, receiver_crypto_secret_key) do
:libsodium_crypto_box.seal_open(payload_primitive.payload, receiver_crypto_public_key,
receiver_crypto_secret_key)
end
def unseal_payload(_message, _crypto_public_key, _crypto_secret_key), do: :error_malformed_message

def verify_signature(%Message{envelope: envelope, payload: payload,
signature: %CD_dashC_BigAttachmentGroup{cesr_elements: [
%CD_dashK_BigControllerIdxSigs{cesr_elements: [signature]}
]}}, sender_sig_public_key) do
text_to_sign = Cesr.produce_text_stream([envelope, payload])
case :libsodium_crypto_sign_ed25519.verify_detached(signature.payload, text_to_sign, sender_sig_public_key) do
0 -> :true # (it's a port of a C API)
_ -> :false
end
end
def verify_signature(_message, _sig_public_key), do: :error_malformed_message

def to_b64(%Message{envelope: envelope, payload: payload, signature: signature}) do
# have to wrap it or it's not a valid count code...
with {:ok, wrapped_message} <- CD_A_GenericGroup.new([envelope, payload, signature]) do
Cesr.produce_text_stream([wrapped_message])
end
end
def to_b64(_message), do: :error_malformed_message

def from_b64(bytes) do
with {:ok, %CD_A_GenericGroup{cesr_elements: [envelope, payload, signature]}}
<- Cesr.consume_primitive_T(bytes) do
%Message{envelope: envelope, payload: payload, signature: signature}
end
end
end
17 changes: 17 additions & 0 deletions lib/TrustSpanningProtocol/relationship.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
defmodule TSP.Relationship do
alias TSP.Keystore
alias TSP.Relationship

@enforce_keys [:our_vid, :their_vid, :our_keystore, :their_keystore, :their_pid]
defstruct [:our_vid, :their_vid, :our_keystore, :their_keystore, :their_pid]

def new(our_vid) do
%Relationship{
our_vid: our_vid,
their_vid: :nil,
our_keystore: Keystore.random(),
their_keystore: Keystore.new(),
their_pid: :nil
}
end
end
3 changes: 2 additions & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ defmodule Cesr.MixProject do
# has support for OrdMap serialization/deserialization.
{:cbor, "~> 1.0", hex: :cbor_ordmap},
{:dialyxir, "~> 1.4", only: [:dev], runtime: false},
{:ex_doc, "0.39.1", only: [:dev], runtime: false}
{:ex_doc, "0.39.1", only: [:dev], runtime: false},
{:libsodium, "~> 2.0.0"}
]
end
end
Loading