1919defmodule 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
44201end
0 commit comments