···11+ISC License
22+33+Copyright (c) 2025 Thomas Gazagnaire
44+55+Permission to use, copy, modify, and/or distribute this software for any
66+purpose with or without fee is hereby granted, provided that the above
77+copyright notice and this permission notice appear in all copies.
88+99+THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
1010+REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
1111+AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
1212+INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
1313+LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
1414+OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
1515+PERFORMANCE OF THIS SOFTWARE.
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Thomas Gazagnaire. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+open Crowbar
77+88+let () = Mirage_crypto_rng_unix.use_default ()
99+1010+let truncate s =
1111+ if String.length s > 1024 then String.sub s 0 1024 else s
1212+1313+let to_bytes buf =
1414+ let len = String.length buf in
1515+ let b = Bytes.create len in
1616+ Bytes.blit_string buf 0 b 0 len;
1717+ b
1818+1919+(** Security block decode - must not crash. *)
2020+let test_decode buf =
2121+ let buf = truncate buf in
2222+ let reader = Bytesrw.Bytes.Reader.of_string buf in
2323+ let dec = Cbort.Rw.make_decoder reader in
2424+ (try
2525+ let cbor = Cbort.Rw.read_cbor dec in
2626+ let _ = Bpsec.security_block_of_cbor cbor in
2727+ ()
2828+ with _ -> ());
2929+ ()
3030+3131+(** BIB create/verify roundtrip. *)
3232+let test_bib_roundtrip key_seed data =
3333+ if String.length key_seed < 32 then ()
3434+ else
3535+ let key = String.sub key_seed 0 32 in
3636+ let data = truncate data in
3737+ if String.length data = 0 then ()
3838+ else
3939+ let source = Bundle.Dtn_none in
4040+ let targets = [ 1 ] in
4141+ let target_data = [ data ] in
4242+ let bib =
4343+ Bpsec.create_bib ~key ~source ~targets ~target_data ()
4444+ in
4545+ let verified = Bpsec.verify_bib ~key bib ~target_data in
4646+ if not verified then fail "BIB roundtrip failed"
4747+4848+(** BCB create/decrypt roundtrip. *)
4949+let test_bcb_roundtrip key_seed data =
5050+ if String.length key_seed < 32 then ()
5151+ else
5252+ let key = String.sub key_seed 0 32 in
5353+ let data = truncate data in
5454+ if String.length data = 0 then ()
5555+ else
5656+ let source = Bundle.Dtn_none in
5757+ let targets = [ 1 ] in
5858+ let target_data = [ data ] in
5959+ let bcb, encrypted =
6060+ Bpsec.create_bcb ~key ~source ~targets ~target_data ()
6161+ in
6262+ match Bpsec.decrypt_bcb ~key bcb ~ciphertext:encrypted with
6363+ | Some decrypted ->
6464+ if decrypted <> target_data then fail "BCB roundtrip mismatch"
6565+ | None -> fail "BCB decryption failed"
6666+6767+(** CBOR roundtrip for security block. *)
6868+let test_cbor_roundtrip key_seed data =
6969+ if String.length key_seed < 32 then ()
7070+ else
7171+ let key = String.sub key_seed 0 32 in
7272+ let data = truncate data in
7373+ if String.length data = 0 then ()
7474+ else
7575+ let source = Bundle.Dtn_none in
7676+ let targets = [ 1 ] in
7777+ let target_data = [ data ] in
7878+ let bib = Bpsec.create_bib ~key ~source ~targets ~target_data () in
7979+ let cbor = Bpsec.security_block_to_cbor bib in
8080+ match Bpsec.security_block_of_cbor cbor with
8181+ | Ok decoded ->
8282+ if decoded.targets <> targets then fail "targets mismatch"
8383+ | Error _ -> fail "CBOR roundtrip decode failed"
8484+8585+let () =
8686+ add_test ~name:"bpsec: decode no crash" [ bytes ] test_decode;
8787+ add_test ~name:"bpsec: BIB roundtrip" [ bytes; bytes ] test_bib_roundtrip;
8888+ add_test ~name:"bpsec: BCB roundtrip" [ bytes; bytes ] test_bcb_roundtrip;
8989+ add_test ~name:"bpsec: CBOR roundtrip" [ bytes; bytes ] test_cbor_roundtrip
+498
lib/bpsec.ml
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Thomas Gazagnaire. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** Bundle Protocol Security (RFC 9172). *)
77+88+module Cbor = Cbort.Cbor
99+1010+(* {1 Security Context} *)
1111+1212+type security_context_id = int
1313+1414+let bib_hmac_sha2 = 1
1515+let bcb_aes_gcm = 2
1616+1717+(* {1 Security Context Flags} *)
1818+1919+type context_flags = { parameters_present : bool }
2020+2121+let context_flags_default = { parameters_present = false }
2222+2323+let int_of_context_flags f = if f.parameters_present then 1 else 0
2424+let context_flags_of_int n = { parameters_present = n land 1 <> 0 }
2525+2626+(* {1 Security Parameters} *)
2727+2828+type bib_parameter =
2929+ | SHA_variant of int
3030+ | Wrapped_key of string
3131+ | Integrity_scope_flags of int
3232+3333+type bcb_parameter =
3434+ | IV of string
3535+ | AES_variant of int
3636+ | Wrapped_key of string
3737+ | AAD_scope_flags of int
3838+3939+type parameter =
4040+ | Bib_param of bib_parameter
4141+ | Bcb_param of bcb_parameter
4242+ | Unknown_param of int * string
4343+4444+(* {1 Security Results} *)
4545+4646+type bib_result = Expected_hmac of string
4747+type bcb_result = Auth_tag of string
4848+4949+type result =
5050+ | Bib_result of bib_result
5151+ | Bcb_result of bcb_result
5252+ | Unknown_result of int * string
5353+5454+(* {1 Security Blocks} *)
5555+5656+type security_target = int
5757+5858+type security_block = {
5959+ targets : security_target list;
6060+ context_id : security_context_id;
6161+ context_flags : context_flags;
6262+ source : Bundle.eid;
6363+ parameters : parameter list;
6464+ results : result list list;
6565+}
6666+6767+(* {1 Block Types} *)
6868+6969+type bib = security_block
7070+type bcb = security_block
7171+7272+let bib_block_type = 11
7373+let bcb_block_type = 12
7474+7575+(* {1 Errors} *)
7676+7777+type error =
7878+ | Invalid_security_context of int
7979+ | Invalid_parameter of int
8080+ | Invalid_result of int
8181+ | Missing_parameter of string
8282+ | Verification_failed
8383+ | Decryption_failed
8484+ | Cbor_error of string
8585+8686+let pp_error fmt = function
8787+ | Invalid_security_context n ->
8888+ Format.fprintf fmt "invalid security context: %d" n
8989+ | Invalid_parameter n -> Format.fprintf fmt "invalid parameter: %d" n
9090+ | Invalid_result n -> Format.fprintf fmt "invalid result: %d" n
9191+ | Missing_parameter name -> Format.fprintf fmt "missing parameter: %s" name
9292+ | Verification_failed -> Format.fprintf fmt "verification failed"
9393+ | Decryption_failed -> Format.fprintf fmt "decryption failed"
9494+ | Cbor_error msg -> Format.fprintf fmt "CBOR error: %s" msg
9595+9696+(* {1 CBOR Encoding/Decoding} *)
9797+9898+let parameter_to_cbor = function
9999+ | Bib_param (SHA_variant v) ->
100100+ Cbor.Array [ Cbor.int 1; Cbor.int v ]
101101+ | Bib_param (Wrapped_key k) ->
102102+ Cbor.Array [ Cbor.int 2; Cbor.Bytes k ]
103103+ | Bib_param (Integrity_scope_flags f) ->
104104+ Cbor.Array [ Cbor.int 3; Cbor.int f ]
105105+ | Bcb_param (IV iv) ->
106106+ Cbor.Array [ Cbor.int 1; Cbor.Bytes iv ]
107107+ | Bcb_param (AES_variant v) ->
108108+ Cbor.Array [ Cbor.int 2; Cbor.int v ]
109109+ | Bcb_param (Wrapped_key k) ->
110110+ Cbor.Array [ Cbor.int 3; Cbor.Bytes k ]
111111+ | Bcb_param (AAD_scope_flags f) ->
112112+ Cbor.Array [ Cbor.int 4; Cbor.int f ]
113113+ | Unknown_param (id, data) ->
114114+ Cbor.Array [ Cbor.int id; Cbor.Bytes data ]
115115+116116+let result_to_cbor = function
117117+ | Bib_result (Expected_hmac h) ->
118118+ Cbor.Array [ Cbor.int 1; Cbor.Bytes h ]
119119+ | Bcb_result (Auth_tag t) ->
120120+ Cbor.Array [ Cbor.int 1; Cbor.Bytes t ]
121121+ | Unknown_result (id, data) ->
122122+ Cbor.Array [ Cbor.int id; Cbor.Bytes data ]
123123+124124+let security_block_to_cbor sb =
125125+ let targets = Cbor.Array (List.map Cbor.int sb.targets) in
126126+ let context_id = Cbor.int sb.context_id in
127127+ let context_flags = Cbor.int (int_of_context_flags sb.context_flags) in
128128+ let source = Bundle.eid_to_cbor sb.source in
129129+ let parameters =
130130+ if sb.context_flags.parameters_present then
131131+ [ Cbor.Array (List.map parameter_to_cbor sb.parameters) ]
132132+ else []
133133+ in
134134+ let results =
135135+ Cbor.Array
136136+ (List.map
137137+ (fun target_results ->
138138+ Cbor.Array (List.map result_to_cbor target_results))
139139+ sb.results)
140140+ in
141141+ Cbor.Array ([ targets; context_id; context_flags; source ] @ parameters @ [ results ])
142142+143143+let parameter_of_cbor context_id cbor =
144144+ match Cbor.to_array cbor with
145145+ | Some [ id_cbor; value_cbor ] -> (
146146+ match Cbor.to_int64 id_cbor with
147147+ | Some id_int ->
148148+ let id = Int64.to_int id_int in
149149+ if context_id = bib_hmac_sha2 then
150150+ match id with
151151+ | 1 -> (
152152+ match Cbor.to_int64 value_cbor with
153153+ | Some v -> Ok (Bib_param (SHA_variant (Int64.to_int v)))
154154+ | None -> Error (Invalid_parameter id))
155155+ | 2 -> (
156156+ match Cbor.to_bytes value_cbor with
157157+ | Some k -> Ok (Bib_param (Wrapped_key k))
158158+ | None -> Error (Invalid_parameter id))
159159+ | 3 -> (
160160+ match Cbor.to_int64 value_cbor with
161161+ | Some f -> Ok (Bib_param (Integrity_scope_flags (Int64.to_int f)))
162162+ | None -> Error (Invalid_parameter id))
163163+ | _ -> (
164164+ match Cbor.to_bytes value_cbor with
165165+ | Some data -> Ok (Unknown_param (id, data))
166166+ | None -> Error (Invalid_parameter id))
167167+ else if context_id = bcb_aes_gcm then
168168+ match id with
169169+ | 1 -> (
170170+ match Cbor.to_bytes value_cbor with
171171+ | Some iv -> Ok (Bcb_param (IV iv))
172172+ | None -> Error (Invalid_parameter id))
173173+ | 2 -> (
174174+ match Cbor.to_int64 value_cbor with
175175+ | Some v -> Ok (Bcb_param (AES_variant (Int64.to_int v)))
176176+ | None -> Error (Invalid_parameter id))
177177+ | 3 -> (
178178+ match Cbor.to_bytes value_cbor with
179179+ | Some k -> Ok (Bcb_param (Wrapped_key k))
180180+ | None -> Error (Invalid_parameter id))
181181+ | 4 -> (
182182+ match Cbor.to_int64 value_cbor with
183183+ | Some f -> Ok (Bcb_param (AAD_scope_flags (Int64.to_int f)))
184184+ | None -> Error (Invalid_parameter id))
185185+ | _ -> (
186186+ match Cbor.to_bytes value_cbor with
187187+ | Some data -> Ok (Unknown_param (id, data))
188188+ | None -> Error (Invalid_parameter id))
189189+ else
190190+ match Cbor.to_bytes value_cbor with
191191+ | Some data -> Ok (Unknown_param (id, data))
192192+ | None -> Error (Invalid_parameter id)
193193+ | None -> Error (Cbor_error "parameter id must be uint"))
194194+ | _ -> Error (Cbor_error "parameter must be array of 2")
195195+196196+let result_of_cbor context_id cbor =
197197+ match Cbor.to_array cbor with
198198+ | Some [ id_cbor; value_cbor ] -> (
199199+ match Cbor.to_int64 id_cbor with
200200+ | Some id_int ->
201201+ let id = Int64.to_int id_int in
202202+ if context_id = bib_hmac_sha2 then
203203+ match id with
204204+ | 1 -> (
205205+ match Cbor.to_bytes value_cbor with
206206+ | Some h -> Ok (Bib_result (Expected_hmac h))
207207+ | None -> Error (Invalid_result id))
208208+ | _ -> (
209209+ match Cbor.to_bytes value_cbor with
210210+ | Some data -> Ok (Unknown_result (id, data))
211211+ | None -> Error (Invalid_result id))
212212+ else if context_id = bcb_aes_gcm then
213213+ match id with
214214+ | 1 -> (
215215+ match Cbor.to_bytes value_cbor with
216216+ | Some t -> Ok (Bcb_result (Auth_tag t))
217217+ | None -> Error (Invalid_result id))
218218+ | _ -> (
219219+ match Cbor.to_bytes value_cbor with
220220+ | Some data -> Ok (Unknown_result (id, data))
221221+ | None -> Error (Invalid_result id))
222222+ else
223223+ match Cbor.to_bytes value_cbor with
224224+ | Some data -> Ok (Unknown_result (id, data))
225225+ | None -> Error (Invalid_result id)
226226+ | None -> Error (Cbor_error "result id must be uint"))
227227+ | _ -> Error (Cbor_error "result must be array of 2")
228228+229229+let security_block_of_cbor cbor =
230230+ match Cbor.to_array cbor with
231231+ | Some elements when List.length elements >= 5 -> (
232232+ let get i = List.nth elements i in
233233+ match Cbor.to_array (get 0) with
234234+ | Some target_cbors -> (
235235+ let targets =
236236+ List.filter_map
237237+ (fun c ->
238238+ match Cbor.to_int64 c with
239239+ | Some n -> Some (Int64.to_int n)
240240+ | None -> None)
241241+ target_cbors
242242+ in
243243+ match Cbor.to_int64 (get 1) with
244244+ | Some context_id_int -> (
245245+ let context_id = Int64.to_int context_id_int in
246246+ match Cbor.to_int64 (get 2) with
247247+ | Some flags_int -> (
248248+ let context_flags = context_flags_of_int (Int64.to_int flags_int) in
249249+ match Bundle.eid_of_cbor (get 3) with
250250+ | Ok source -> (
251251+ let param_idx, result_idx =
252252+ if context_flags.parameters_present then (4, 5) else (4, 4)
253253+ in
254254+ let parameters =
255255+ if context_flags.parameters_present then
256256+ match Cbor.to_array (get param_idx) with
257257+ | Some param_cbors ->
258258+ List.filter_map
259259+ (fun c ->
260260+ match parameter_of_cbor context_id c with
261261+ | Ok p -> Some p
262262+ | Error _ -> None)
263263+ param_cbors
264264+ | None -> []
265265+ else []
266266+ in
267267+ let result_idx =
268268+ if context_flags.parameters_present then result_idx else param_idx
269269+ in
270270+ match Cbor.to_array (get result_idx) with
271271+ | Some result_arrays ->
272272+ let results =
273273+ List.map
274274+ (fun arr ->
275275+ match Cbor.to_array arr with
276276+ | Some result_cbors ->
277277+ List.filter_map
278278+ (fun c ->
279279+ match result_of_cbor context_id c with
280280+ | Ok r -> Some r
281281+ | Error _ -> None)
282282+ result_cbors
283283+ | None -> [])
284284+ result_arrays
285285+ in
286286+ Ok
287287+ {
288288+ targets;
289289+ context_id;
290290+ context_flags;
291291+ source;
292292+ parameters;
293293+ results;
294294+ }
295295+ | None -> Error (Cbor_error "results must be array"))
296296+ | Error msg -> Error (Cbor_error msg))
297297+ | None -> Error (Cbor_error "flags must be uint"))
298298+ | None -> Error (Cbor_error "context id must be uint"))
299299+ | None -> Error (Cbor_error "targets must be array"))
300300+ | Some _ -> Error (Cbor_error "security block needs at least 5 elements")
301301+ | None -> Error (Cbor_error "security block must be array")
302302+303303+(* {1 Crypto Operations} *)
304304+305305+let compute_hmac ~key ~sha_variant ~scope_flags:_ data =
306306+ let module Hash =
307307+ (val match sha_variant with
308308+ | 5 -> (module Mirage_crypto.Hash.SHA256 : Mirage_crypto.Hash.S)
309309+ | 6 -> (module Mirage_crypto.Hash.SHA384 : Mirage_crypto.Hash.S)
310310+ | 7 -> (module Mirage_crypto.Hash.SHA512 : Mirage_crypto.Hash.S)
311311+ | _ -> (module Mirage_crypto.Hash.SHA256 : Mirage_crypto.Hash.S))
312312+ in
313313+ let key = Cstruct.of_string key in
314314+ let data = Cstruct.of_string data in
315315+ Cstruct.to_string (Mirage_crypto.Hash.mac (module Hash) ~key data)
316316+317317+let create_bib ~key ?(sha_variant = 5) ~source ~targets ~target_data () =
318318+ let scope_flags = 0x07 in (* Include primary block, target header, security header *)
319319+ let parameters =
320320+ [
321321+ Bib_param (SHA_variant sha_variant);
322322+ Bib_param (Integrity_scope_flags scope_flags);
323323+ ]
324324+ in
325325+ let results =
326326+ List.map
327327+ (fun data ->
328328+ let hmac = compute_hmac ~key ~sha_variant ~scope_flags data in
329329+ [ Bib_result (Expected_hmac hmac) ])
330330+ target_data
331331+ in
332332+ {
333333+ targets;
334334+ context_id = bib_hmac_sha2;
335335+ context_flags = { parameters_present = true };
336336+ source;
337337+ parameters;
338338+ results;
339339+ }
340340+341341+let verify_bib ~key bib ~target_data =
342342+ let sha_variant =
343343+ List.find_map
344344+ (function Bib_param (SHA_variant v) -> Some v | _ -> None)
345345+ bib.parameters
346346+ |> Option.value ~default:5
347347+ in
348348+ let scope_flags =
349349+ List.find_map
350350+ (function Bib_param (Integrity_scope_flags f) -> Some f | _ -> None)
351351+ bib.parameters
352352+ |> Option.value ~default:0x07
353353+ in
354354+ List.for_all2
355355+ (fun data results ->
356356+ let expected =
357357+ List.find_map
358358+ (function Bib_result (Expected_hmac h) -> Some h | _ -> None)
359359+ results
360360+ in
361361+ match expected with
362362+ | None -> false
363363+ | Some expected_hmac ->
364364+ let computed = compute_hmac ~key ~sha_variant ~scope_flags data in
365365+ String.equal expected_hmac computed)
366366+ target_data bib.results
367367+368368+let encrypt_aes_gcm ~key ~iv ~aad ~plaintext =
369369+ let key = Mirage_crypto.Cipher_block.AES.GCM.of_secret (Cstruct.of_string key) in
370370+ let iv = Cstruct.of_string iv in
371371+ let adata = Cstruct.of_string aad in
372372+ let result =
373373+ Mirage_crypto.Cipher_block.AES.GCM.authenticate_encrypt ~key ~nonce:iv ~adata
374374+ (Cstruct.of_string plaintext)
375375+ in
376376+ (* GCM appends the tag to the ciphertext *)
377377+ let tag_len = 16 in
378378+ let ciphertext_len = Cstruct.length result - tag_len in
379379+ let ciphertext = Cstruct.to_string (Cstruct.sub result 0 ciphertext_len) in
380380+ let tag = Cstruct.to_string (Cstruct.sub result ciphertext_len tag_len) in
381381+ (ciphertext, tag)
382382+383383+let decrypt_aes_gcm ~key ~iv ~aad ~ciphertext ~tag =
384384+ let key = Mirage_crypto.Cipher_block.AES.GCM.of_secret (Cstruct.of_string key) in
385385+ let iv = Cstruct.of_string iv in
386386+ let adata = Cstruct.of_string aad in
387387+ (* GCM expects ciphertext with tag appended *)
388388+ let data = Cstruct.of_string (ciphertext ^ tag) in
389389+ match
390390+ Mirage_crypto.Cipher_block.AES.GCM.authenticate_decrypt ~key ~nonce:iv ~adata
391391+ data
392392+ with
393393+ | Some plaintext -> Some (Cstruct.to_string plaintext)
394394+ | None -> None
395395+396396+let generate_iv () =
397397+ (* 12 bytes for AES-GCM *)
398398+ Cstruct.to_string (Mirage_crypto_rng.generate 12)
399399+400400+let create_bcb ~key ?(aes_variant = 3) ~source ~targets ~target_data () =
401401+ let iv = generate_iv () in
402402+ let aad_scope_flags = 0x07 in
403403+ let parameters =
404404+ [
405405+ Bcb_param (IV iv);
406406+ Bcb_param (AES_variant aes_variant);
407407+ Bcb_param (AAD_scope_flags aad_scope_flags);
408408+ ]
409409+ in
410410+ let encrypted_data, results =
411411+ List.fold_left_map
412412+ (fun _acc data ->
413413+ let aad = "" in (* Simplified - full impl would include headers *)
414414+ let ciphertext, tag = encrypt_aes_gcm ~key ~iv ~aad ~plaintext:data in
415415+ (ciphertext, [ Bcb_result (Auth_tag tag) ]))
416416+ () target_data
417417+ in
418418+ let _ = encrypted_data in
419419+ let encrypted =
420420+ List.map
421421+ (fun data ->
422422+ let aad = "" in
423423+ let ciphertext, _tag = encrypt_aes_gcm ~key ~iv ~aad ~plaintext:data in
424424+ ciphertext)
425425+ target_data
426426+ in
427427+ ( {
428428+ targets;
429429+ context_id = bcb_aes_gcm;
430430+ context_flags = { parameters_present = true };
431431+ source;
432432+ parameters;
433433+ results;
434434+ },
435435+ encrypted )
436436+437437+let decrypt_bcb ~key bcb ~ciphertext =
438438+ let iv =
439439+ List.find_map
440440+ (function Bcb_param (IV iv) -> Some iv | _ -> None)
441441+ bcb.parameters
442442+ in
443443+ match iv with
444444+ | None -> None
445445+ | Some iv ->
446446+ let aad = "" in
447447+ let decrypted =
448448+ List.map2
449449+ (fun ct results ->
450450+ let tag =
451451+ List.find_map
452452+ (function Bcb_result (Auth_tag t) -> Some t | _ -> None)
453453+ results
454454+ in
455455+ match tag with
456456+ | None -> None
457457+ | Some tag -> decrypt_aes_gcm ~key ~iv ~aad ~ciphertext:ct ~tag)
458458+ ciphertext bcb.results
459459+ in
460460+ if List.for_all Option.is_some decrypted then
461461+ Some (List.filter_map Fun.id decrypted)
462462+ else None
463463+464464+(* {1 Pretty Printing} *)
465465+466466+let pp_parameter fmt = function
467467+ | Bib_param (SHA_variant v) -> Format.fprintf fmt "sha_variant=%d" v
468468+ | Bib_param (Wrapped_key _) -> Format.fprintf fmt "wrapped_key=<bytes>"
469469+ | Bib_param (Integrity_scope_flags f) ->
470470+ Format.fprintf fmt "integrity_scope=0x%x" f
471471+ | Bcb_param (IV _) -> Format.fprintf fmt "iv=<bytes>"
472472+ | Bcb_param (AES_variant v) -> Format.fprintf fmt "aes_variant=%d" v
473473+ | Bcb_param (Wrapped_key _) -> Format.fprintf fmt "wrapped_key=<bytes>"
474474+ | Bcb_param (AAD_scope_flags f) -> Format.fprintf fmt "aad_scope=0x%x" f
475475+ | Unknown_param (id, _) -> Format.fprintf fmt "unknown(%d)=<bytes>" id
476476+477477+let pp_result fmt = function
478478+ | Bib_result (Expected_hmac _) -> Format.fprintf fmt "hmac=<bytes>"
479479+ | Bcb_result (Auth_tag _) -> Format.fprintf fmt "tag=<bytes>"
480480+ | Unknown_result (id, _) -> Format.fprintf fmt "unknown(%d)=<bytes>" id
481481+482482+let pp_security_block fmt sb =
483483+ Format.fprintf fmt
484484+ "@[<v 2>security_block {@ targets=[%a];@ context=%d;@ source=%a;@ \
485485+ parameters=[%a];@ results=[%a]@ }@]"
486486+ (Format.pp_print_list ~pp_sep:(fun fmt () -> Format.fprintf fmt ", ")
487487+ Format.pp_print_int)
488488+ sb.targets sb.context_id Bundle.pp_eid sb.source
489489+ (Format.pp_print_list ~pp_sep:(fun fmt () -> Format.fprintf fmt "; ")
490490+ pp_parameter)
491491+ sb.parameters
492492+ (Format.pp_print_list ~pp_sep:(fun fmt () -> Format.fprintf fmt "; ")
493493+ (Format.pp_print_list ~pp_sep:(fun fmt () -> Format.fprintf fmt ", ")
494494+ pp_result))
495495+ sb.results
496496+497497+let pp_bib = pp_security_block
498498+let pp_bcb = pp_security_block
+198
lib/bpsec.mli
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Thomas Gazagnaire. All rights reserved.
33+ SPDX-License-Identifier: ISC
44+ ---------------------------------------------------------------------------*)
55+66+(** Bundle Protocol Security (RFC 9172).
77+88+ This module implements BPSec providing integrity and confidentiality
99+ services for Bundle Protocol v7 bundles.
1010+1111+ {b Security blocks}
1212+1313+ BPSec defines two extension block types:
1414+ - {b Block Integrity Block (BIB)} - provides integrity protection using
1515+ HMAC or digital signatures
1616+ - {b Block Confidentiality Block (BCB)} - provides confidentiality using
1717+ authenticated encryption (AES-GCM)
1818+1919+ {b Security contexts}
2020+2121+ A security context defines the cryptographic algorithms and parameters
2222+ used. RFC 9173 defines the default context using:
2323+ - HMAC-SHA256 for integrity (BIB)
2424+ - AES-GCM with 256-bit keys for confidentiality (BCB)
2525+2626+ {b References}
2727+ - {{:https://datatracker.ietf.org/doc/html/rfc9172}RFC 9172} - Bundle
2828+ Protocol Security (BPSec)
2929+ - {{:https://datatracker.ietf.org/doc/html/rfc9173}RFC 9173} - Default
3030+ Security Contexts for BPSec *)
3131+3232+(** {1 Security Context} *)
3333+3434+type security_context_id = int
3535+(** Security context identifier. Standard contexts:
3636+ - 1 = BIB-HMAC-SHA2 (RFC 9173)
3737+ - 2 = BCB-AES-GCM (RFC 9173) *)
3838+3939+val bib_hmac_sha2 : security_context_id
4040+(** BIB-HMAC-SHA2 security context (RFC 9173 Section 3). *)
4141+4242+val bcb_aes_gcm : security_context_id
4343+(** BCB-AES-GCM security context (RFC 9173 Section 4). *)
4444+4545+(** {1 Security Context Flags} *)
4646+4747+type context_flags = {
4848+ parameters_present : bool; (** Security context parameters are present. *)
4949+}
5050+(** Security context flags (RFC 9172 Section 3.6). *)
5151+5252+val context_flags_default : context_flags
5353+5454+(** {1 Security Parameters} *)
5555+5656+(** Security context parameter for BIB-HMAC-SHA2 (RFC 9173 Section 3.3). *)
5757+type bib_parameter =
5858+ | SHA_variant of int (** id=1: SHA variant (5=SHA-256, 6=SHA-384, 7=SHA-512) *)
5959+ | Wrapped_key of string (** id=2: Wrapped key bytes *)
6060+ | Integrity_scope_flags of int (** id=3: Scope flags *)
6161+6262+(** Security context parameter for BCB-AES-GCM (RFC 9173 Section 4.3). *)
6363+type bcb_parameter =
6464+ | IV of string (** id=1: Initialization vector (12 bytes for AES-GCM) *)
6565+ | AES_variant of int (** id=2: AES variant (1=A128GCM, 3=A256GCM) *)
6666+ | Wrapped_key of string (** id=3: Wrapped key bytes *)
6767+ | AAD_scope_flags of int (** id=4: AAD scope flags *)
6868+6969+type parameter =
7070+ | Bib_param of bib_parameter
7171+ | Bcb_param of bcb_parameter
7272+ | Unknown_param of int * string
7373+7474+(** {1 Security Results} *)
7575+7676+(** Security result for BIB (integrity tag/signature). *)
7777+type bib_result = Expected_hmac of string (** id=1: Expected HMAC value *)
7878+7979+(** Security result for BCB (authentication tag). *)
8080+type bcb_result = Auth_tag of string (** id=1: Authentication tag *)
8181+8282+type result =
8383+ | Bib_result of bib_result
8484+ | Bcb_result of bcb_result
8585+ | Unknown_result of int * string
8686+8787+(** {1 Security Blocks} *)
8888+8989+type security_target = int
9090+(** Block number of the target block (RFC 9172 Section 3.3). *)
9191+9292+type security_block = {
9393+ targets : security_target list;
9494+ (** Block numbers of protected blocks. *)
9595+ context_id : security_context_id; (** Security context identifier. *)
9696+ context_flags : context_flags; (** Security context flags. *)
9797+ source : Bundle.eid; (** Security source (BPA that added this block). *)
9898+ parameters : parameter list; (** Per-target security parameters. *)
9999+ results : result list list;
100100+ (** Per-target security results (one list per target). *)
101101+}
102102+(** Abstract security block (BIB or BCB share this structure). *)
103103+104104+(** {1 Block Integrity Block (BIB)} *)
105105+106106+type bib = security_block
107107+(** Block Integrity Block - provides integrity services. *)
108108+109109+val bib_block_type : int
110110+(** BIB block type number (11). *)
111111+112112+(** {1 Block Confidentiality Block (BCB)} *)
113113+114114+type bcb = security_block
115115+(** Block Confidentiality Block - provides confidentiality services. *)
116116+117117+val bcb_block_type : int
118118+(** BCB block type number (12). *)
119119+120120+(** {1 Errors} *)
121121+122122+type error =
123123+ | Invalid_security_context of int
124124+ | Invalid_parameter of int
125125+ | Invalid_result of int
126126+ | Missing_parameter of string
127127+ | Verification_failed
128128+ | Decryption_failed
129129+ | Cbor_error of string
130130+131131+val pp_error : error Fmt.t
132132+133133+(** {1 CBOR Encoding/Decoding} *)
134134+135135+val security_block_to_cbor : security_block -> Cbort.Cbor.t
136136+(** [security_block_to_cbor sb] encodes a security block as CBOR. *)
137137+138138+val security_block_of_cbor : Cbort.Cbor.t -> (security_block, error) result
139139+(** [security_block_of_cbor cbor] decodes a security block from CBOR. *)
140140+141141+(** {1 Integrity Operations (BIB)} *)
142142+143143+val compute_hmac :
144144+ key:string -> sha_variant:int -> scope_flags:int -> string -> string
145145+(** [compute_hmac ~key ~sha_variant ~scope_flags data] computes HMAC over data.
146146+ @param sha_variant 5=SHA-256, 6=SHA-384, 7=SHA-512 *)
147147+148148+val create_bib :
149149+ key:string ->
150150+ ?sha_variant:int ->
151151+ source:Bundle.eid ->
152152+ targets:security_target list ->
153153+ target_data:string list ->
154154+ unit ->
155155+ bib
156156+(** [create_bib ~key ~source ~targets ~target_data ()] creates a BIB protecting
157157+ the specified target blocks. *)
158158+159159+val verify_bib : key:string -> bib -> target_data:string list -> bool
160160+(** [verify_bib ~key bib ~target_data] verifies the integrity of target blocks.
161161+*)
162162+163163+(** {1 Confidentiality Operations (BCB)} *)
164164+165165+val encrypt_aes_gcm :
166166+ key:string -> iv:string -> aad:string -> plaintext:string -> string * string
167167+(** [encrypt_aes_gcm ~key ~iv ~aad ~plaintext] returns [(ciphertext, tag)]. *)
168168+169169+val decrypt_aes_gcm :
170170+ key:string ->
171171+ iv:string ->
172172+ aad:string ->
173173+ ciphertext:string ->
174174+ tag:string ->
175175+ string option
176176+(** [decrypt_aes_gcm ~key ~iv ~aad ~ciphertext ~tag] returns [Some plaintext] on
177177+ success. *)
178178+179179+val create_bcb :
180180+ key:string ->
181181+ ?aes_variant:int ->
182182+ source:Bundle.eid ->
183183+ targets:security_target list ->
184184+ target_data:string list ->
185185+ unit ->
186186+ bcb * string list
187187+(** [create_bcb ~key ~source ~targets ~target_data ()] creates a BCB and returns
188188+ the encrypted target data. *)
189189+190190+val decrypt_bcb :
191191+ key:string -> bcb -> ciphertext:string list -> string list option
192192+(** [decrypt_bcb ~key bcb ~ciphertext] decrypts the protected blocks. *)
193193+194194+(** {1 Pretty Printing} *)
195195+196196+val pp_security_block : security_block Fmt.t
197197+val pp_bib : bib Fmt.t
198198+val pp_bcb : bcb Fmt.t