Skip to content

Commit 3d49989

Browse files
committed
feat: TO0.OwnerSign implementation
-Implemented logic to build and send the TO0.OwnerSign message after recieving the HelloAck from the Rendezvous. -Added documentation on testing the TO0 protocol with the current draft setup. -Added tests for the new TO0.OwnerSign functionality. Signed-off-by: Riccardo Nalgi <riccardo.nalgi@secomind.com>
1 parent 2cdad4b commit 3d49989

4 files changed

Lines changed: 438 additions & 7 deletions

File tree

apps/astarte_pairing/lib/astarte_pairing/fdo_client.ex

Lines changed: 63 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,18 @@ defmodule Astarte.Pairing.FDOClient do
2323
require Logger
2424

2525
@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
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.
2929
"""
3030
def to0_hello() do
3131
url = "#{fdo_rendezvous_url!()}/fdo/101/msg/20"
32-
headers = [{"Content-Type", "application/cbor"}, {"Content-Length", "0"}]
32+
33+
headers = [
34+
{"Content-Type", "application/cbor"},
35+
{"Content-Length", "0"}
36+
]
37+
3338
request_body = CBOR.encode([])
3439

3540
Logger.debug("Sending TO0.Hello to FDO rendezvous server...", url: url)
@@ -41,8 +46,8 @@ defmodule Astarte.Pairing.FDOClient do
4146

4247
case TO0Util.get_nonce_from_hello_ack(body) do
4348
{:ok, nonce} ->
44-
# TODO: to0owner_sign
45-
{:ok, nonce}
49+
Logger.debug("Nonce retrieved from TO0.HelloAck", nonce: Base.encode16(nonce))
50+
to0_owner_sign(nonce, get_auth_bearer(headers))
4651

4752
{:error, reason} ->
4853
Logger.error("Failed to get nonce from TO0.HelloAck", reason: reason)
@@ -59,6 +64,58 @@ defmodule Astarte.Pairing.FDOClient do
5964
end
6065
end
6166

67+
defp get_auth_bearer(headers) do
68+
Enum.find_value(headers, fn
69+
{"authorization", value} ->
70+
Logger.debug("Authorization Bearer retrieved: #{inspect(value)}")
71+
value
72+
73+
_ ->
74+
nil
75+
end)
76+
end
77+
78+
@doc """
79+
TO0.OwnerSign - Type 22 message to register ownership.
80+
Sends ownership voucher and waits for response from rendezvous server.
81+
Returns decoded TO0.AcceptOwner (message 23) with negotiated wait time.
82+
"""
83+
def to0_owner_sign(nonce, bearer) do
84+
Logger.debug("Sending TO0.OwnerSign...")
85+
86+
with {:ok, owner_key} <- TO0Util.get_owner_private_key(),
87+
{:ok, ownership_voucher} <- TO0Util.get_ownership_voucher(),
88+
{:ok, addr_entries} <- TO0Util.get_astarte_rv_to2_addr_entries(),
89+
{:ok, request_body} <-
90+
TO0Util.build_owner_sign_message(ownership_voucher, owner_key, nonce, addr_entries) do
91+
url = "#{fdo_rendezvous_url!()}/fdo/101/msg/22"
92+
93+
headers = [
94+
{"Content-Type", "application/cbor"},
95+
{"Content-Length", byte_size(request_body)},
96+
{"Authorization", bearer}
97+
]
98+
99+
Logger.debug("Sending TO0.OwnerSign", payload_size: byte_size(request_body))
100+
101+
case http_client().post(url, request_body, headers) do
102+
{:ok, %{status_code: 200, body: _body}} ->
103+
Logger.debug("TO0.OwnerSign completed successfully!")
104+
105+
{:ok, %{status_code: status_code, body: body}} ->
106+
Logger.error(
107+
"TO0.OwnerSign failed with status #{status_code}, response: #{inspect(body)}"
108+
)
109+
110+
{:error, reason} ->
111+
Logger.error("TO0.OwnerSign request failed", reason: reason)
112+
end
113+
else
114+
{:error, reason} ->
115+
Logger.error("Failed to prepare TO0.OwnerSign message", reason: reason)
116+
end
117+
end
118+
62119
defp fdo_rendezvous_url! do
63120
Config.fdo_rendezvous_url!()
64121
end
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
defmodule Astarte.Pairing.MockFDOApi do
2+
require Logger
3+
4+
def get_ownership_voucher do
5+
ownership_voucher = build_mock_ownership_voucher()
6+
{:ok, ownership_voucher}
7+
end
8+
9+
def get_owner_private_key do
10+
owner_key = build_mock_owner_key()
11+
{:ok, owner_key}
12+
end
13+
14+
defp build_mock_ownership_voucher do
15+
"""
16+
-----BEGIN OWNERSHIP VOUCHER-----
17+
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
52+
uliN+61J1dB0nA==
53+
-----END OWNERSHIP VOUCHER-----
54+
"""
55+
end
56+
57+
defp build_mock_owner_key do
58+
"""
59+
-----BEGIN EC PRIVATE KEY-----
60+
MHcCAQEEIFlbTEE1Ce+RSqhU8FqxsY7eNb9BaBWOTw6qFv7l0DZtoAoGCCqGSM49
61+
AwEHoUQDQgAEocPEIHIrn08VRO5zkkDztwp72Sw0BSm0mZeLgOKkHLUPdVFFlc0E
62+
O82b1/S2Cwzwh8MIDDx0CN2b+IBl5bRwOw==
63+
-----END EC PRIVATE KEY-----
64+
"""
65+
end
66+
end

apps/astarte_pairing/lib/astarte_pairing/to0_util.ex

Lines changed: 158 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,13 @@
1919
defmodule Astarte.Pairing.TO0Util do
2020
require Logger
2121

22+
alias Astarte.Pairing.MockFDOApi
23+
2224
@doc """
2325
Decodes the TO0.HelloAck CBOR body and returns the extracted nonce.
2426
"""
2527
def get_nonce_from_hello_ack(body) do
26-
Logger.info("Decoding TO0.HelloAck CBOR body: #{inspect(body)}")
28+
Logger.debug("Decoding TO0.HelloAck CBOR body: #{inspect(body)}")
2729

2830
case CBOR.decode(body) do
2931
{:ok, [%CBOR.Tag{tag: :bytes, value: nonce}], _rest}
@@ -41,4 +43,159 @@ defmodule Astarte.Pairing.TO0Util do
4143
{:error, {:cbor_decode_error, reason}}
4244
end
4345
end
46+
47+
def get_ownership_voucher() do
48+
MockFDOApi.get_ownership_voucher()
49+
end
50+
51+
def decode_ownership_voucher(voucher_pem) do
52+
ov_data =
53+
voucher_pem
54+
|> String.replace("-----BEGIN OWNERSHIP VOUCHER-----", "")
55+
|> String.replace("-----END OWNERSHIP VOUCHER-----", "")
56+
|> String.replace(~r/\s/, "")
57+
58+
with {:ok, cbor_data} <- Base.decode64(ov_data),
59+
{:ok, result, _rest} <- CBOR.decode(cbor_data) do
60+
{:ok, result}
61+
else
62+
{:error, reason} -> {:error, reason}
63+
other -> {:error, "PEM parsing failed: #{inspect(other)}"}
64+
end
65+
end
66+
67+
def get_owner_private_key() do
68+
MockFDOApi.get_owner_private_key()
69+
end
70+
71+
def sign_with_owner_key(data, owner_key) do
72+
case :public_key.pem_decode(owner_key) do
73+
[{:ECPrivateKey, der_data, :not_encrypted}] ->
74+
with {:ok, ec_private_key} <- safe_der_decode(der_data),
75+
{:ok, raw_priv} <- extract_raw_private_key(ec_private_key),
76+
{:ok, signature} <- safe_sign(data, raw_priv) do
77+
{:ok, signature}
78+
else
79+
{:error, reason} ->
80+
Logger.error("Signing failed", error: inspect(reason))
81+
{:error, reason}
82+
end
83+
84+
_ ->
85+
Logger.error("PEM decode failed for owner key")
86+
{:error, :pem_decode_failed}
87+
end
88+
end
89+
90+
def safe_der_decode(der_data) do
91+
try do
92+
{:ok, :public_key.der_decode(:ECPrivateKey, der_data)}
93+
rescue
94+
e -> {:error, {:der_decode_failed, e}}
95+
end
96+
end
97+
98+
defp extract_raw_private_key({:ECPrivateKey, _version, bin, _params, _pub, _extra}) do
99+
case byte_size(bin) do
100+
32 -> {:ok, bin}
101+
n when n < 32 -> {:ok, :binary.copy(<<0>>, 32 - n) <> bin}
102+
n -> {:error, {:key_too_long, n}}
103+
end
104+
end
105+
106+
def safe_sign(data, priv_key) do
107+
# ECDSA with SHA-256 requires a 32-byte private key secp256r1
108+
if is_binary(priv_key) and byte_size(priv_key) == 32 do
109+
try do
110+
{:ok, :crypto.sign(:ecdsa, :sha256, data, [priv_key, :secp256r1])}
111+
rescue
112+
e -> {:error, {:signing_failed, e}}
113+
end
114+
else
115+
{:error, :invalid_private_key_format}
116+
end
117+
end
118+
119+
def build_rv_to2_addr_entry(ip, dns, port, protocol) do
120+
rv_entry = [ip, dns, port, protocol]
121+
encoded_entry = CBOR.encode([rv_entry])
122+
{:ok, encoded_entry}
123+
end
124+
125+
defp build_to1d_rv(entries) do
126+
to1d_rv = CBOR.encode([entries])
127+
{:ok, to1d_rv}
128+
end
129+
130+
defp build_to0d(ov, wait_seconds, nonce) do
131+
to0d = CBOR.encode([ov, wait_seconds, nonce])
132+
{:ok, to0d}
133+
end
134+
135+
defp add_cbor_tag(payload) do
136+
%CBOR.Tag{tag: :bytes, value: payload}
137+
end
138+
139+
defp build_to1d_to0d_hash(to0d) do
140+
to1d_to0d_hash_value = :crypto.hash(:sha256, to0d)
141+
# 47 is the key for SHA-256
142+
to1d_to0d_hash = CBOR.encode([47, to1d_to0d_hash_value])
143+
{:ok, to1d_to0d_hash}
144+
end
145+
146+
defp build_to1d_blob_payload(to1d_rv, to1d_to0d_hash) do
147+
blob_payload = CBOR.encode([to1d_rv, to1d_to0d_hash])
148+
{:ok, blob_payload}
149+
end
150+
151+
def build_cose_sign1(payload, owner_key) do
152+
# Protected header: ES256 algorithm (ECDSA with SHA-256)
153+
# -7 is ES256
154+
protected_header = %{1 => -7}
155+
protected_header_cbor = CBOR.encode(protected_header)
156+
157+
sig_structure = ["Signature1", protected_header_cbor, <<>>, payload]
158+
sig_structure_cbor = CBOR.encode(sig_structure)
159+
160+
with {:ok, raw_signature} <- sign_with_owner_key(sig_structure_cbor, owner_key) do
161+
cose_sign1_array = [
162+
add_cbor_tag(protected_header_cbor),
163+
%{},
164+
add_cbor_tag(payload),
165+
add_cbor_tag(raw_signature)
166+
]
167+
168+
# Tag 18 is associated with COSE_Sign1
169+
cose_sign1 = %CBOR.Tag{tag: 18, value: cose_sign1_array}
170+
{:ok, cose_sign1}
171+
else
172+
{:error, reason} -> {:error, {:signing_error, reason}}
173+
end
174+
end
175+
176+
def get_astarte_rv_to2_addr_entries() do
177+
with {:ok, rv_entry1} <- build_rv_to2_addr_entry(CBOR.encode([]), "pippo", 8080, 3),
178+
{:ok, rv_entry2} <- build_rv_to2_addr_entry(CBOR.encode([]), "paperino", 8080, 3) do
179+
{:ok, [rv_entry1, rv_entry2]}
180+
else
181+
{:error, reason} -> {:error, reason}
182+
end
183+
end
184+
185+
def build_owner_sign_message(ownership_voucher, owner_key, nonce, addr_entries) do
186+
with {:ok, decoded_ownership_voucher} <- decode_ownership_voucher(ownership_voucher),
187+
{:ok, to0d} <- build_to0d(decoded_ownership_voucher, 3600, nonce),
188+
{:ok, to1d_to0d_hash} <- build_to1d_to0d_hash(to0d),
189+
{:ok, to1d_rv} <- build_to1d_rv(addr_entries),
190+
{:ok, blob_payload} <- build_to1d_blob_payload(to1d_rv, to1d_to0d_hash),
191+
{:ok, signature} <- build_cose_sign1(blob_payload, owner_key) do
192+
result = CBOR.encode([add_cbor_tag(to0d), signature])
193+
Logger.debug("build_owner_sign_message success: #{inspect(result)}")
194+
{:ok, result}
195+
else
196+
error ->
197+
Logger.debug("build_owner_sign_message error: #{inspect(error)}")
198+
{:error, error}
199+
end
200+
end
44201
end

0 commit comments

Comments
 (0)