Skip to content

Commit caa2c67

Browse files
committed
feat(pairing): expose ownership API
expose ownserhip api, save voucher and key into the db (with ttl) link the related device to the voucher Signed-off-by: Eddy Babetto <eddy.babetto@secomind.com>
1 parent 7f1de2d commit caa2c67

File tree

6 files changed

+145
-1
lines changed

6 files changed

+145
-1
lines changed
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
#
2+
# This file is part of Astarte.
3+
#
4+
# Copyright 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.FDO.VoucherManager do
20+
alias Astarte.Pairing.Queries
21+
require Logger
22+
23+
def save_voucher(realm_name, voucher_blob, pkey_blob) do
24+
with {:ok, inserted_voucher} <-
25+
Queries.create_ownership_voucher(realm_name, voucher_blob, pkey_blob),
26+
device_guuid <- extract_device_guuid_from_voucher_data(voucher_blob),
27+
{:ok, _} <-
28+
Queries.update_device_registration_voucher(
29+
realm_name,
30+
device_guuid,
31+
inserted_voucher.id
32+
) do
33+
:ok
34+
end
35+
end
36+
37+
defp extract_device_guuid_from_voucher_data(voucher_blob) do
38+
with {:ok, cbor_data} <- clean_and_b64_decode_voucher_blob(voucher_blob),
39+
{:ok, decoded_voucher, _rest} <- cbor_data |> CBOR.decode(),
40+
{:ok, decoded_header, _rest} <- extract_and_decode_voucher_header(decoded_voucher),
41+
device_guid_binary <- device_id_from_header(decoded_header) do
42+
UUID.binary_to_string!(device_guid_binary)
43+
else
44+
error ->
45+
"error extracting device guid from ownership voucher: #{inspect(error)}"
46+
|> Logger.error()
47+
48+
error
49+
end
50+
end
51+
52+
defp clean_and_b64_decode_voucher_blob(voucher_blob) do
53+
ov_data =
54+
voucher_blob
55+
|> String.replace("-----BEGIN OWNERSHIP VOUCHER-----", "")
56+
|> String.replace("-----END OWNERSHIP VOUCHER-----", "")
57+
|> String.replace(~r/\s/, "")
58+
59+
with {:ok, cbor_data} <- Base.decode64(ov_data) do
60+
{:ok, cbor_data}
61+
end
62+
end
63+
64+
defp extract_and_decode_voucher_header(decoded_voucher) do
65+
decoded_voucher |> Enum.at(1) |> Map.fetch!(:value) |> CBOR.decode()
66+
end
67+
68+
defp device_id_from_header(decoded_header) do
69+
decoded_header
70+
|> Enum.at(1)
71+
|> Map.fetch!(:value)
72+
end
73+
end

apps/astarte_pairing/lib/astarte_pairing/queries.ex

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ defmodule Astarte.Pairing.Queries do
2828
alias Astarte.DataAccess.KvStore
2929
alias Astarte.DataAccess.Realms.Realm
3030
alias Astarte.DataAccess.Repo
31+
alias Astarte.DataAccess.FDO.OwnershipVoucher
3132

3233
require Logger
3334

@@ -223,6 +224,33 @@ defmodule Astarte.Pairing.Queries do
223224
{:ok, count}
224225
end
225226

227+
def create_ownership_voucher(realm_name, voucher_blob, pkey_blob) do
228+
keyspace_name = Realm.keyspace_name(realm_name)
229+
opts = [prefix: keyspace_name, consistency: Consistency.device_info(:write)]
230+
231+
timestamp_ms =
232+
DateTime.utc_now()
233+
|> DateTime.add(7, :day)
234+
|> DateTime.to_unix(:millisecond)
235+
236+
%OwnershipVoucher{
237+
voucher_data: voucher_blob,
238+
private_key: pkey_blob,
239+
expiry: timestamp_ms
240+
}
241+
|> Repo.insert(opts)
242+
end
243+
244+
def update_device_registration_voucher(realm_name, device_id, voucher_id) do
245+
keyspace_name = Realm.keyspace_name(realm_name)
246+
247+
%Device{
248+
device_id: device_id,
249+
ownership_voucher: voucher_id
250+
}
251+
|> Repo.insert(prefix: keyspace_name, consistency: Consistency.device_info(:write))
252+
end
253+
226254
defp do_register_device(
227255
realm_name,
228256
device_id,
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
#
2+
# This file is part of Astarte.
3+
#
4+
# Copyright 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.PairingWeb.OwnershipVoucherController do
20+
use Astarte.PairingWeb, :controller
21+
22+
alias Astarte.Pairing.FDO.VoucherManager
23+
24+
action_fallback Astarte.PairingWeb.FallbackController
25+
26+
def create(conn, %{
27+
"ownership_voucher" => voucher,
28+
"private_key" => key,
29+
"realm_name" => realm_name
30+
}) do
31+
with :ok <- VoucherManager.save_voucher(realm_name, voucher, key) do
32+
conn
33+
|> send_resp(200, "")
34+
end
35+
end
36+
end

apps/astarte_pairing/lib/astarte_pairing_web/router.ex

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@ defmodule Astarte.PairingWeb.Router do
3838

3939
get "/version", VersionController, :show
4040

41+
scope "/ownership-voucher" do
42+
pipe_through :agent_api
43+
post "/", OwnershipVoucherController, :create
44+
end
45+
4146
scope "/agent" do
4247
pipe_through :agent_api
4348

apps/astarte_pairing/mix.exs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,8 @@ defmodule Astarte.Pairing.Mixfile do
119119
{:mimic, "~> 1.11", only: :test},
120120
{:credo, "~> 1.7", only: [:dev, :test], runtime: false},
121121
{:con_cache, "~> 1.1"},
122-
{:astarte_events, path: astarte_lib("astarte_events")}
122+
{:astarte_events, path: astarte_lib("astarte_events")},
123+
{:cbor, "~> 1.0"}
123124
]
124125
end
125126

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"},

0 commit comments

Comments
 (0)