Skip to content

Commit a4416cd

Browse files
committed
feat: Handle the first TO0 message
Adds the logic for the Pairing Service to send the first TO0 Protocol message to the Rendezvous Server and to receive the associated HelloAck as response. An associated module to test the added logic is also included. Signed-off-by: Riccardo Nalgi <riccardo.nalgi@secomind.com>
1 parent fb74b98 commit a4416cd

6 files changed

Lines changed: 188 additions & 1 deletion

File tree

apps/astarte_pairing/README.md

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ database, (we suggest scylla)
3333

3434
``` shell
3535
docker run --rm -d -p 9042:9042 --name scylla scylladb/scylla
36-
docker run --rn -d --net=host -p 8080/tcp ispirata/docker-alpine-cfssl-autotest:astarte
36+
docker run --rm -d -p 5672:5672 -p 15672:15672 --name rabbit rabbitmq:3.12.0-management
37+
docker run --rm -d --net=host -p 8080/tcp ispirata/docker-alpine-cfssl-autotest:astarte
3738
```
3839

3940

@@ -49,3 +50,29 @@ these resources are located.
4950
``` shell
5051
CASSANDRA_NODES=localhost CFSSL_API_URL=http://localhost:8080 mix test
5152
```
53+
54+
# Test FDO
55+
56+
To test FDO, the manufacturer and Device CA keys are required and
57+
can be generated from the following tools:
58+
59+
# Generate manufacturer keys
60+
docker run --rm \
61+
-v $(pwd)/compose/fdo-keys:/keys \
62+
quay.io/fido-fdo/admin-cli:latest \
63+
generate-key-and-cert manufacturer \
64+
--destination-dir /keys
65+
66+
# Generate device CA keys
67+
docker run --rm \
68+
-v $(pwd)/compose/fdo-keys:/keys \
69+
quay.io/fido-fdo/admin-cli:latest \
70+
generate-key-and-cert device-ca \
71+
--destination-dir /keys
72+
73+
# Set permissions
74+
chmod 644 compose/fdo-keys/*.pem
75+
chmod 600 compose/fdo-keys/*.der
76+
77+
An ownership voucher and owner private key are also required.
78+
For testing purposes, a mock key and voucher have been stored in the priv folder of the Pairing Service.
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
#
2+
# This file is part of Astarte.
3+
#
4+
# Copyright 2017-2025 SECO Mind Srl
5+
#
6+
# Licensed under the Apache License, Version 2.0 (the "License");
7+
# you may not use this file except in compliance with the License.
8+
# You may obtain a copy of the License at
9+
#
10+
# http://www.apache.org/licenses/LICENSE-2.0
11+
#
12+
# Unless required by applicable law or agreed to in writing, software
13+
# distributed under the License is distributed on an "AS IS" BASIS,
14+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
# See the License for the specific language governing permissions and
16+
# limitations under the License.
17+
#
18+
19+
defmodule Astarte.Pairing.FDOClient do
20+
alias Astarte.Pairing.Config
21+
alias Astarte.Pairing.TO0Util
22+
23+
require Logger
24+
25+
@doc """
26+
TO0.Hello - Type 20 message to initiate TO0 protocol
27+
Sends an empty array as per FDO specification section 5.3.1
28+
Returns decoded TO0.HelloAck (message 21) with rendezvous nonce
29+
"""
30+
def to0_hello() do
31+
url = "#{fdo_rendezvous_url!()}/fdo/101/msg/20"
32+
headers = [{"Content-Type", "application/cbor"}, {"Content-Length", "0"}]
33+
request_body = CBOR.encode([])
34+
35+
Logger.debug("Sending TO0.Hello to FDO rendezvous server...", url: url)
36+
37+
case http_client().post(url, request_body, headers) do
38+
{:ok, %{status_code: 200, headers: headers, body: body}} ->
39+
Logger.debug("TO0.Hello completed successfully")
40+
Logger.debug(inspect(headers))
41+
42+
case TO0Util.get_nonce_from_hello_ack(body) do
43+
{:ok, nonce} ->
44+
# TODO: to0owner_sign
45+
{:ok, nonce}
46+
47+
{:error, reason} ->
48+
Logger.error("Failed to get nonce from TO0.HelloAck", reason: reason)
49+
end
50+
51+
{:ok, %{status_code: status_code, body: body}} ->
52+
Logger.error("TO0.Hello failed with:",
53+
status_code: status_code,
54+
response_body: body
55+
)
56+
57+
{:error, reason} ->
58+
Logger.error("TO0.Hello HTTP request failed", reason: inspect(reason))
59+
end
60+
end
61+
62+
defp fdo_rendezvous_url! do
63+
Config.fdo_rendezvous_url!()
64+
end
65+
66+
defp http_client do
67+
Application.get_env(:astarte_pairing, :fdo_http_client, HTTPoison)
68+
end
69+
end
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
#
2+
# This file is part of Astarte.
3+
#
4+
# Copyright 2017-2025 SECO Mind Srl
5+
#
6+
# Licensed under the Apache License, Version 2.0 (the "License");
7+
# you may not use this file except in compliance with the License.
8+
# You may obtain a copy of the License at
9+
#
10+
# http://www.apache.org/licenses/LICENSE-2.0
11+
#
12+
# Unless required by applicable law or agreed to in writing, software
13+
# distributed under the License is distributed on an "AS IS" BASIS,
14+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
# See the License for the specific language governing permissions and
16+
# limitations under the License.
17+
#
18+
19+
defmodule Astarte.Pairing.TO0Util do
20+
require Logger
21+
22+
@doc """
23+
Decodes the TO0.HelloAck CBOR body and returns the extracted nonce.
24+
"""
25+
def get_nonce_from_hello_ack(body) do
26+
Logger.info("Decoding TO0.HelloAck CBOR body: #{inspect(body)}")
27+
28+
case CBOR.decode(body) do
29+
{:ok, [%CBOR.Tag{tag: :bytes, value: nonce}], _rest}
30+
when is_binary(nonce) and byte_size(nonce) == 16 ->
31+
{:ok, nonce}
32+
33+
{:ok, [%CBOR.Tag{tag: :bytes, value: nonce}], _rest}
34+
when is_binary(nonce) and byte_size(nonce) != 16 ->
35+
{:error, {:unexpected_binary, nonce}}
36+
37+
{:ok, decoded, _rest} ->
38+
{:error, {:unexpected_cbor_format, decoded}}
39+
40+
{:error, reason} ->
41+
{:error, {:cbor_decode_error, reason}}
42+
end
43+
end
44+

apps/astarte_pairing/mix.exs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ defmodule Astarte.Pairing.Mixfile do
8787
{:phoenix_ecto, "~> 4.0"},
8888
{:phoenix_view, "~> 2.0"},
8989
{:jason, "~> 1.2"},
90+
{:cbor, "~> 1.0"},
9091
{:guardian, "~> 2.3.2"},
9192
{:remote_ip, "~> 1.0"},
9293
{:excoveralls, "~> 0.15", only: :test},
@@ -105,6 +106,7 @@ defmodule Astarte.Pairing.Mixfile do
105106
# See also: https://github.com/deadtrickster/ssl_verify_fun.erl/pull/27
106107
{:ssl_verify_fun, "~> 1.1.0", manager: :rebar3, override: true},
107108
{:cfxxl, github: "ispirata/cfxxl"},
109+
{:httpoison, "~> 1.6"},
108110
{:astarte_data_access, path: astarte_lib("astarte_data_access")},
109111
{:bcrypt_elixir, "~> 2.2"},
110112
{:xandra, "~> 0.19"},

apps/astarte_pairing/mix.lock

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"bcrypt_elixir": {:hex, :bcrypt_elixir, "2.3.1", "5114d780459a04f2b4aeef52307de23de961b69e13a5cd98a911e39fda13f420", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "42182d5f46764def15bf9af83739e3bf4ad22661b1c34fc3e88558efced07279"},
88
"bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"},
99
"castore": {:hex, :castore, "1.0.15", "8aa930c890fe18b6fe0a0cff27b27d0d4d231867897bd23ea772dee561f032a3", [:mix], [], "hexpm", "96ce4c69d7d5d7a0761420ef743e2f4096253931a3ba69e5ff8ef1844fe446d3"},
10+
"cbor": {:hex, :cbor, "1.0.1", "39511158e8ea5a57c1fcb9639aaa7efde67129678fee49ebbda780f6f24959b0", [:mix], [], "hexpm", "5431acbe7a7908f17f6a9cd43311002836a34a8ab01876918d8cfb709cd8b6a2"},
1011
"certifi": {:hex, :certifi, "2.9.0", "6f2a475689dd47f19fb74334859d460a2dc4e3252a3324bd2111b8f0429e7e21", [:rebar3], [], "hexpm", "266da46bdb06d6c6d35fde799bcb28d36d985d424ad7c08b5bb48f5b5cdd4641"},
1112
"cfxxl": {:git, "https://github.com/ispirata/cfxxl.git", "98dc50b1cfe5a682b051b38b83cebc644bc08488", []},
1213
"combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"},
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
defmodule Astarte.Pairing.TO0UtilTest do
2+
use ExUnit.Case, async: true
3+
4+
alias Astarte.Pairing.TO0Util
5+
6+
describe "get_nonce_from_hello_ack/1" do
7+
test "returns nonce for actual FDO HelloAck CBOR payload (binary nonce)" do
8+
valid_nonce = <<32, 54, 127, 243, 66, 48, 228, 115, 59, 186, 230, 246, 198, 179, 113, 78>>
9+
hello_ack_cbor = CBOR.encode([valid_nonce])
10+
assert {:ok, ^valid_nonce} = TO0Util.get_nonce_from_hello_ack(hello_ack_cbor)
11+
end
12+
13+
test "fails with non-CBOR binary" do
14+
invalid_binary_nonce = <<1, 2, 3, 4, 5>>
15+
hello_ack_cbor = CBOR.encode([invalid_binary_nonce])
16+
assert {:error, {:unexpected_binary, _}} = TO0Util.get_nonce_from_hello_ack(hello_ack_cbor)
17+
end
18+
19+
test "fails with CBOR not formatted as FDO expects" do
20+
wrong_struct1 = %{"nonce" => "map instead of list"}
21+
wrong_cbor1 = CBOR.encode(wrong_struct1)
22+
23+
assert {:error, {:unexpected_cbor_format, _}} =
24+
TO0Util.get_nonce_from_hello_ack(wrong_cbor1)
25+
26+
wrong_cbor2 =
27+
CBOR.encode([
28+
<<32, 54, 127, 243, 66, 48, 228, 115, 59, 186, 230, 246, 198, 179, 113, 78>>,
29+
<<32, 54, 127, 243, 66, 48, 228, 115, 59, 186, 230, 246, 198, 179, 113, 78>>
30+
])
31+
32+
assert {:error, {:unexpected_cbor_format, _}} =
33+
TO0Util.get_nonce_from_hello_ack(wrong_cbor2)
34+
end
35+
36+
test "fails with CBOR single binary but wrong length" do
37+
invalid_nonce = <<1, 2, 3, 4, 5, 6, 7, 8>>
38+
hello_ack_cbor = CBOR.encode([invalid_nonce])
39+
40+
assert {:error, {:unexpected_binary, ^invalid_nonce}} =
41+
TO0Util.get_nonce_from_hello_ack(hello_ack_cbor)
42+
end
43+
end
44+
end

0 commit comments

Comments
 (0)