CCSDS Space Data Link Security (355.0-B-2)
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add comprehensive SDLS tests: SA lifecycle, crypto, keystore, security log

141 new tests (192 total, was 51). Adapted from borealis test suite.

- test_crypto: 25 tests — AES-CCM/GCM roundtrips, NIST SP 800-38C/38D
known answer tests, nonce validation, AAD mismatch, tag truncation
- test_sa: 53 tests — SA constructors, IV management/wraparound,
anti-replay window (accept/reject/wraparound/disabled), SA lifecycle
(start/stop/rekey/expire), PDU wire format roundtrips, config validation
- test_keystore: 29 tests — CRUD, key lifecycle state machine, SDLS
integration (enc/auth/dec/verify access policies), serialization,
cipher-specific key length validation
- test_key: 21 tests — Key state machine, lifecycle transitions,
invalid transitions, Keyring operations
- test_security: 23 tests — Event encoding roundtrips, store operations,
alarm flags, event tags, pretty-printing

Also exports SA PDU command types in sa.mli that were previously hidden.

+2012
+90
lib/sa.mli
··· 250 250 251 251 val keyed : 252 252 spi:int -> scid:int -> vcid:int -> ek_id:Keyid.t -> ak_id:Keyid.t -> entry 253 + 254 + (** {1 SA PDU Wire Formats (SDLS-EP)} *) 255 + 256 + type create_cmd = { 257 + spi : int; 258 + scid : int; 259 + vcid : int; 260 + ecs : ecs option; 261 + acs : acs option; 262 + iv_len : int; 263 + mac_len : int; 264 + sn_len : int; 265 + arsnw : int; 266 + abm : abm; 267 + } 268 + 269 + val pp_create_cmd : create_cmd Fmt.t 270 + val write_create_cmd : Binary.Writer.t -> create_cmd -> unit 271 + 272 + val read_create_cmd : 273 + Binary.Reader.t -> (create_cmd, [> `Truncated | `Invalid ]) result 274 + 275 + type delete_cmd = { spi : int } 276 + 277 + val pp_delete_cmd : delete_cmd Fmt.t 278 + val write_delete_cmd : Binary.Writer.t -> delete_cmd -> unit 279 + 280 + val read_delete_cmd : 281 + Binary.Reader.t -> (delete_cmd, [> `Truncated | `Invalid ]) result 282 + 283 + type set_arsn_cmd = { spi : int; arsn : bytes } 284 + 285 + val pp_set_arsn_cmd : set_arsn_cmd Fmt.t 286 + val write_set_arsn_cmd : Binary.Writer.t -> set_arsn_cmd -> unit 287 + 288 + val read_set_arsn_cmd : 289 + sn_len:int -> 290 + Binary.Reader.t -> 291 + (set_arsn_cmd, [> `Truncated | `Invalid ]) result 292 + 293 + type set_arsnw_cmd = { spi : int; arsnw : int } 294 + 295 + val pp_set_arsnw_cmd : set_arsnw_cmd Fmt.t 296 + val write_set_arsnw_cmd : Binary.Writer.t -> set_arsnw_cmd -> unit 297 + 298 + val read_set_arsnw_cmd : 299 + Binary.Reader.t -> (set_arsnw_cmd, [> `Truncated | `Invalid ]) result 300 + 301 + type rekey_cmd = { spi : int; ekid : Keyid.t } 302 + 303 + val pp_rekey_cmd : rekey_cmd Fmt.t 304 + val write_rekey_cmd : Binary.Writer.t -> rekey_cmd -> unit 305 + 306 + val read_rekey_cmd : 307 + Binary.Reader.t -> (rekey_cmd, [> `Truncated | `Invalid ]) result 308 + 309 + type expire_cmd = { spi : int } 310 + 311 + val pp_expire_cmd : expire_cmd Fmt.t 312 + val write_expire_cmd : Binary.Writer.t -> expire_cmd -> unit 313 + 314 + val read_expire_cmd : 315 + Binary.Reader.t -> (expire_cmd, [> `Truncated | `Invalid ]) result 316 + 317 + type status_reply = { 318 + spi : int; 319 + state : state; 320 + ecs : ecs option; 321 + acs : acs option; 322 + iv_len : int; 323 + mac_len : int; 324 + sn_len : int; 325 + arsnw : int; 326 + } 327 + 328 + val pp_status_reply : status_reply Fmt.t 329 + val write_status_reply : Binary.Writer.t -> status_reply -> unit 330 + 331 + val read_status_reply : 332 + Binary.Reader.t -> (status_reply, [> `Truncated | `Invalid ]) result 333 + 334 + type read_arsn_reply = { spi : int; arsn : bytes } 335 + 336 + val pp_read_arsn_reply : read_arsn_reply Fmt.t 337 + val write_read_arsn_reply : Binary.Writer.t -> read_arsn_reply -> unit 338 + 339 + val read_read_arsn_reply : 340 + sn_len:int -> 341 + Binary.Reader.t -> 342 + (read_arsn_reply, [> `Truncated | `Invalid ]) result
+290
test/test_key.ml
··· 1 + (** Unit tests for Key module (key lifecycle state machine). *) 2 + 3 + open Sdls 4 + 5 + let key_state = 6 + Alcotest.testable Key.pp_state (fun a b -> 7 + Key.int_of_state a = Key.int_of_state b) 8 + 9 + let key_error = Alcotest.testable Key.pp_error ( = ) 10 + let _ = key_error 11 + 12 + (* {1 State Encoding} *) 13 + 14 + let test_state_roundtrip () = 15 + let states = 16 + [ Key.Empty; Key.Pending; Key.Active; Key.Deprecated; Key.Zeroized ] 17 + in 18 + List.iter 19 + (fun s -> 20 + let n = Key.int_of_state s in 21 + match Key.state_of_int n with 22 + | Some s' -> Alcotest.(check key_state) "roundtrip" s s' 23 + | None -> Alcotest.fail (Fmt.str "state_of_int %d failed" n)) 24 + states 25 + 26 + let test_state_values () = 27 + (* Match wire encoding *) 28 + Alcotest.(check int) "empty" 0 (Key.int_of_state Key.Empty); 29 + Alcotest.(check int) "pending" 1 (Key.int_of_state Key.Pending); 30 + Alcotest.(check int) "active" 2 (Key.int_of_state Key.Active); 31 + Alcotest.(check int) "deprecated" 3 (Key.int_of_state Key.Deprecated); 32 + Alcotest.(check int) "zeroized" 4 (Key.int_of_state Key.Zeroized) 33 + 34 + let test_state_invalid () = 35 + Alcotest.(check (option key_state)) "invalid 5" None (Key.state_of_int 5); 36 + Alcotest.(check (option key_state)) "invalid 255" None (Key.state_of_int 255); 37 + Alcotest.(check (option key_state)) "invalid -1" None (Key.state_of_int (-1)) 38 + 39 + (* {1 Key Construction} *) 40 + 41 + let test_empty_key () = 42 + let k = Key.empty ~kid:0x0001 ~algorithm:1 in 43 + Alcotest.(check int) "kid" 0x0001 (Key.kid k); 44 + Alcotest.(check key_state) "state" Key.Empty (Key.state k); 45 + Alcotest.(check bool) "not usable" false (Key.is_usable k); 46 + Alcotest.(check (option bytes)) "no material" None (Key.get_material k) 47 + 48 + let test_make_key () = 49 + let material = Bytes.of_string "0123456789abcdef" in 50 + let k = Key.v ~kid:0x0002 ~algorithm:1 ~material in 51 + Alcotest.(check int) "kid" 0x0002 (Key.kid k); 52 + Alcotest.(check key_state) "state" Key.Pending (Key.state k); 53 + Alcotest.(check bool) "not usable (pending)" false (Key.is_usable k) 54 + 55 + (* {1 State Transitions} *) 56 + 57 + let test_update_empty_to_pending () = 58 + let k = Key.empty ~kid:0x0001 ~algorithm:1 in 59 + let material = Bytes.of_string "secret_key_12345" in 60 + match Key.update ~material k with 61 + | Ok k' -> Alcotest.(check key_state) "state" Key.Pending (Key.state k') 62 + | Error e -> Alcotest.fail (Fmt.str "update failed: %a" Key.pp_error e) 63 + 64 + let test_update_pending_to_pending () = 65 + let material1 = Bytes.of_string "first_key_______" in 66 + let material2 = Bytes.of_string "second_key______" in 67 + let k = Key.v ~kid:0x0001 ~algorithm:1 ~material:material1 in 68 + match Key.update ~material:material2 k with 69 + | Ok k' -> 70 + Alcotest.(check key_state) "still pending" Key.Pending (Key.state k') 71 + | Error e -> Alcotest.fail (Fmt.str "update failed: %a" Key.pp_error e) 72 + 73 + let test_activate_pending_to_active () = 74 + let material = Bytes.of_string "secret_key_12345" in 75 + let k = Key.v ~kid:0x0001 ~algorithm:1 ~material in 76 + match Key.activate k with 77 + | Ok k' -> ( 78 + Alcotest.(check key_state) "state" Key.Active (Key.state k'); 79 + Alcotest.(check bool) "usable" true (Key.is_usable k'); 80 + (* Verify material is accessible *) 81 + match Key.get_material k' with 82 + | Some m -> Alcotest.(check int) "material len" 16 (Bytes.length m) 83 + | None -> Alcotest.fail "no material") 84 + | Error e -> Alcotest.fail (Fmt.str "activate failed: %a" Key.pp_error e) 85 + 86 + let test_activate_empty_fails () = 87 + let k = Key.empty ~kid:0x0001 ~algorithm:1 in 88 + match Key.activate k with 89 + | Ok _ -> Alcotest.fail "should fail" 90 + | Error (Key.Invalid_state_transition { from; to_ }) -> 91 + Alcotest.(check key_state) "from" Key.Empty from; 92 + Alcotest.(check key_state) "to" Key.Active to_ 93 + | Error e -> Alcotest.fail (Fmt.str "unexpected error: %a" Key.pp_error e) 94 + 95 + let test_activate_already_active () = 96 + let material = Bytes.of_string "secret_key_12345" in 97 + let k = Key.v ~kid:0x0001 ~algorithm:1 ~material in 98 + let k = Result.get_ok (Key.activate k) in 99 + match Key.activate k with 100 + | Ok _ -> Alcotest.fail "should return error for already active" 101 + | Error Key.Key_already_active -> () 102 + | Error e -> Alcotest.fail (Fmt.str "unexpected error: %a" Key.pp_error e) 103 + 104 + let test_deactivate_active_to_deprecated () = 105 + let material = Bytes.of_string "secret_key_12345" in 106 + let k = Key.v ~kid:0x0001 ~algorithm:1 ~material in 107 + let k = Result.get_ok (Key.activate k) in 108 + match Key.deactivate k with 109 + | Ok k' -> 110 + Alcotest.(check key_state) "state" Key.Deprecated (Key.state k'); 111 + Alcotest.(check bool) "not usable" false (Key.is_usable k') 112 + | Error e -> Alcotest.fail (Fmt.str "deactivate failed: %a" Key.pp_error e) 113 + 114 + let test_deactivate_idempotent () = 115 + let material = Bytes.of_string "secret_key_12345" in 116 + let k = Key.v ~kid:0x0001 ~algorithm:1 ~material in 117 + let k = Result.get_ok (Key.activate k) in 118 + let k = Result.get_ok (Key.deactivate k) in 119 + match Key.deactivate k with 120 + | Ok k' -> 121 + Alcotest.(check key_state) 122 + "still deprecated" Key.Deprecated (Key.state k') 123 + | Error e -> Alcotest.fail (Fmt.str "deactivate failed: %a" Key.pp_error e) 124 + 125 + let test_destroy_deprecated () = 126 + let material = Bytes.of_string "secret_key_12345" in 127 + let k = Key.v ~kid:0x0001 ~algorithm:1 ~material in 128 + let k = Result.get_ok (Key.activate k) in 129 + let k = Result.get_ok (Key.deactivate k) in 130 + match Key.destroy k with 131 + | Ok k' -> 132 + Alcotest.(check key_state) "state" Key.Zeroized (Key.state k'); 133 + Alcotest.(check bool) "not usable" false (Key.is_usable k'); 134 + Alcotest.(check (option bytes)) "no material" None (Key.get_material k') 135 + | Error e -> Alcotest.fail (Fmt.str "destroy failed: %a" Key.pp_error e) 136 + 137 + let test_destroy_pending () = 138 + let material = Bytes.of_string "secret_key_12345" in 139 + let k = Key.v ~kid:0x0001 ~algorithm:1 ~material in 140 + match Key.destroy k with 141 + | Ok k' -> Alcotest.(check key_state) "state" Key.Zeroized (Key.state k') 142 + | Error e -> Alcotest.fail (Fmt.str "destroy failed: %a" Key.pp_error e) 143 + 144 + let test_destroy_active_fails () = 145 + let material = Bytes.of_string "secret_key_12345" in 146 + let k = Key.v ~kid:0x0001 ~algorithm:1 ~material in 147 + let k = Result.get_ok (Key.activate k) in 148 + match Key.destroy k with 149 + | Ok _ -> Alcotest.fail "should fail (must deactivate first)" 150 + | Error (Key.Invalid_state_transition { from; to_ }) -> 151 + Alcotest.(check key_state) "from" Key.Active from; 152 + Alcotest.(check key_state) "to" Key.Zeroized to_ 153 + | Error e -> Alcotest.fail (Fmt.str "unexpected error: %a" Key.pp_error e) 154 + 155 + let test_expire_pending () = 156 + let material = Bytes.of_string "secret_key_12345" in 157 + let k = Key.v ~kid:0x0001 ~algorithm:1 ~material in 158 + match Key.expire k with 159 + | Ok k' -> Alcotest.(check key_state) "state" Key.Zeroized (Key.state k') 160 + | Error e -> Alcotest.fail (Fmt.str "expire failed: %a" Key.pp_error e) 161 + 162 + let test_full_lifecycle () = 163 + (* Empty -> Pending -> Active -> Deprecated -> Zeroized *) 164 + let k = Key.empty ~kid:0x0001 ~algorithm:1 in 165 + Alcotest.(check key_state) "empty" Key.Empty (Key.state k); 166 + 167 + let material = Bytes.of_string "secret_key_12345" in 168 + let k = Result.get_ok (Key.update ~material k) in 169 + Alcotest.(check key_state) "pending" Key.Pending (Key.state k); 170 + 171 + let k = Result.get_ok (Key.activate k) in 172 + Alcotest.(check key_state) "active" Key.Active (Key.state k); 173 + Alcotest.(check bool) "usable" true (Key.is_usable k); 174 + 175 + let k = Result.get_ok (Key.deactivate k) in 176 + Alcotest.(check key_state) "deprecated" Key.Deprecated (Key.state k); 177 + Alcotest.(check bool) "not usable" false (Key.is_usable k); 178 + 179 + let k = Result.get_ok (Key.destroy k) in 180 + Alcotest.(check key_state) "zeroized" Key.Zeroized (Key.state k) 181 + 182 + (* {1 Keyring Tests} *) 183 + 184 + let test_keyring_add_find () = 185 + let k1 = 186 + Key.v ~kid:0x0001 ~algorithm:1 187 + ~material:(Bytes.of_string "key1____________") 188 + in 189 + let k2 = 190 + Key.v ~kid:0x0002 ~algorithm:1 191 + ~material:(Bytes.of_string "key2____________") 192 + in 193 + let ring = Key.Keyring.empty |> Key.Keyring.add k1 |> Key.Keyring.add k2 in 194 + match Key.Keyring.find 0x0001 ring with 195 + | Some k -> Alcotest.(check int) "kid" 0x0001 (Key.kid k) 196 + | None -> Alcotest.fail "key not found" 197 + 198 + let test_keyring_replace () = 199 + let k1 = 200 + Key.v ~kid:0x0001 ~algorithm:1 201 + ~material:(Bytes.of_string "key1____________") 202 + in 203 + let k1' = Result.get_ok (Key.activate k1) in 204 + let ring = Key.Keyring.empty |> Key.Keyring.add k1 |> Key.Keyring.add k1' in 205 + match Key.Keyring.find 0x0001 ring with 206 + | Some k -> Alcotest.(check key_state) "active" Key.Active (Key.state k) 207 + | None -> Alcotest.fail "key not found" 208 + 209 + let test_keyring_inventory () = 210 + let k1 = 211 + Key.v ~kid:0x0001 ~algorithm:1 212 + ~material:(Bytes.of_string "key1____________") 213 + in 214 + let k2 = 215 + Key.v ~kid:0x0002 ~algorithm:1 216 + ~material:(Bytes.of_string "key2____________") 217 + in 218 + let k2 = Result.get_ok (Key.activate k2) in 219 + let k3 = Key.empty ~kid:0x0003 ~algorithm:1 in 220 + let ring = 221 + Key.Keyring.empty |> Key.Keyring.add k1 |> Key.Keyring.add k2 222 + |> Key.Keyring.add k3 223 + in 224 + let inv = Key.Keyring.inventory ring in 225 + Alcotest.(check int) "count" 3 (List.length inv); 226 + let check_entry (kid, state) = (kid, Key.int_of_state state) in 227 + let inv' = List.map check_entry inv in 228 + Alcotest.(check bool) "has 0x0001 pending" true (List.mem (0x0001, 1) inv'); 229 + Alcotest.(check bool) "has 0x0002 active" true (List.mem (0x0002, 2) inv'); 230 + Alcotest.(check bool) "has 0x0003 empty" true (List.mem (0x0003, 0) inv') 231 + 232 + let test_keyring_find_active () = 233 + let k1 = 234 + Key.v ~kid:0x0001 ~algorithm:1 235 + ~material:(Bytes.of_string "key1____________") 236 + in 237 + let k2 = 238 + Key.v ~kid:0x0002 ~algorithm:1 239 + ~material:(Bytes.of_string "key2____________") 240 + in 241 + let k2 = Result.get_ok (Key.activate k2) in 242 + let k3 = 243 + Key.v ~kid:0x0003 ~algorithm:1 244 + ~material:(Bytes.of_string "key3____________") 245 + in 246 + let k3 = Result.get_ok (Key.activate k3) in 247 + let ring = 248 + Key.Keyring.empty |> Key.Keyring.add k1 |> Key.Keyring.add k2 249 + |> Key.Keyring.add k3 250 + in 251 + let active = Key.Keyring.find_active ring in 252 + Alcotest.(check int) "active count" 2 (List.length active) 253 + 254 + (* {1 Test Suite} *) 255 + 256 + let suite = 257 + ( "key", 258 + [ 259 + (* State encoding *) 260 + Alcotest.test_case "state roundtrip" `Quick test_state_roundtrip; 261 + Alcotest.test_case "state values" `Quick test_state_values; 262 + Alcotest.test_case "state invalid" `Quick test_state_invalid; 263 + (* Construction *) 264 + Alcotest.test_case "empty key" `Quick test_empty_key; 265 + Alcotest.test_case "make key" `Quick test_make_key; 266 + (* Transitions *) 267 + Alcotest.test_case "update empty->pending" `Quick 268 + test_update_empty_to_pending; 269 + Alcotest.test_case "update pending->pending" `Quick 270 + test_update_pending_to_pending; 271 + Alcotest.test_case "activate pending->active" `Quick 272 + test_activate_pending_to_active; 273 + Alcotest.test_case "activate empty fails" `Quick test_activate_empty_fails; 274 + Alcotest.test_case "activate already active" `Quick 275 + test_activate_already_active; 276 + Alcotest.test_case "deactivate active->deprecated" `Quick 277 + test_deactivate_active_to_deprecated; 278 + Alcotest.test_case "deactivate idempotent" `Quick 279 + test_deactivate_idempotent; 280 + Alcotest.test_case "destroy deprecated" `Quick test_destroy_deprecated; 281 + Alcotest.test_case "destroy pending" `Quick test_destroy_pending; 282 + Alcotest.test_case "destroy active fails" `Quick test_destroy_active_fails; 283 + Alcotest.test_case "expire pending" `Quick test_expire_pending; 284 + Alcotest.test_case "full lifecycle" `Quick test_full_lifecycle; 285 + (* Keyring *) 286 + Alcotest.test_case "keyring add/find" `Quick test_keyring_add_find; 287 + Alcotest.test_case "keyring replace" `Quick test_keyring_replace; 288 + Alcotest.test_case "keyring inventory" `Quick test_keyring_inventory; 289 + Alcotest.test_case "keyring find active" `Quick test_keyring_find_active; 290 + ] )
+488
test/test_keystore.ml
··· 1 + (** Tests for Keystore module. *) 2 + 3 + open Sdls 4 + 5 + let test_key = Bytes.of_string "0123456789abcdef" 6 + let test_key2 = Bytes.of_string "fedcba9876543210" 7 + 8 + (** {1 In-Memory Keystore Tests} *) 9 + 10 + let test_add_get_key () = 11 + let store = Keystore.in_memory () in 12 + let key_id = Keyid.of_int_exn 1 in 13 + Keystore.add store key_id test_key; 14 + match Keystore.get store key_id with 15 + | Some e -> 16 + Alcotest.(check bool) "key matches" true (Bytes.equal e.material test_key) 17 + | None -> Alcotest.fail "key not found" 18 + 19 + let test_key_not_found () = 20 + let store = Keystore.in_memory () in 21 + let key_id = Keyid.of_int_exn 99 in 22 + match Keystore.get store key_id with 23 + | Some _ -> Alcotest.fail "should not find key" 24 + | None -> () 25 + 26 + let test_multiple_keys () = 27 + let store = Keystore.in_memory () in 28 + let key1 = Keyid.of_int_exn 1 in 29 + let key2 = Keyid.of_int_exn 2 in 30 + Keystore.add store key1 test_key; 31 + Keystore.add store key2 test_key2; 32 + (match Keystore.get store key1 with 33 + | Some e -> 34 + Alcotest.(check bool) "key1" true (Bytes.equal e.material test_key) 35 + | None -> Alcotest.fail "key1 not found"); 36 + match Keystore.get store key2 with 37 + | Some e -> 38 + Alcotest.(check bool) "key2" true (Bytes.equal e.material test_key2) 39 + | None -> Alcotest.fail "key2 not found" 40 + 41 + let test_overwrite_key () = 42 + let store = Keystore.in_memory () in 43 + let key_id = Keyid.of_int_exn 1 in 44 + Keystore.add store key_id test_key; 45 + Keystore.add store key_id test_key2; 46 + match Keystore.get store key_id with 47 + | Some e -> 48 + Alcotest.(check bool) 49 + "key updated" true 50 + (Bytes.equal e.material test_key2) 51 + | None -> Alcotest.fail "key not found after update" 52 + 53 + (** {1 Key Lifecycle Tests} *) 54 + 55 + let test_key_lifecycle () = 56 + let store = Keystore.in_memory () in 57 + let key_id = Keyid.of_int_exn 1 in 58 + (* Add key - should be Pre_active *) 59 + Keystore.add store key_id test_key; 60 + (match Keystore.get store key_id with 61 + | Some e -> 62 + Alcotest.(check bool) 63 + "initial state is Pre_active" true (e.state = Pre_active) 64 + | None -> Alcotest.fail "key not found"); 65 + (* Activate key *) 66 + Alcotest.(check bool) 67 + "activate succeeds" true 68 + (Keystore.activate store key_id); 69 + (match Keystore.get store key_id with 70 + | Some e -> Alcotest.(check bool) "state is Active" true (e.state = Active) 71 + | None -> Alcotest.fail "key not found after activate"); 72 + (* Deactivate key *) 73 + Alcotest.(check bool) 74 + "deactivate succeeds" true 75 + (Keystore.deactivate store key_id); 76 + (match Keystore.get store key_id with 77 + | Some e -> 78 + Alcotest.(check bool) "state is Deactivated" true (e.state = Deactivated) 79 + | None -> Alcotest.fail "key not found after deactivate"); 80 + (* Destroy key *) 81 + Alcotest.(check bool) "destroy succeeds" true (Keystore.destroy store key_id); 82 + match Keystore.get store key_id with 83 + | Some e -> 84 + Alcotest.(check bool) "state is Destroyed" true (e.state = Destroyed); 85 + Alcotest.(check int) "material is empty" 0 (Bytes.length e.material) 86 + | None -> Alcotest.fail "key not found after destroy" 87 + 88 + let test_invalid_lifecycle_transitions () = 89 + let store = Keystore.in_memory () in 90 + let key_id = Keyid.of_int_exn 1 in 91 + (* Can't activate non-existent key *) 92 + Alcotest.(check bool) 93 + "can't activate missing key" false 94 + (Keystore.activate store key_id); 95 + (* Can't deactivate non-existent key *) 96 + Alcotest.(check bool) 97 + "can't deactivate missing key" false 98 + (Keystore.deactivate store key_id); 99 + (* Add and try invalid transitions *) 100 + Keystore.add store key_id test_key; 101 + (* Can't deactivate Pre_active key *) 102 + Alcotest.(check bool) 103 + "can't deactivate Pre_active" false 104 + (Keystore.deactivate store key_id); 105 + (* Activate then try double-activate *) 106 + ignore (Keystore.activate store key_id); 107 + Alcotest.(check bool) 108 + "can't double-activate" false 109 + (Keystore.activate store key_id) 110 + 111 + (** {1 SDLS Integration Tests} *) 112 + 113 + let test_get_encryption_key () = 114 + let store = Keystore.in_memory () in 115 + let key_id = Keyid.of_int_exn 256 in 116 + Keystore.add store key_id test_key; 117 + (* Key is Pre_active, should not be returned *) 118 + (match Keystore.get_encryption_key store key_id with 119 + | Some _ -> Alcotest.fail "should not return Pre_active key" 120 + | None -> ()); 121 + (* Activate the key *) 122 + ignore (Keystore.activate store key_id); 123 + match Keystore.get_encryption_key store key_id with 124 + | Some k -> Alcotest.(check bool) "key matches" true (Bytes.equal k test_key) 125 + | None -> Alcotest.fail "encryption key not found" 126 + 127 + let test_get_auth_key () = 128 + let store = Keystore.in_memory () in 129 + let key_id = Keyid.of_int_exn 257 in 130 + Keystore.add store key_id test_key; 131 + (* Activate the key *) 132 + ignore (Keystore.activate store key_id); 133 + match Keystore.get_auth_key store key_id with 134 + | Some k -> Alcotest.(check bool) "key matches" true (Bytes.equal k test_key) 135 + | None -> Alcotest.fail "auth key not found" 136 + 137 + let test_get_decryption_key () = 138 + (* Per CCSDS 355.1-B-1: Deactivated keys can decrypt old data *) 139 + let store = Keystore.in_memory () in 140 + let key_id = Keyid.of_int_exn 258 in 141 + Keystore.add store key_id test_key; 142 + (* Pre_active: should NOT be returned *) 143 + (match Keystore.get_decryption_key store key_id with 144 + | Some _ -> Alcotest.fail "should not return Pre_active key" 145 + | None -> ()); 146 + (* Active: should be returned *) 147 + ignore (Keystore.activate store key_id); 148 + (match Keystore.get_decryption_key store key_id with 149 + | Some k -> 150 + Alcotest.(check bool) "active key matches" true (Bytes.equal k test_key) 151 + | None -> Alcotest.fail "active key not found for decrypt"); 152 + (* Deactivated: should STILL be returned (this is the key difference!) *) 153 + ignore (Keystore.deactivate store key_id); 154 + (match Keystore.get_decryption_key store key_id with 155 + | Some k -> 156 + Alcotest.(check bool) 157 + "deactivated key matches" true (Bytes.equal k test_key) 158 + | None -> Alcotest.fail "deactivated key should be returned for decrypt"); 159 + (* Destroyed: should NOT be returned *) 160 + ignore (Keystore.destroy store key_id); 161 + match Keystore.get_decryption_key store key_id with 162 + | Some _ -> Alcotest.fail "should not return Destroyed key" 163 + | None -> () 164 + 165 + let test_get_verify_key () = 166 + (* Verify key follows same rules as decryption key *) 167 + let store = Keystore.in_memory () in 168 + let key_id = Keyid.of_int_exn 259 in 169 + Keystore.add store key_id test_key; 170 + ignore (Keystore.activate store key_id); 171 + ignore (Keystore.deactivate store key_id); 172 + (* Deactivated key should be returned for verify *) 173 + match Keystore.get_verify_key store key_id with 174 + | Some k -> 175 + Alcotest.(check bool) 176 + "deactivated key for verify" true (Bytes.equal k test_key) 177 + | None -> Alcotest.fail "deactivated key should be returned for verify" 178 + 179 + let test_list_keys () = 180 + let store = Keystore.in_memory () in 181 + let key1 = Keyid.of_int_exn 1 in 182 + let key2 = Keyid.of_int_exn 2 in 183 + let key3 = Keyid.of_int_exn 3 in 184 + Keystore.add store key1 test_key; 185 + Keystore.add store key2 test_key; 186 + Keystore.add store key3 test_key; 187 + let keys = Keystore.list store in 188 + Alcotest.(check int) "three keys" 3 (List.length keys); 189 + Alcotest.(check bool) "contains key1" true (List.mem key1 keys); 190 + Alcotest.(check bool) "contains key2" true (List.mem key2 keys); 191 + Alcotest.(check bool) "contains key3" true (List.mem key3 keys) 192 + 193 + let test_list_by_state () = 194 + let store = Keystore.in_memory () in 195 + let key1 = Keyid.of_int_exn 1 in 196 + let key2 = Keyid.of_int_exn 2 in 197 + let key3 = Keyid.of_int_exn 3 in 198 + Keystore.add store key1 test_key; 199 + Keystore.add store key2 test_key; 200 + Keystore.add store key3 test_key; 201 + (* All should be Pre_active *) 202 + let pre_active = Keystore.list_by_state store Pre_active in 203 + Alcotest.(check int) "three Pre_active" 3 (List.length pre_active); 204 + (* Activate key1 and key2 *) 205 + ignore (Keystore.activate store key1); 206 + ignore (Keystore.activate store key2); 207 + let active = Keystore.list_by_state store Active in 208 + Alcotest.(check int) "two Active" 2 (List.length active); 209 + let pre_active = Keystore.list_by_state store Pre_active in 210 + Alcotest.(check int) "one Pre_active" 1 (List.length pre_active) 211 + 212 + let test_remove_key () = 213 + let store = Keystore.in_memory () in 214 + let key_id = Keyid.of_int_exn 1 in 215 + Keystore.add store key_id test_key; 216 + Alcotest.(check bool) 217 + "key exists" true 218 + (Option.is_some (Keystore.get store key_id)); 219 + Keystore.remove store key_id; 220 + Alcotest.(check bool) 221 + "key removed" true 222 + (Option.is_none (Keystore.get store key_id)) 223 + 224 + (** {1 Entry Serialization Tests} *) 225 + 226 + let test_entry_roundtrip_pre_active () = 227 + let entry : Keystore.entry = { material = test_key; state = Pre_active } in 228 + let w = Binary.Writer.create 64 in 229 + Keystore.write_entry w entry; 230 + let encoded = Binary.Writer.contents w in 231 + match Keystore.read_entry (Binary.Reader.of_bytes encoded) with 232 + | Some entry' -> 233 + Alcotest.(check bool) "state" true (entry'.state = Keystore.Pre_active); 234 + Alcotest.(check bool) 235 + "material" true 236 + (Bytes.equal entry.material entry'.material) 237 + | None -> Alcotest.fail "entry deserialization failed" 238 + 239 + let test_entry_roundtrip_active () = 240 + let entry : Keystore.entry = { material = test_key; state = Active } in 241 + let w = Binary.Writer.create 64 in 242 + Keystore.write_entry w entry; 243 + let encoded = Binary.Writer.contents w in 244 + match Keystore.read_entry (Binary.Reader.of_bytes encoded) with 245 + | Some entry' -> 246 + Alcotest.(check bool) "state" true (entry'.state = Keystore.Active); 247 + Alcotest.(check bool) 248 + "material" true 249 + (Bytes.equal entry.material entry'.material) 250 + | None -> Alcotest.fail "entry deserialization failed" 251 + 252 + let test_entry_roundtrip_deactivated () = 253 + let entry : Keystore.entry = { material = test_key2; state = Deactivated } in 254 + let w = Binary.Writer.create 64 in 255 + Keystore.write_entry w entry; 256 + let encoded = Binary.Writer.contents w in 257 + match Keystore.read_entry (Binary.Reader.of_bytes encoded) with 258 + | Some entry' -> 259 + Alcotest.(check bool) "state" true (entry'.state = Keystore.Deactivated); 260 + Alcotest.(check bool) 261 + "material" true 262 + (Bytes.equal entry.material entry'.material) 263 + | None -> Alcotest.fail "entry deserialization failed" 264 + 265 + let test_entry_roundtrip_destroyed () = 266 + (* Destroyed keys should have empty material *) 267 + let entry : Keystore.entry = { material = Bytes.empty; state = Destroyed } in 268 + let w = Binary.Writer.create 64 in 269 + Keystore.write_entry w entry; 270 + let encoded = Binary.Writer.contents w in 271 + match Keystore.read_entry (Binary.Reader.of_bytes encoded) with 272 + | Some entry' -> 273 + Alcotest.(check bool) "state" true (entry'.state = Keystore.Destroyed); 274 + Alcotest.(check int) "material length" 0 (Bytes.length entry'.material) 275 + | None -> Alcotest.fail "entry deserialization failed" 276 + 277 + let test_entry_roundtrip_large_key () = 278 + (* Test with a larger key (e.g., 64-byte key for HMAC-SHA-512) *) 279 + let large_key = Bytes.init 64 (fun i -> Char.chr (i * 7 land 0xFF)) in 280 + let entry : Keystore.entry = { material = large_key; state = Active } in 281 + let w = Binary.Writer.create 128 in 282 + Keystore.write_entry w entry; 283 + let encoded = Binary.Writer.contents w in 284 + match Keystore.read_entry (Binary.Reader.of_bytes encoded) with 285 + | Some entry' -> 286 + Alcotest.(check int) "material length" 64 (Bytes.length entry'.material); 287 + Alcotest.(check bool) 288 + "material" true 289 + (Bytes.equal large_key entry'.material) 290 + | None -> Alcotest.fail "entry deserialization failed" 291 + 292 + let test_entry_roundtrip_all_states () = 293 + let states = 294 + [ 295 + Keystore.Pre_active; 296 + Keystore.Active; 297 + Keystore.Deactivated; 298 + Keystore.Destroyed; 299 + ] 300 + in 301 + List.iter 302 + (fun state -> 303 + let material = 304 + if state = Keystore.Destroyed then Bytes.empty else test_key 305 + in 306 + let entry : Keystore.entry = { material; state } in 307 + let w = Binary.Writer.create 64 in 308 + Keystore.write_entry w entry; 309 + let encoded = Binary.Writer.contents w in 310 + match Keystore.read_entry (Binary.Reader.of_bytes encoded) with 311 + | Some entry' -> 312 + Alcotest.(check bool) 313 + (Format.asprintf "state %a" Keystore.pp_key_state state) 314 + true (entry'.state = state) 315 + | None -> Alcotest.fail "entry deserialization failed") 316 + states 317 + 318 + (** {1 Key Length Validation Tests} *) 319 + 320 + let aes256_key = Bytes.init 32 (fun i -> Char.chr (i land 0xFF)) 321 + let hmac256_key = Bytes.init 32 (fun i -> Char.chr (i * 3 land 0xFF)) 322 + let hmac384_key = Bytes.init 48 (fun i -> Char.chr (i * 5 land 0xFF)) 323 + let hmac512_key = Bytes.init 64 (fun i -> Char.chr (i * 7 land 0xFF)) 324 + 325 + let test_add_for_cipher_aes256_valid () = 326 + let store = Keystore.in_memory () in 327 + let key_id = Keyid.of_int_exn 100 in 328 + match Keystore.add_for_cipher store key_id Keystore.AES_256 aes256_key with 329 + | Ok () -> ( 330 + (* Key should be added in Pre_active state *) 331 + match Keystore.get store key_id with 332 + | Some e -> 333 + Alcotest.(check bool) "state" true (e.state = Pre_active); 334 + Alcotest.(check int) "length" 32 (Bytes.length e.material) 335 + | None -> Alcotest.fail "key not found") 336 + | Error _ -> Alcotest.fail "should accept 32-byte AES-256 key" 337 + 338 + let test_add_for_cipher_aes256_too_short () = 339 + let store = Keystore.in_memory () in 340 + let key_id = Keyid.of_int_exn 101 in 341 + let short_key = Bytes.make 16 '\x00' in 342 + match Keystore.add_for_cipher store key_id Keystore.AES_256 short_key with 343 + | Ok () -> Alcotest.fail "should reject 16-byte key for AES-256" 344 + | Error (`Invalid_key_len e) -> 345 + Alcotest.(check int) "expected" 32 e.expected; 346 + Alcotest.(check int) "actual" 16 e.actual 347 + 348 + let test_add_for_cipher_aes256_too_long () = 349 + let store = Keystore.in_memory () in 350 + let key_id = Keyid.of_int_exn 102 in 351 + let long_key = Bytes.make 64 '\x00' in 352 + (* AES-256 requires EXACT length, so 64 bytes should be rejected *) 353 + match Keystore.add_for_cipher store key_id Keystore.AES_256 long_key with 354 + | Ok () -> Alcotest.fail "should reject 64-byte key for AES-256" 355 + | Error (`Invalid_key_len e) -> 356 + Alcotest.(check int) "expected" 32 e.expected; 357 + Alcotest.(check int) "actual" 64 e.actual 358 + 359 + let test_add_for_cipher_hmac256_valid () = 360 + let store = Keystore.in_memory () in 361 + let key_id = Keyid.of_int_exn 103 in 362 + match 363 + Keystore.add_for_cipher store key_id Keystore.HMAC_SHA_256 hmac256_key 364 + with 365 + | Ok () -> () 366 + | Error _ -> Alcotest.fail "should accept 32-byte HMAC-SHA-256 key" 367 + 368 + let test_add_for_cipher_hmac256_too_short () = 369 + let store = Keystore.in_memory () in 370 + let key_id = Keyid.of_int_exn 104 in 371 + let short_key = Bytes.make 24 '\x00' in 372 + match 373 + Keystore.add_for_cipher store key_id Keystore.HMAC_SHA_256 short_key 374 + with 375 + | Ok () -> Alcotest.fail "should reject 24-byte key for HMAC-SHA-256" 376 + | Error (`Invalid_key_len e) -> 377 + Alcotest.(check int) "expected" 32 e.expected; 378 + Alcotest.(check int) "actual" 24 e.actual 379 + 380 + let test_add_for_cipher_hmac256_longer_ok () = 381 + (* HMAC keys can be longer than minimum *) 382 + let store = Keystore.in_memory () in 383 + let key_id = Keyid.of_int_exn 105 in 384 + let long_key = Bytes.make 64 '\x00' in 385 + match Keystore.add_for_cipher store key_id Keystore.HMAC_SHA_256 long_key with 386 + | Ok () -> () 387 + | Error _ -> Alcotest.fail "should accept 64-byte key for HMAC-SHA-256" 388 + 389 + let test_add_for_cipher_hmac384_min () = 390 + let store = Keystore.in_memory () in 391 + let key_id = Keyid.of_int_exn 106 in 392 + match 393 + Keystore.add_for_cipher store key_id Keystore.HMAC_SHA_384 hmac384_key 394 + with 395 + | Ok () -> () 396 + | Error _ -> Alcotest.fail "should accept 48-byte HMAC-SHA-384 key" 397 + 398 + let test_add_for_cipher_hmac384_too_short () = 399 + let store = Keystore.in_memory () in 400 + let key_id = Keyid.of_int_exn 107 in 401 + let short_key = Bytes.make 32 '\x00' in 402 + (* 32 bytes is too short for HMAC-SHA-384 (needs 48) *) 403 + match 404 + Keystore.add_for_cipher store key_id Keystore.HMAC_SHA_384 short_key 405 + with 406 + | Ok () -> Alcotest.fail "should reject 32-byte key for HMAC-SHA-384" 407 + | Error (`Invalid_key_len e) -> 408 + Alcotest.(check int) "expected" 48 e.expected; 409 + Alcotest.(check int) "actual" 32 e.actual 410 + 411 + let test_add_for_cipher_hmac512_min () = 412 + let store = Keystore.in_memory () in 413 + let key_id = Keyid.of_int_exn 108 in 414 + match 415 + Keystore.add_for_cipher store key_id Keystore.HMAC_SHA_512 hmac512_key 416 + with 417 + | Ok () -> () 418 + | Error _ -> Alcotest.fail "should accept 64-byte HMAC-SHA-512 key" 419 + 420 + let test_add_for_cipher_hmac512_too_short () = 421 + let store = Keystore.in_memory () in 422 + let key_id = Keyid.of_int_exn 109 in 423 + let short_key = Bytes.make 48 '\x00' in 424 + (* 48 bytes is too short for HMAC-SHA-512 (needs 64) *) 425 + match 426 + Keystore.add_for_cipher store key_id Keystore.HMAC_SHA_512 short_key 427 + with 428 + | Ok () -> Alcotest.fail "should reject 48-byte key for HMAC-SHA-512" 429 + | Error (`Invalid_key_len e) -> 430 + Alcotest.(check int) "expected" 64 e.expected; 431 + Alcotest.(check int) "actual" 48 e.actual 432 + 433 + let suite = 434 + ( "keystore", 435 + [ 436 + (* In-memory keystore *) 437 + Alcotest.test_case "add/get key" `Quick test_add_get_key; 438 + Alcotest.test_case "key not found" `Quick test_key_not_found; 439 + Alcotest.test_case "multiple keys" `Quick test_multiple_keys; 440 + Alcotest.test_case "overwrite key" `Quick test_overwrite_key; 441 + (* Key lifecycle *) 442 + Alcotest.test_case "key lifecycle" `Quick test_key_lifecycle; 443 + Alcotest.test_case "invalid transitions" `Quick 444 + test_invalid_lifecycle_transitions; 445 + (* SDLS integration *) 446 + Alcotest.test_case "get encryption key" `Quick test_get_encryption_key; 447 + Alcotest.test_case "get auth key" `Quick test_get_auth_key; 448 + Alcotest.test_case "get decryption key" `Quick test_get_decryption_key; 449 + Alcotest.test_case "get verify key" `Quick test_get_verify_key; 450 + (* List operations *) 451 + Alcotest.test_case "list keys" `Quick test_list_keys; 452 + Alcotest.test_case "list by state" `Quick test_list_by_state; 453 + Alcotest.test_case "remove key" `Quick test_remove_key; 454 + (* Entry serialization *) 455 + Alcotest.test_case "entry roundtrip Pre_active" `Quick 456 + test_entry_roundtrip_pre_active; 457 + Alcotest.test_case "entry roundtrip Active" `Quick 458 + test_entry_roundtrip_active; 459 + Alcotest.test_case "entry roundtrip Deactivated" `Quick 460 + test_entry_roundtrip_deactivated; 461 + Alcotest.test_case "entry roundtrip Destroyed" `Quick 462 + test_entry_roundtrip_destroyed; 463 + Alcotest.test_case "entry roundtrip large key" `Quick 464 + test_entry_roundtrip_large_key; 465 + Alcotest.test_case "entry roundtrip all states" `Quick 466 + test_entry_roundtrip_all_states; 467 + (* Key length validation *) 468 + Alcotest.test_case "add_for_cipher AES-256 valid" `Quick 469 + test_add_for_cipher_aes256_valid; 470 + Alcotest.test_case "add_for_cipher AES-256 too short" `Quick 471 + test_add_for_cipher_aes256_too_short; 472 + Alcotest.test_case "add_for_cipher AES-256 too long" `Quick 473 + test_add_for_cipher_aes256_too_long; 474 + Alcotest.test_case "add_for_cipher HMAC-256 valid" `Quick 475 + test_add_for_cipher_hmac256_valid; 476 + Alcotest.test_case "add_for_cipher HMAC-256 too short" `Quick 477 + test_add_for_cipher_hmac256_too_short; 478 + Alcotest.test_case "add_for_cipher HMAC-256 longer ok" `Quick 479 + test_add_for_cipher_hmac256_longer_ok; 480 + Alcotest.test_case "add_for_cipher HMAC-384 min" `Quick 481 + test_add_for_cipher_hmac384_min; 482 + Alcotest.test_case "add_for_cipher HMAC-384 too short" `Quick 483 + test_add_for_cipher_hmac384_too_short; 484 + Alcotest.test_case "add_for_cipher HMAC-512 min" `Quick 485 + test_add_for_cipher_hmac512_min; 486 + Alcotest.test_case "add_for_cipher HMAC-512 too short" `Quick 487 + test_add_for_cipher_hmac512_too_short; 488 + ] )
+793
test/test_sa.ml
··· 1 + (** Tests for Sa module - Security Association types and management. *) 2 + 3 + open Sdls 4 + 5 + let hex s = Hex.decode_exn s 6 + 7 + (* {1 Stream API Helpers} 8 + 9 + Wrappers for the stream-based write/read API to simplify tests. *) 10 + 11 + let encode_with writer_fn value size = 12 + let w = Binary.Writer.create size in 13 + writer_fn w value; 14 + Binary.Writer.contents w 15 + 16 + let parse_with reader_fn buf = reader_fn (Binary.Reader.of_bytes buf) 17 + 18 + (** {1 SA Constructor Tests} *) 19 + 20 + let test_sa_gcm_only () = 21 + let sa = Sa.gcm_only ~spi:1 ~scid:3 ~vcid:0 in 22 + Alcotest.(check int) "spi" 1 sa.config.spi; 23 + Alcotest.(check bool) "encryption" true sa.config.encryption; 24 + Alcotest.(check bool) "authentication" true sa.config.authentication; 25 + Alcotest.(check int) "iv_len" 12 sa.config.iv_len; 26 + Alcotest.(check int) "mac_len" 16 sa.config.mac_len 27 + 28 + let test_sa_auth_only () = 29 + let sa = Sa.auth_only ~spi:2 ~scid:3 ~vcid:0 in 30 + Alcotest.(check bool) "encryption" false sa.config.encryption; 31 + Alcotest.(check bool) "authentication" true sa.config.authentication 32 + 33 + (** {1 IV Management Tests} *) 34 + 35 + let test_increment_iv () = 36 + let iv = hex "000000000000000000000000" in 37 + let iv' = Sa.increment_iv iv in 38 + Alcotest.(check string) 39 + "incremented" "000000000000000000000001" (Hex.encode iv') 40 + 41 + let test_increment_iv_wraparound () = 42 + let iv = hex "0000000000000000000000ff" in 43 + let iv' = Sa.increment_iv iv in 44 + Alcotest.(check string) 45 + "wraparound" "000000000000000000000100" (Hex.encode iv') 46 + 47 + let test_increment_iv_full_wraparound () = 48 + let iv = hex "ffffffffffffffffffff" in 49 + let iv' = Sa.increment_iv iv in 50 + Alcotest.(check string) 51 + "full wraparound" "00000000000000000000" (Hex.encode iv') 52 + 53 + let test_is_iv_max () = 54 + let max_iv = hex "ffffffffffffffff" in 55 + Alcotest.(check bool) "all-FF is max" true (Sa.is_iv_max max_iv); 56 + let not_max = hex "fffffffffffffffe" in 57 + Alcotest.(check bool) "almost-max not max" false (Sa.is_iv_max not_max); 58 + let zero_iv = hex "0000000000000000" in 59 + Alcotest.(check bool) "zero not max" false (Sa.is_iv_max zero_iv) 60 + 61 + let test_increment_iv_safe () = 62 + let normal = hex "000000000000000000000000" in 63 + (match Sa.increment_iv_safe normal with 64 + | Some _ -> () 65 + | None -> Alcotest.fail "should increment normal IV"); 66 + let max_iv = hex "ffffffffffffffffffffffff" in 67 + match Sa.increment_iv_safe max_iv with 68 + | Some _ -> Alcotest.fail "should return None on max IV" 69 + | None -> () 70 + 71 + (** {1 Big-Endian Comparison Tests} *) 72 + 73 + let test_cmp_be_unequal_lengths () = 74 + let short = hex "01" in 75 + let long = hex "000001" in 76 + Alcotest.(check int) "1 = 0x000001" 0 (Sa.cmp_be short long); 77 + Alcotest.(check int) "0x000001 = 1" 0 (Sa.cmp_be long short); 78 + let two = hex "02" in 79 + Alcotest.(check int) "2 > 0x000001" 1 (Sa.cmp_be two long); 80 + Alcotest.(check int) "0x000001 < 2" (-1) (Sa.cmp_be long two) 81 + 82 + let test_diff_be_unequal_lengths () = 83 + let short = hex "05" in 84 + let long = hex "000002" in 85 + Alcotest.(check int) "5 - 2 = 3" 3 (Sa.diff_be short long); 86 + let short_two = hex "02" in 87 + let long_five = hex "000005" in 88 + Alcotest.(check int) "0x000005 - 2 = 3" 3 (Sa.diff_be long_five short_two) 89 + 90 + let test_diff_be_bounded_high_order () = 91 + let a = hex "01000000" in 92 + let b = hex "00000001" in 93 + match Sa.diff_be_bounded a b ~limit:1000 with 94 + | Some _ -> Alcotest.fail "should exceed limit" 95 + | None -> () 96 + 97 + let test_diff_be_bounded_small () = 98 + let a = hex "00000064" in 99 + let b = hex "0000005a" in 100 + match Sa.diff_be_bounded a b ~limit:100 with 101 + | Some diff -> Alcotest.(check int) "diff" 10 diff 102 + | None -> Alcotest.fail "should compute diff" 103 + 104 + let test_diff_be_bounded_negative () = 105 + let a = hex "00000001" in 106 + let b = hex "00000064" in 107 + match Sa.diff_be_bounded a b ~limit:1000 with 108 + | Some _ -> Alcotest.fail "should return None for negative" 109 + | None -> () 110 + 111 + let test_diff_be_bounded_various_lengths () = 112 + let a2 = hex "0064" in 113 + let b2 = hex "005a" in 114 + (match Sa.diff_be_bounded a2 b2 ~limit:100 with 115 + | Some diff -> Alcotest.(check int) "2-byte diff" 10 diff 116 + | None -> Alcotest.fail "should compute 2-byte diff"); 117 + 118 + let a3 = hex "000064" in 119 + let b3 = hex "00005a" in 120 + (match Sa.diff_be_bounded a3 b3 ~limit:100 with 121 + | Some diff -> Alcotest.(check int) "3-byte diff" 10 diff 122 + | None -> Alcotest.fail "should compute 3-byte diff"); 123 + 124 + let a8 = hex "0000000000000064" in 125 + let b8 = hex "000000000000005a" in 126 + match Sa.diff_be_bounded a8 b8 ~limit:100 with 127 + | Some diff -> Alcotest.(check int) "8-byte diff" 10 diff 128 + | None -> Alcotest.fail "should compute 8-byte diff" 129 + 130 + (** {1 Anti-Replay Tests} *) 131 + 132 + let test_anti_replay_accept_newer () = 133 + let sa = Sa.v ~sn_len:4 ~arsnw:1024 ~spi:1 ~scid:3 ~vcid:0 () in 134 + let sa = { sa with dyn = { sa.dyn with arsn = hex "00000010" } } in 135 + let received = hex "00000020" in 136 + Alcotest.(check bool) 137 + "accept newer" true 138 + (Sa.check_anti_replay ~config:sa.config ~dyn:sa.dyn received) 139 + 140 + let test_anti_replay_reject_duplicate () = 141 + let sa = Sa.v ~sn_len:4 ~arsnw:1024 ~spi:1 ~scid:3 ~vcid:0 () in 142 + let sa = { sa with dyn = { sa.dyn with arsn = hex "00000010" } } in 143 + let received = hex "00000010" in 144 + Alcotest.(check bool) 145 + "reject duplicate" false 146 + (Sa.check_anti_replay ~config:sa.config ~dyn:sa.dyn received) 147 + 148 + let test_anti_replay_window () = 149 + let sa = Sa.v ~sn_len:4 ~arsnw:10 ~spi:1 ~scid:3 ~vcid:0 () in 150 + let sa = { sa with dyn = { sa.dyn with arsn = hex "00000020" } } in 151 + let in_window = hex "00000018" in 152 + Alcotest.(check bool) 153 + "in window" true 154 + (Sa.check_anti_replay ~config:sa.config ~dyn:sa.dyn in_window); 155 + let out_window = hex "00000010" in 156 + Alcotest.(check bool) 157 + "out of window" false 158 + (Sa.check_anti_replay ~config:sa.config ~dyn:sa.dyn out_window) 159 + 160 + let test_anti_replay_disabled_when_sn_len_zero () = 161 + let sa = Sa.v ~sn_len:0 ~arsnw:1024 ~spi:1 ~scid:3 ~vcid:0 () in 162 + let any_sn = hex "00000001" in 163 + Alcotest.(check bool) 164 + "anti-replay disabled" true 165 + (Sa.check_anti_replay ~config:sa.config ~dyn:sa.dyn any_sn) 166 + 167 + let test_anti_replay_wraparound () = 168 + let sa = Sa.v ~sn_len:2 ~arsnw:10 ~spi:1 ~scid:3 ~vcid:0 () in 169 + let dyn = 170 + Sa.update_replay_window ~config:sa.config ~dyn:sa.dyn 171 + (Bytes.of_string "\x00\x61") 172 + in 173 + let dyn = 174 + Sa.update_replay_window ~config:sa.config ~dyn (Bytes.of_string "\x00\x62") 175 + in 176 + let dyn = 177 + Sa.update_replay_window ~config:sa.config ~dyn (Bytes.of_string "\x00\x63") 178 + in 179 + let dyn = 180 + Sa.update_replay_window ~config:sa.config ~dyn (Bytes.of_string "\x00\x64") 181 + in 182 + Alcotest.(check bool) 183 + "0x61 already seen" false 184 + (Sa.check_anti_replay ~config:sa.config ~dyn (Bytes.of_string "\x00\x61")); 185 + Alcotest.(check bool) 186 + "0x64 is current" false 187 + (Sa.check_anti_replay ~config:sa.config ~dyn (Bytes.of_string "\x00\x64")); 188 + Alcotest.(check bool) 189 + "0x65 is new" true 190 + (Sa.check_anti_replay ~config:sa.config ~dyn (Bytes.of_string "\x00\x65")) 191 + 192 + let test_anti_replay_big_delta () = 193 + let sa = Sa.v ~sn_len:4 ~arsnw:10 ~spi:1 ~scid:3 ~vcid:0 () in 194 + let sa = { sa with dyn = { sa.dyn with arsn = hex "00000064" } } in 195 + let sn_200 = hex "000000c8" in 196 + Alcotest.(check bool) 197 + "accept SN far ahead" true 198 + (Sa.check_anti_replay ~config:sa.config ~dyn:sa.dyn sn_200); 199 + let sn_100 = hex "00000064" in 200 + Alcotest.(check bool) 201 + "reject current ARSN" false 202 + (Sa.check_anti_replay ~config:sa.config ~dyn:sa.dyn sn_100); 203 + let sn_89 = hex "00000059" in 204 + Alcotest.(check bool) 205 + "reject out of window" false 206 + (Sa.check_anti_replay ~config:sa.config ~dyn:sa.dyn sn_89) 207 + 208 + (** {1 SA Bounds Tests} *) 209 + 210 + let test_sn_len_bounds () = 211 + let valid_sa = Sa.v ~sn_len:8 ~spi:1 ~scid:3 ~vcid:0 () in 212 + Alcotest.(check int) "max sn_len" 8 valid_sa.config.sn_len; 213 + let zero_sn = Sa.v ~sn_len:0 ~spi:1 ~scid:3 ~vcid:0 () in 214 + Alcotest.(check int) "zero sn_len" 0 zero_sn.config.sn_len 215 + 216 + let test_arsnw_bounds () = 217 + let valid_sa = Sa.v ~arsnw:65536 ~spi:1 ~scid:3 ~vcid:0 () in 218 + Alcotest.(check int) "max arsnw" 65536 valid_sa.config.arsnw; 219 + let zero_arsnw = Sa.v ~arsnw:0 ~spi:1 ~scid:3 ~vcid:0 () in 220 + Alcotest.(check int) "zero arsnw (disabled)" 0 zero_arsnw.config.arsnw 221 + 222 + (** {1 SA Management Tests} *) 223 + 224 + let test_sa_start () = 225 + let sa = 226 + Sa.keyed ~spi:1 ~scid:3 ~vcid:0 ~ek_id:(Keyid.of_int_exn 1) 227 + ~ak_id:(Keyid.of_int_exn 1) 228 + in 229 + Alcotest.(check bool) "starts keyed" true (sa.dyn.lifecycle = Sa.Keyed); 230 + match Sa.start sa with 231 + | Ok sa' -> 232 + Alcotest.(check bool) 233 + "now operational" true 234 + (sa'.dyn.lifecycle = Sa.Operational) 235 + | Error _ -> Alcotest.fail "start should succeed" 236 + 237 + let test_sa_stop () = 238 + let sa = Sa.gcm_only ~spi:1 ~scid:3 ~vcid:0 in 239 + Alcotest.(check bool) 240 + "starts operational" true 241 + (sa.dyn.lifecycle = Sa.Operational); 242 + match Sa.stop sa with 243 + | Ok sa' -> 244 + Alcotest.(check bool) "now keyed" true (sa'.dyn.lifecycle = Sa.Keyed) 245 + | Error _ -> Alcotest.fail "stop should succeed" 246 + 247 + let test_sa_rekey () = 248 + let sa = Sa.gcm_only ~spi:1 ~scid:3 ~vcid:0 in 249 + let sa = 250 + { sa with dyn = { sa.dyn with iv = hex "000000000000000000000010" } } 251 + in 252 + let sa' = 253 + Sa.rekey ~ek_id:(Keyid.of_int_exn 2) ~ak_id:(Keyid.of_int_exn 2) sa 254 + in 255 + Alcotest.(check int) "new ek_id" 2 (Keyid.to_int sa'.dyn.ek_id); 256 + Alcotest.(check int) "new ak_id" 2 (Keyid.to_int sa'.dyn.ak_id); 257 + Alcotest.(check string) 258 + "iv reset" "000000000000000000000000" (Hex.encode sa'.dyn.iv) 259 + 260 + let test_sa_expire () = 261 + let sa = Sa.gcm_only ~spi:1 ~scid:3 ~vcid:0 in 262 + let sa' = Sa.expire sa in 263 + Alcotest.(check bool) "now unkeyed" true (sa'.dyn.lifecycle = Sa.Unkeyed); 264 + Alcotest.(check int) "ek_id cleared" 0 (Keyid.to_int sa'.dyn.ek_id); 265 + Alcotest.(check int) "ak_id cleared" 0 (Keyid.to_int sa'.dyn.ak_id) 266 + 267 + let test_sa_status () = 268 + let sa = Sa.gcm_only ~spi:1 ~scid:3 ~vcid:0 in 269 + let status = Sa.status sa in 270 + Alcotest.(check int) "spi matches" 1 status.spi; 271 + Alcotest.(check bool) "operational" true (status.state = Sa.Operational) 272 + 273 + let test_sa_set_arsn () = 274 + let sa = Sa.v ~sn_len:4 ~spi:1 ~scid:3 ~vcid:0 () in 275 + let new_arsn = hex "00000064" in 276 + match Sa.set_arsn ~arsn:new_arsn sa with 277 + | Ok sa' -> 278 + Alcotest.(check string) "arsn set" "00000064" (Hex.encode sa'.dyn.arsn) 279 + | Error _ -> Alcotest.fail "set_arsn should succeed" 280 + 281 + let test_sa_set_arsnw () = 282 + let sa = Sa.gcm_only ~spi:1 ~scid:3 ~vcid:0 in 283 + match Sa.set_arsnw ~arsnw:100 sa with 284 + | Ok sa' -> Alcotest.(check int) "arsnw set" 100 sa'.config.arsnw 285 + | Error _ -> Alcotest.fail "set_arsnw should succeed" 286 + 287 + (** {1 SA PDU Wire Format Tests} *) 288 + 289 + let test_delete_cmd_roundtrip () = 290 + let spi = 0x1234 in 291 + let cmd : Sa.delete_cmd = { spi } in 292 + let encoded = encode_with Sa.write_delete_cmd cmd 2 in 293 + Alcotest.(check int) "len" 2 (Bytes.length encoded); 294 + match parse_with Sa.read_delete_cmd encoded with 295 + | Ok cmd' -> Alcotest.(check int) "spi" spi cmd'.spi 296 + | Error `Truncated -> Alcotest.fail "truncated" 297 + | Error `Invalid -> Alcotest.fail "invalid" 298 + 299 + let test_set_arsn_cmd_roundtrip () = 300 + let spi = 0x0001 in 301 + let arsn = hex "0000000000001234" in 302 + let cmd : Sa.set_arsn_cmd = { spi; arsn } in 303 + let encoded = encode_with Sa.write_set_arsn_cmd cmd 10 in 304 + Alcotest.(check int) "len" 10 (Bytes.length encoded); 305 + match Sa.read_set_arsn_cmd ~sn_len:8 (Binary.Reader.of_bytes encoded) with 306 + | Ok cmd' -> 307 + Alcotest.(check int) "spi" spi cmd'.spi; 308 + Alcotest.(check string) "arsn" (Hex.encode arsn) (Hex.encode cmd'.arsn) 309 + | Error `Truncated -> Alcotest.fail "truncated" 310 + | Error `Invalid -> Alcotest.fail "invalid" 311 + 312 + let test_set_arsnw_cmd_roundtrip () = 313 + let spi = 0x0005 in 314 + let arsnw = 128 in 315 + let cmd : Sa.set_arsnw_cmd = { spi; arsnw } in 316 + let encoded = encode_with Sa.write_set_arsnw_cmd cmd 4 in 317 + Alcotest.(check int) "len" 4 (Bytes.length encoded); 318 + match parse_with Sa.read_set_arsnw_cmd encoded with 319 + | Ok cmd' -> 320 + Alcotest.(check int) "spi" spi cmd'.spi; 321 + Alcotest.(check int) "arsnw" arsnw cmd'.arsnw 322 + | Error `Truncated -> Alcotest.fail "truncated" 323 + | Error `Invalid -> Alcotest.fail "invalid" 324 + 325 + let test_set_arsnw_cmd_max_window () = 326 + let spi = 0x0010 in 327 + let arsnw = 65535 in 328 + let cmd : Sa.set_arsnw_cmd = { spi; arsnw } in 329 + let encoded = encode_with Sa.write_set_arsnw_cmd cmd 4 in 330 + match parse_with Sa.read_set_arsnw_cmd encoded with 331 + | Ok cmd' -> Alcotest.(check int) "arsnw" arsnw cmd'.arsnw 332 + | Error `Truncated -> Alcotest.fail "truncated" 333 + | Error `Invalid -> Alcotest.fail "invalid" 334 + 335 + let test_rekey_cmd_roundtrip () = 336 + let spi = 0x0003 in 337 + let ekid = Keyid.of_int_exn 0x0082 in 338 + let cmd : Sa.rekey_cmd = { spi; ekid } in 339 + let encoded = encode_with Sa.write_rekey_cmd cmd 4 in 340 + Alcotest.(check int) "len" 4 (Bytes.length encoded); 341 + match parse_with Sa.read_rekey_cmd encoded with 342 + | Ok cmd' -> 343 + Alcotest.(check int) "spi" spi cmd'.spi; 344 + Alcotest.(check int) "ekid" (Keyid.to_int ekid) (Keyid.to_int cmd'.ekid) 345 + | Error `Truncated -> Alcotest.fail "truncated" 346 + | Error `Invalid -> Alcotest.fail "invalid" 347 + 348 + let test_expire_cmd_roundtrip () = 349 + let spi = 0x0007 in 350 + let cmd : Sa.expire_cmd = { spi } in 351 + let encoded = encode_with Sa.write_expire_cmd cmd 2 in 352 + Alcotest.(check int) "len" 2 (Bytes.length encoded); 353 + match parse_with Sa.read_expire_cmd encoded with 354 + | Ok cmd' -> Alcotest.(check int) "spi" spi cmd'.spi 355 + | Error `Truncated -> Alcotest.fail "truncated" 356 + | Error `Invalid -> Alcotest.fail "invalid" 357 + 358 + let test_read_arsn_reply_roundtrip () = 359 + let spi = 0x0002 in 360 + let arsn = hex "00000000DEADBEEF" in 361 + let reply : Sa.read_arsn_reply = { spi; arsn } in 362 + let encoded = encode_with Sa.write_read_arsn_reply reply 10 in 363 + Alcotest.(check int) "len" 10 (Bytes.length encoded); 364 + match Sa.read_read_arsn_reply ~sn_len:8 (Binary.Reader.of_bytes encoded) with 365 + | Ok reply' -> 366 + Alcotest.(check int) "spi" spi reply'.spi; 367 + Alcotest.(check string) "arsn" (Hex.encode arsn) (Hex.encode reply'.arsn) 368 + | Error `Truncated -> Alcotest.fail "truncated" 369 + | Error `Invalid -> Alcotest.fail "invalid" 370 + 371 + let test_status_reply_roundtrip () = 372 + let spi = 0x0003 in 373 + let reply : Sa.status_reply = 374 + { 375 + spi; 376 + state = Sa.Operational; 377 + ecs = Some Sa.AES_256_GCM; 378 + acs = Some Sa.AES_256_CMAC; 379 + iv_len = 12; 380 + mac_len = 16; 381 + sn_len = 4; 382 + arsnw = 1024; 383 + } 384 + in 385 + let encoded = encode_with Sa.write_status_reply reply 11 in 386 + match parse_with Sa.read_status_reply encoded with 387 + | Ok reply' -> 388 + Alcotest.(check int) "spi" spi reply'.spi; 389 + Alcotest.(check int) "iv_len" reply.iv_len reply'.iv_len; 390 + Alcotest.(check int) "mac_len" reply.mac_len reply'.mac_len; 391 + Alcotest.(check int) "sn_len" reply.sn_len reply'.sn_len; 392 + Alcotest.(check int) "arsnw" reply.arsnw reply'.arsnw 393 + | Error `Truncated -> Alcotest.fail "truncated" 394 + | Error `Invalid -> Alcotest.fail "invalid" 395 + 396 + let test_status_reply_no_crypto () = 397 + let spi = 0x0000 in 398 + let reply : Sa.status_reply = 399 + { 400 + spi; 401 + state = Sa.Disabled; 402 + ecs = None; 403 + acs = None; 404 + iv_len = 0; 405 + mac_len = 0; 406 + sn_len = 0; 407 + arsnw = 0; 408 + } 409 + in 410 + let encoded = encode_with Sa.write_status_reply reply 9 in 411 + Alcotest.(check int) "len" 9 (Bytes.length encoded); 412 + match parse_with Sa.read_status_reply encoded with 413 + | Ok reply' -> 414 + Alcotest.(check bool) "ecs is none" true (Option.is_none reply'.ecs); 415 + Alcotest.(check bool) "acs is none" true (Option.is_none reply'.acs) 416 + | Error `Truncated -> Alcotest.fail "truncated" 417 + | Error `Invalid -> Alcotest.fail "invalid" 418 + 419 + let test_create_cmd_roundtrip () = 420 + let spi = 0x0005 in 421 + let scid = 3 in 422 + let vcid = 0 in 423 + let cmd : Sa.create_cmd = 424 + { 425 + spi; 426 + scid; 427 + vcid; 428 + ecs = Some Sa.AES_256_CCM; 429 + acs = Some Sa.HMAC_SHA_256; 430 + iv_len = 12; 431 + mac_len = 16; 432 + sn_len = 4; 433 + arsnw = 128; 434 + abm = Sa.All; 435 + } 436 + in 437 + let encoded = encode_with Sa.write_create_cmd cmd 14 in 438 + match parse_with Sa.read_create_cmd encoded with 439 + | Ok cmd' -> 440 + Alcotest.(check int) "spi" spi cmd'.spi; 441 + Alcotest.(check int) "scid" scid cmd'.scid; 442 + Alcotest.(check int) "vcid" vcid cmd'.vcid; 443 + Alcotest.(check int) "iv_len" cmd.iv_len cmd'.iv_len; 444 + Alcotest.(check int) "mac_len" cmd.mac_len cmd'.mac_len; 445 + Alcotest.(check int) "sn_len" cmd.sn_len cmd'.sn_len; 446 + Alcotest.(check int) "arsnw" cmd.arsnw cmd'.arsnw 447 + | Error `Truncated -> Alcotest.fail "truncated" 448 + | Error `Invalid -> Alcotest.fail "invalid" 449 + 450 + let test_create_cmd_with_abm () = 451 + let spi = 0x0006 in 452 + let scid = 10 in 453 + let vcid = 1 in 454 + let abm_mask = hex "FFFFFFFF00000000" in 455 + let cmd : Sa.create_cmd = 456 + { 457 + spi; 458 + scid; 459 + vcid; 460 + ecs = None; 461 + acs = Some Sa.AES_256_CMAC; 462 + iv_len = 0; 463 + mac_len = 16; 464 + sn_len = 0; 465 + arsnw = 0; 466 + abm = Sa.Mask abm_mask; 467 + } 468 + in 469 + let encoded = encode_with Sa.write_create_cmd cmd 20 in 470 + match parse_with Sa.read_create_cmd encoded with 471 + | Ok cmd' -> ( 472 + match cmd'.abm with 473 + | Sa.Mask m -> 474 + Alcotest.(check string) "abm" (Hex.encode abm_mask) (Hex.encode m) 475 + | Sa.All -> Alcotest.fail "expected mask") 476 + | Error `Truncated -> Alcotest.fail "truncated" 477 + | Error `Invalid -> Alcotest.fail "invalid" 478 + 479 + let test_index_of_sn_large_sn () = 480 + (* 8-byte SN close to max value - would overflow naive implementation *) 481 + let large_sn = Bytes.make 8 '\xFF' in 482 + Bytes.set large_sn 7 '\xFE'; 483 + (* 0xFFFFFFFFFFFFFFFE *) 484 + let sa = Sa.v ~sn_len:8 ~arsnw:1024 ~spi:1 ~scid:3 ~vcid:0 () in 485 + (* Should not crash or give wrong index *) 486 + let result = Sa.check_anti_replay ~config:sa.config ~dyn:sa.dyn large_sn in 487 + Alcotest.(check bool) "large SN accepted" true result 488 + 489 + (** {1 Config/Dyn Persistence Serialization Tests} *) 490 + 491 + let test_config_roundtrip () = 492 + let config = 493 + Sa.config ~encryption:true ~authentication:true ~ecs:(Some Sa.AES_256_GCM) 494 + ~acs:(Some Sa.AES_256_CMAC) ~iv_len:12 ~mac_len:16 ~sn_len:4 ~arsnw:1024 495 + ~abm:Sa.All ~spi:0x1234 ~scid:3 ~vcid:5 () 496 + in 497 + let w = Binary.Writer.create 256 in 498 + Sa.write_config w config; 499 + let encoded = Binary.Writer.contents w in 500 + match Sa.read_config (Binary.Reader.of_bytes encoded) with 501 + | Some config' -> 502 + Alcotest.(check int) "spi" config.spi config'.spi; 503 + Alcotest.(check int) "scid" (fst config.gvcid) (fst config'.gvcid); 504 + Alcotest.(check int) "vcid" (snd config.gvcid) (snd config'.gvcid); 505 + Alcotest.(check bool) "encryption" config.encryption config'.encryption; 506 + Alcotest.(check bool) 507 + "authentication" config.authentication config'.authentication; 508 + Alcotest.(check int) "iv_len" config.iv_len config'.iv_len; 509 + Alcotest.(check int) "mac_len" config.mac_len config'.mac_len; 510 + Alcotest.(check int) "sn_len" config.sn_len config'.sn_len; 511 + Alcotest.(check int) "arsnw" config.arsnw config'.arsnw 512 + | None -> Alcotest.fail "config deserialization failed" 513 + 514 + let test_config_roundtrip_with_abm_mask () = 515 + let abm_mask = hex "FFFFFFFF00000000FFFFFFFF" in 516 + let config = 517 + Sa.config ~encryption:false ~authentication:true ~ecs:None 518 + ~acs:(Some Sa.HMAC_SHA_512) ~iv_len:0 ~mac_len:32 ~sn_len:8 ~arsnw:4096 519 + ~abm:(Sa.Mask abm_mask) ~spi:0x5678 ~scid:10 ~vcid:2 () 520 + in 521 + let w = Binary.Writer.create 256 in 522 + Sa.write_config w config; 523 + let encoded = Binary.Writer.contents w in 524 + match Sa.read_config (Binary.Reader.of_bytes encoded) with 525 + | Some config' -> ( 526 + Alcotest.(check bool) "encryption" false config'.encryption; 527 + Alcotest.(check bool) "authentication" true config'.authentication; 528 + match config'.abm with 529 + | Sa.Mask m -> 530 + Alcotest.(check string) 531 + "abm mask" (Hex.encode abm_mask) (Hex.encode m) 532 + | Sa.All -> Alcotest.fail "expected Mask, got All") 533 + | None -> Alcotest.fail "config deserialization failed" 534 + 535 + let test_config_roundtrip_no_crypto () = 536 + let config = 537 + Sa.config ~encryption:false ~authentication:false ~ecs:None ~acs:None 538 + ~iv_len:0 ~mac_len:0 ~sn_len:0 ~arsnw:0 ~abm:Sa.All ~spi:0x0000 ~scid:0 539 + ~vcid:0 () 540 + in 541 + let w = Binary.Writer.create 256 in 542 + Sa.write_config w config; 543 + let encoded = Binary.Writer.contents w in 544 + match Sa.read_config (Binary.Reader.of_bytes encoded) with 545 + | Some config' -> 546 + Alcotest.(check bool) "ecs is none" true (Option.is_none config'.ecs); 547 + Alcotest.(check bool) "acs is none" true (Option.is_none config'.acs); 548 + Alcotest.(check int) "iv_len" 0 config'.iv_len; 549 + Alcotest.(check int) "mac_len" 0 config'.mac_len 550 + | None -> Alcotest.fail "config deserialization failed" 551 + 552 + let test_dyn_roundtrip () = 553 + let config = 554 + Sa.config ~iv_len:12 ~sn_len:4 ~arsnw:128 ~spi:1 ~scid:3 ~vcid:0 () 555 + in 556 + let dyn = 557 + Sa.dyn ~lifecycle:Sa.Operational ~ek_id:(Keyid.of_int_exn 0x1000) 558 + ~ak_id:(Keyid.of_int_exn 0x2000) ~config () 559 + in 560 + (* Modify IV and ARSN to non-zero values *) 561 + let dyn = 562 + { dyn with iv = hex "112233445566778899aabbcc"; arsn = hex "00001234" } 563 + in 564 + let w = Binary.Writer.create 256 in 565 + Sa.write_dyn w dyn; 566 + let encoded = Binary.Writer.contents w in 567 + match Sa.read_dyn (Binary.Reader.of_bytes encoded) with 568 + | Some dyn' -> 569 + Alcotest.(check bool) 570 + "lifecycle" 571 + (dyn.lifecycle = Sa.Operational) 572 + (dyn'.lifecycle = Sa.Operational); 573 + Alcotest.(check int) 574 + "ek_id" (Keyid.to_int dyn.ek_id) (Keyid.to_int dyn'.ek_id); 575 + Alcotest.(check int) 576 + "ak_id" (Keyid.to_int dyn.ak_id) (Keyid.to_int dyn'.ak_id); 577 + Alcotest.(check string) "iv" (Hex.encode dyn.iv) (Hex.encode dyn'.iv); 578 + Alcotest.(check string) 579 + "arsn" (Hex.encode dyn.arsn) (Hex.encode dyn'.arsn); 580 + Alcotest.(check int) 581 + "replay_window size" 582 + (Bitv.length dyn.replay_window) 583 + (Bitv.length dyn'.replay_window) 584 + | None -> Alcotest.fail "dyn deserialization failed" 585 + 586 + let test_dyn_roundtrip_with_replay_window () = 587 + let config = 588 + Sa.config ~iv_len:12 ~sn_len:4 ~arsnw:64 ~spi:1 ~scid:3 ~vcid:0 () 589 + in 590 + let dyn = Sa.dyn ~lifecycle:Sa.Operational ~config () in 591 + (* Set some bits in the replay window *) 592 + Bitv.set dyn.replay_window 0 true; 593 + Bitv.set dyn.replay_window 5 true; 594 + Bitv.set dyn.replay_window 63 true; 595 + let w = Binary.Writer.create 256 in 596 + Sa.write_dyn w dyn; 597 + let encoded = Binary.Writer.contents w in 598 + match Sa.read_dyn (Binary.Reader.of_bytes encoded) with 599 + | Some dyn' -> 600 + Alcotest.(check bool) "bit 0" true (Bitv.get dyn'.replay_window 0); 601 + Alcotest.(check bool) "bit 5" true (Bitv.get dyn'.replay_window 5); 602 + Alcotest.(check bool) "bit 63" true (Bitv.get dyn'.replay_window 63); 603 + Alcotest.(check bool) "bit 1" false (Bitv.get dyn'.replay_window 1); 604 + Alcotest.(check bool) "bit 62" false (Bitv.get dyn'.replay_window 62) 605 + | None -> Alcotest.fail "dyn deserialization failed" 606 + 607 + let test_dyn_roundtrip_all_states () = 608 + let config = 609 + Sa.config ~iv_len:8 ~sn_len:2 ~arsnw:32 ~spi:1 ~scid:3 ~vcid:0 () 610 + in 611 + let states = [ Sa.Disabled; Sa.Unkeyed; Sa.Keyed; Sa.Operational ] in 612 + List.iter 613 + (fun state -> 614 + let dyn = Sa.dyn ~lifecycle:state ~config () in 615 + let w = Binary.Writer.create 256 in 616 + Sa.write_dyn w dyn; 617 + let encoded = Binary.Writer.contents w in 618 + match Sa.read_dyn (Binary.Reader.of_bytes encoded) with 619 + | Some dyn' -> 620 + Alcotest.(check bool) 621 + (Format.asprintf "state %a" Sa.pp_state state) 622 + true (dyn'.lifecycle = state) 623 + | None -> Alcotest.fail "dyn deserialization failed") 624 + states 625 + 626 + (** {1 SA Config Validation Tests} *) 627 + 628 + let test_config_aead_requires_auth () = 629 + (* AEAD cipher (GCM) with authentication=false should be rejected *) 630 + match 631 + Sa.config_result ~encryption:true ~authentication:false 632 + ~ecs:(Some Sa.AES_256_GCM) ~spi:1 ~scid:3 ~vcid:0 () 633 + with 634 + | Error (Sa.Aead_requires_authentication _) -> () 635 + | Error _ -> Alcotest.fail "wrong error type" 636 + | Ok _ -> Alcotest.fail "should reject GCM with authentication=false" 637 + 638 + let test_config_ccm_requires_auth () = 639 + (* AEAD cipher (CCM) with authentication=false should be rejected *) 640 + match 641 + Sa.config_result ~encryption:true ~authentication:false 642 + ~ecs:(Some Sa.AES_256_CCM) ~spi:1 ~scid:3 ~vcid:0 () 643 + with 644 + | Error (Sa.Aead_requires_authentication _) -> () 645 + | Error _ -> Alcotest.fail "wrong error type" 646 + | Ok _ -> Alcotest.fail "should reject CCM with authentication=false" 647 + 648 + let test_config_encryption_needs_cipher () = 649 + (* encryption=true with ecs=None should be rejected *) 650 + match 651 + Sa.config_result ~encryption:true ~authentication:true ~ecs:None ~spi:1 652 + ~scid:3 ~vcid:0 () 653 + with 654 + | Error Sa.Encryption_without_cipher -> () 655 + | Error _ -> Alcotest.fail "wrong error type" 656 + | Ok _ -> Alcotest.fail "should reject encryption=true without cipher" 657 + 658 + let test_config_auth_only_needs_acs () = 659 + (* authentication=true, encryption=false without acs should be rejected *) 660 + match 661 + Sa.config_result ~encryption:false ~authentication:true ~ecs:None ~acs:None 662 + ~spi:1 ~scid:3 ~vcid:0 () 663 + with 664 + | Error Sa.Auth_only_requires_acs -> () 665 + | Error _ -> Alcotest.fail "wrong error type" 666 + | Ok _ -> Alcotest.fail "should reject auth-only without acs" 667 + 668 + let test_config_valid_gcm () = 669 + (* Valid GCM config should succeed *) 670 + match 671 + Sa.config_result ~encryption:true ~authentication:true 672 + ~ecs:(Some Sa.AES_256_GCM) ~spi:1 ~scid:3 ~vcid:0 () 673 + with 674 + | Ok _ -> () 675 + | Error e -> 676 + Alcotest.fail 677 + (Format.asprintf "should accept valid GCM config: %a" Sa.pp_config_error 678 + e) 679 + 680 + let test_config_valid_auth_only () = 681 + (* Valid auth-only config should succeed *) 682 + match 683 + Sa.config_result ~encryption:false ~authentication:true ~ecs:None 684 + ~acs:(Some Sa.AES_256_CMAC) ~spi:1 ~scid:3 ~vcid:0 () 685 + with 686 + | Ok _ -> () 687 + | Error e -> 688 + Alcotest.fail 689 + (Format.asprintf "should accept valid auth-only config: %a" 690 + Sa.pp_config_error e) 691 + 692 + let test_config_raises_on_invalid () = 693 + (* The non-result config should raise Invalid_argument for invalid config *) 694 + try 695 + let _ = 696 + Sa.config ~encryption:true ~authentication:false 697 + ~ecs:(Some Sa.AES_256_GCM) ~spi:1 ~scid:3 ~vcid:0 () 698 + in 699 + Alcotest.fail "should raise Invalid_argument" 700 + with Invalid_argument _ -> () 701 + 702 + (** {1 Test Suite} *) 703 + 704 + let suite = 705 + ( "sa", 706 + [ 707 + Alcotest.test_case "gcm_only" `Quick test_sa_gcm_only; 708 + Alcotest.test_case "auth_only" `Quick test_sa_auth_only; 709 + Alcotest.test_case "increment_iv" `Quick test_increment_iv; 710 + Alcotest.test_case "increment_iv wraparound" `Quick 711 + test_increment_iv_wraparound; 712 + Alcotest.test_case "increment_iv full wraparound" `Quick 713 + test_increment_iv_full_wraparound; 714 + Alcotest.test_case "is_iv_max" `Quick test_is_iv_max; 715 + Alcotest.test_case "increment_iv_safe" `Quick test_increment_iv_safe; 716 + Alcotest.test_case "cmp_be unequal lengths" `Quick 717 + test_cmp_be_unequal_lengths; 718 + Alcotest.test_case "diff_be unequal lengths" `Quick 719 + test_diff_be_unequal_lengths; 720 + Alcotest.test_case "diff_be_bounded high order" `Quick 721 + test_diff_be_bounded_high_order; 722 + Alcotest.test_case "diff_be_bounded small" `Quick 723 + test_diff_be_bounded_small; 724 + Alcotest.test_case "diff_be_bounded negative" `Quick 725 + test_diff_be_bounded_negative; 726 + Alcotest.test_case "diff_be_bounded various lengths" `Quick 727 + test_diff_be_bounded_various_lengths; 728 + Alcotest.test_case "anti-replay accept newer" `Quick 729 + test_anti_replay_accept_newer; 730 + Alcotest.test_case "anti-replay reject duplicate" `Quick 731 + test_anti_replay_reject_duplicate; 732 + Alcotest.test_case "anti-replay window" `Quick test_anti_replay_window; 733 + Alcotest.test_case "anti-replay disabled when sn_len=0" `Quick 734 + test_anti_replay_disabled_when_sn_len_zero; 735 + Alcotest.test_case "anti-replay wraparound" `Quick 736 + test_anti_replay_wraparound; 737 + Alcotest.test_case "anti-replay big delta" `Quick 738 + test_anti_replay_big_delta; 739 + Alcotest.test_case "sn_len bounds" `Quick test_sn_len_bounds; 740 + Alcotest.test_case "arsnw bounds" `Quick test_arsnw_bounds; 741 + Alcotest.test_case "SA start" `Quick test_sa_start; 742 + Alcotest.test_case "SA stop" `Quick test_sa_stop; 743 + Alcotest.test_case "SA rekey" `Quick test_sa_rekey; 744 + Alcotest.test_case "SA expire" `Quick test_sa_expire; 745 + Alcotest.test_case "SA status" `Quick test_sa_status; 746 + Alcotest.test_case "SA set_arsn" `Quick test_sa_set_arsn; 747 + Alcotest.test_case "SA set_arsnw" `Quick test_sa_set_arsnw; 748 + (* PDU wire format tests *) 749 + Alcotest.test_case "delete_cmd roundtrip" `Quick test_delete_cmd_roundtrip; 750 + Alcotest.test_case "set_arsn_cmd roundtrip" `Quick 751 + test_set_arsn_cmd_roundtrip; 752 + Alcotest.test_case "set_arsnw_cmd roundtrip" `Quick 753 + test_set_arsnw_cmd_roundtrip; 754 + Alcotest.test_case "set_arsnw_cmd max window" `Quick 755 + test_set_arsnw_cmd_max_window; 756 + Alcotest.test_case "rekey_cmd roundtrip" `Quick test_rekey_cmd_roundtrip; 757 + Alcotest.test_case "expire_cmd roundtrip" `Quick test_expire_cmd_roundtrip; 758 + Alcotest.test_case "read_arsn_reply roundtrip" `Quick 759 + test_read_arsn_reply_roundtrip; 760 + Alcotest.test_case "status_reply roundtrip" `Quick 761 + test_status_reply_roundtrip; 762 + Alcotest.test_case "status_reply no crypto" `Quick 763 + test_status_reply_no_crypto; 764 + Alcotest.test_case "create_cmd roundtrip" `Quick test_create_cmd_roundtrip; 765 + Alcotest.test_case "create_cmd with ABM" `Quick test_create_cmd_with_abm; 766 + (* Anti-replay overflow tests *) 767 + Alcotest.test_case "index_of_sn large SN" `Quick test_index_of_sn_large_sn; 768 + (* Config/Dyn persistence serialization tests *) 769 + Alcotest.test_case "config roundtrip" `Quick test_config_roundtrip; 770 + Alcotest.test_case "config roundtrip with ABM mask" `Quick 771 + test_config_roundtrip_with_abm_mask; 772 + Alcotest.test_case "config roundtrip no crypto" `Quick 773 + test_config_roundtrip_no_crypto; 774 + Alcotest.test_case "dyn roundtrip" `Quick test_dyn_roundtrip; 775 + Alcotest.test_case "dyn roundtrip with replay window" `Quick 776 + test_dyn_roundtrip_with_replay_window; 777 + Alcotest.test_case "dyn roundtrip all states" `Quick 778 + test_dyn_roundtrip_all_states; 779 + (* Config validation tests *) 780 + Alcotest.test_case "config AEAD requires auth" `Quick 781 + test_config_aead_requires_auth; 782 + Alcotest.test_case "config CCM requires auth" `Quick 783 + test_config_ccm_requires_auth; 784 + Alcotest.test_case "config encryption needs cipher" `Quick 785 + test_config_encryption_needs_cipher; 786 + Alcotest.test_case "config auth-only needs acs" `Quick 787 + test_config_auth_only_needs_acs; 788 + Alcotest.test_case "config valid GCM" `Quick test_config_valid_gcm; 789 + Alcotest.test_case "config valid auth-only" `Quick 790 + test_config_valid_auth_only; 791 + Alcotest.test_case "config raises on invalid" `Quick 792 + test_config_raises_on_invalid; 793 + ] )
+5
test/test_sdls.ml
··· 5 5 ("mc", Test_mc.tests); 6 6 Test_cmac.suite; 7 7 Test_hmac.suite; 8 + Test_crypto.suite; 9 + Test_sa.suite; 10 + Test_keystore.suite; 11 + Test_key.suite; 12 + Test_security.suite; 8 13 ]
+346
test/test_security.ml
··· 1 + (** Tests for Security module - Security Log and events. *) 2 + 3 + open Sdls 4 + 5 + (* {1 Test Helpers} *) 6 + 7 + (** Create a store, log an event, and return it for testing. *) 8 + let create_event_via_store log_fn = 9 + let store = Security.in_memory () in 10 + log_fn store; 11 + match Security.dump store with 12 + | [ e ] -> e 13 + | _ -> failwith "expected exactly one event" 14 + 15 + (* {1 Event Encoding/Decoding Tests} *) 16 + 17 + let test_auth_failure_roundtrip () = 18 + let event = 19 + create_event_via_store (fun s -> 20 + Security.auth_failure s ~timestamp:12345L ~spi:1 ~reason:Bad_mac ~vcid:0 21 + ()) 22 + in 23 + let w = Binary.Writer.create 64 in 24 + Security.write_event w event; 25 + let buf = Binary.Writer.contents w in 26 + let r = Binary.Reader.of_bytes buf in 27 + match Security.read_event r with 28 + | Ok event' -> ( 29 + Alcotest.(check int64) 30 + "timestamp" 12345L 31 + (Security.event_timestamp event'); 32 + match Security.event_data event' with 33 + | Security.Auth_failure { spi; reason; vcid } -> 34 + Alcotest.(check int) "spi" 1 spi; 35 + Alcotest.(check bool) "reason=Bad_mac" true (reason = Security.Bad_mac); 36 + Alcotest.(check bool) "vcid present" true (Option.is_some vcid) 37 + | _ -> Alcotest.fail "wrong event type") 38 + | Error _ -> Alcotest.fail "read_event failed" 39 + 40 + let test_sa_change_roundtrip () = 41 + let event = 42 + create_event_via_store (fun s -> 43 + Security.sa_change s ~timestamp:99999L ~spi:2 ~transition:Sa_started) 44 + in 45 + let w = Binary.Writer.create 64 in 46 + Security.write_event w event; 47 + let buf = Binary.Writer.contents w in 48 + let r = Binary.Reader.of_bytes buf in 49 + match Security.read_event r with 50 + | Ok event' -> ( 51 + Alcotest.(check int64) 52 + "timestamp" 99999L 53 + (Security.event_timestamp event'); 54 + match Security.event_data event' with 55 + | Security.Sa_change { spi; transition } -> 56 + Alcotest.(check int) "spi" 2 spi; 57 + Alcotest.(check bool) 58 + "transition=Sa_started" true (transition = Sa_started) 59 + | _ -> Alcotest.fail "wrong event type") 60 + | Error _ -> Alcotest.fail "read_event failed" 61 + 62 + let test_key_change_roundtrip () = 63 + let event = 64 + create_event_via_store (fun s -> 65 + Security.key_change s ~timestamp:1000L ~kid:0x0100 66 + ~transition:Key_activated) 67 + in 68 + let w = Binary.Writer.create 64 in 69 + Security.write_event w event; 70 + let buf = Binary.Writer.contents w in 71 + let r = Binary.Reader.of_bytes buf in 72 + match Security.read_event r with 73 + | Ok event' -> ( 74 + match Security.event_data event' with 75 + | Security.Key_change { kid; transition } -> 76 + Alcotest.(check int) "kid" 0x0100 kid; 77 + Alcotest.(check bool) 78 + "transition=Key_activated" true 79 + (transition = Key_activated) 80 + | _ -> Alcotest.fail "wrong event type") 81 + | Error _ -> Alcotest.fail "read_event failed" 82 + 83 + let test_alarm_reset_roundtrip () = 84 + let store = Security.in_memory () in 85 + Security.set_alarm store; 86 + Security.alarm_reset store ~timestamp:500L; 87 + let events = Security.dump store in 88 + (* alarm_reset only logs if alarm was set *) 89 + let event = List.hd events in 90 + let w = Binary.Writer.create 64 in 91 + Security.write_event w event; 92 + let buf = Binary.Writer.contents w in 93 + let r = Binary.Reader.of_bytes buf in 94 + match Security.read_event r with 95 + | Ok event' -> ( 96 + match Security.event_data event' with 97 + | Security.Alarm_reset -> () 98 + | _ -> Alcotest.fail "wrong event type") 99 + | Error _ -> Alcotest.fail "read_event failed" 100 + 101 + let test_self_test_roundtrip () = 102 + let event = 103 + create_event_via_store (fun s -> 104 + Security.self_test s ~timestamp:700L ~success:false) 105 + in 106 + let w = Binary.Writer.create 64 in 107 + Security.write_event w event; 108 + let buf = Binary.Writer.contents w in 109 + let r = Binary.Reader.of_bytes buf in 110 + match Security.read_event r with 111 + | Ok event' -> ( 112 + match Security.event_data event' with 113 + | Security.Self_test { success } -> 114 + Alcotest.(check bool) "success" false success 115 + | _ -> Alcotest.fail "wrong event type") 116 + | Error _ -> Alcotest.fail "read_event failed" 117 + 118 + let test_log_erased_roundtrip () = 119 + let store = Security.in_memory () in 120 + Security.erase store ~timestamp:999L; 121 + let event = List.hd (Security.dump store) in 122 + let w = Binary.Writer.create 64 in 123 + Security.write_event w event; 124 + let buf = Binary.Writer.contents w in 125 + let r = Binary.Reader.of_bytes buf in 126 + match Security.read_event r with 127 + | Ok event' -> ( 128 + match Security.event_data event' with 129 + | Security.Log_erased -> () 130 + | _ -> Alcotest.fail "wrong event type") 131 + | Error _ -> Alcotest.fail "read_event failed" 132 + 133 + (* {1 Store Tests} *) 134 + 135 + let test_store_append_and_count () = 136 + let store = Security.in_memory ~max_events:10 () in 137 + Alcotest.(check int) "initial count" 0 (Security.count store); 138 + Security.alarm_reset store ~timestamp:1L; 139 + (* No alarm set, so no event logged *) 140 + Alcotest.(check int) 141 + "count after alarm_reset (no alarm)" 0 (Security.count store); 142 + Security.self_test store ~timestamp:2L ~success:true; 143 + Alcotest.(check int) "count after self_test" 1 (Security.count store); 144 + Security.sa_change store ~timestamp:3L ~spi:1 ~transition:Sa_created; 145 + Alcotest.(check int) "count after sa_change" 2 (Security.count store) 146 + 147 + let test_store_dump () = 148 + let store = Security.in_memory ~max_events:10 () in 149 + Security.self_test store ~timestamp:1L ~success:true; 150 + Security.sa_change store ~timestamp:2L ~spi:1 ~transition:Sa_deleted; 151 + let events = Security.dump store in 152 + Alcotest.(check int) "dump length" 2 (List.length events); 153 + let e0 = List.nth events 0 in 154 + let e1 = List.nth events 1 in 155 + Alcotest.(check int64) "e0 timestamp" 1L (Security.event_timestamp e0); 156 + Alcotest.(check int64) "e1 timestamp" 2L (Security.event_timestamp e1) 157 + 158 + let test_store_erase () = 159 + let store = Security.in_memory ~max_events:10 () in 160 + Security.self_test store ~timestamp:1L ~success:true; 161 + Security.sa_change store ~timestamp:2L ~spi:1 ~transition:Sa_deleted; 162 + Alcotest.(check int) "count before erase" 2 (Security.count store); 163 + Security.erase store ~timestamp:3L; 164 + (* erase adds Log_erased event *) 165 + Alcotest.(check int) "count after erase" 1 (Security.count store); 166 + match Security.event_data (List.hd (Security.dump store)) with 167 + | Security.Log_erased -> () 168 + | _ -> Alcotest.fail "expected Log_erased event" 169 + 170 + let test_store_capacity () = 171 + let store = Security.in_memory ~max_events:5 () in 172 + let initial_cap = Security.capacity store in 173 + Alcotest.(check bool) "initial capacity > 0" true (initial_cap > 0); 174 + Security.self_test store ~timestamp:1L ~success:true; 175 + let cap_after = Security.capacity store in 176 + Alcotest.(check bool) "capacity decreases" true (cap_after < initial_cap) 177 + 178 + let test_store_max_events () = 179 + let store = Security.in_memory ~max_events:3 () in 180 + Security.self_test store ~timestamp:1L ~success:true; 181 + Security.self_test store ~timestamp:2L ~success:true; 182 + Security.self_test store ~timestamp:3L ~success:true; 183 + Alcotest.(check int) "count at max" 3 (Security.count store); 184 + (* Append beyond max should not increase count *) 185 + Security.self_test store ~timestamp:4L ~success:true; 186 + Alcotest.(check int) "count still at max" 3 (Security.count store) 187 + 188 + (* {1 Alarm Tests} *) 189 + 190 + let test_alarm_default () = 191 + let store = Security.in_memory () in 192 + Alcotest.(check bool) "default alarm" false (Security.get_alarm store) 193 + 194 + let test_alarm_set () = 195 + let store = Security.in_memory () in 196 + Security.set_alarm store; 197 + Alcotest.(check bool) "alarm after set" true (Security.get_alarm store) 198 + 199 + let test_alarm_reset () = 200 + let store = Security.in_memory () in 201 + Security.set_alarm store; 202 + Security.alarm_reset store ~timestamp:100L; 203 + Alcotest.(check bool) "alarm after reset" false (Security.get_alarm store); 204 + (* Check that Alarm_reset event was logged *) 205 + match Security.dump store with 206 + | [ e ] -> ( 207 + match Security.event_data e with 208 + | Security.Alarm_reset -> () 209 + | _ -> Alcotest.fail "expected Alarm_reset event") 210 + | _ -> Alcotest.fail "expected exactly one event" 211 + 212 + let test_auth_failure_sets_alarm () = 213 + let store = Security.in_memory () in 214 + Alcotest.(check bool) "alarm before" false (Security.get_alarm store); 215 + Security.auth_failure store ~timestamp:1L ~spi:1 ~reason:Bad_mac (); 216 + Alcotest.(check bool) 217 + "alarm after auth_failure" true (Security.get_alarm store) 218 + 219 + (* {1 Invalid Input Tests} *) 220 + 221 + let test_invalid_vcid_returns_error () = 222 + (* Construct a malformed Auth_failure event with VCID=255 (invalid: max is 63) *) 223 + let buf = Bytes.create 20 in 224 + (* Timestamp: 8 bytes *) 225 + Bytes.set_int64_be buf 0 12345L; 226 + (* Tag: auth_failure = 0x01 *) 227 + Bytes.set_uint8 buf 8 0x01; 228 + (* Length: 4 bytes (SPI + reason + vcid) *) 229 + Bytes.set_uint16_be buf 9 4; 230 + (* SPI: 0x0001 *) 231 + Bytes.set_uint16_be buf 11 0x0001; 232 + (* Reason byte: 0x80 = has_vcid flag set, reason=0 (Bad_mac) *) 233 + Bytes.set_uint8 buf 13 0x80; 234 + (* VCID: 255 (invalid - max is 63) *) 235 + Bytes.set_uint8 buf 14 255; 236 + let r = Binary.Reader.of_bytes buf in 237 + match Security.read_event r with 238 + | Error (`Invalid_vcid 255) -> () 239 + | Error (`Invalid_vcid v) -> 240 + Alcotest.fail (Printf.sprintf "wrong vcid value: %d" v) 241 + | Error `Truncated -> Alcotest.fail "unexpected truncated error" 242 + | Error (`Invalid_tag t) -> 243 + Alcotest.fail (Printf.sprintf "unexpected invalid tag: %d" t) 244 + | Ok _ -> Alcotest.fail "should reject invalid VCID" 245 + 246 + let test_valid_vcid_63_accepted () = 247 + (* Construct an Auth_failure event with VCID=63 (max valid value) *) 248 + let buf = Bytes.create 20 in 249 + Bytes.set_int64_be buf 0 12345L; 250 + Bytes.set_uint8 buf 8 0x01; 251 + Bytes.set_uint16_be buf 9 4; 252 + Bytes.set_uint16_be buf 11 0x0001; 253 + Bytes.set_uint8 buf 13 0x80; 254 + Bytes.set_uint8 buf 14 63; 255 + let r = Binary.Reader.of_bytes buf in 256 + match Security.read_event r with 257 + | Ok event -> ( 258 + match Security.event_data event with 259 + | Security.Auth_failure { vcid = Some v; _ } -> 260 + Alcotest.(check int) "vcid" 63 v 261 + | Security.Auth_failure { vcid = None; _ } -> 262 + Alcotest.fail "vcid should be present" 263 + | _ -> Alcotest.fail "wrong event type") 264 + | Error _ -> Alcotest.fail "should accept valid VCID 63" 265 + 266 + (* {1 Event Tag Tests} *) 267 + 268 + let test_event_tags () = 269 + Alcotest.(check int) "tag_auth_failure" 0x01 Security.tag_auth_failure; 270 + Alcotest.(check int) "tag_sa_change" 0x02 Security.tag_sa_change; 271 + Alcotest.(check int) "tag_key_change" 0x03 Security.tag_key_change; 272 + Alcotest.(check int) "tag_alarm_reset" 0x04 Security.tag_alarm_reset; 273 + Alcotest.(check int) "tag_self_test" 0x05 Security.tag_self_test; 274 + Alcotest.(check int) "tag_log_erased" 0x06 Security.tag_log_erased 275 + 276 + (* {1 Pretty-Printing Tests} *) 277 + 278 + let test_pp_auth_failure_reason () = 279 + let s = Format.asprintf "%a" Security.pp_auth_failure_reason Bad_mac in 280 + Alcotest.(check bool) "non-empty" true (String.length s > 0) 281 + 282 + let test_pp_sa_transition () = 283 + let s = Format.asprintf "%a" Security.pp_sa_transition Sa_created in 284 + Alcotest.(check bool) "non-empty" true (String.length s > 0) 285 + 286 + let test_pp_key_transition () = 287 + let s = Format.asprintf "%a" Security.pp_key_transition Key_received in 288 + Alcotest.(check bool) "non-empty" true (String.length s > 0) 289 + 290 + let test_pp_event_data () = 291 + let s = 292 + Format.asprintf "%a" Security.pp_event_data 293 + (Security.Auth_failure { spi = 1; reason = Bad_mac; vcid = None }) 294 + in 295 + Alcotest.(check bool) "non-empty" true (String.length s > 0) 296 + 297 + let test_pp_event () = 298 + let event = 299 + create_event_via_store (fun s -> 300 + Security.self_test s ~timestamp:100L ~success:true) 301 + in 302 + let s = Format.asprintf "%a" Security.pp_event event in 303 + Alcotest.(check bool) "non-empty" true (String.length s > 0) 304 + 305 + (* {1 Test Suite} *) 306 + 307 + let suite = 308 + ( "security", 309 + [ 310 + (* Event encoding/decoding *) 311 + Alcotest.test_case "auth_failure roundtrip" `Quick 312 + test_auth_failure_roundtrip; 313 + Alcotest.test_case "sa_change roundtrip" `Quick test_sa_change_roundtrip; 314 + Alcotest.test_case "key_change roundtrip" `Quick test_key_change_roundtrip; 315 + Alcotest.test_case "alarm_reset roundtrip" `Quick 316 + test_alarm_reset_roundtrip; 317 + Alcotest.test_case "self_test roundtrip" `Quick test_self_test_roundtrip; 318 + Alcotest.test_case "log_erased roundtrip" `Quick test_log_erased_roundtrip; 319 + (* Store operations *) 320 + Alcotest.test_case "store append and count" `Quick 321 + test_store_append_and_count; 322 + Alcotest.test_case "store dump" `Quick test_store_dump; 323 + Alcotest.test_case "store erase" `Quick test_store_erase; 324 + Alcotest.test_case "store capacity" `Quick test_store_capacity; 325 + Alcotest.test_case "store max events" `Quick test_store_max_events; 326 + (* Alarm operations *) 327 + Alcotest.test_case "alarm default" `Quick test_alarm_default; 328 + Alcotest.test_case "alarm set" `Quick test_alarm_set; 329 + Alcotest.test_case "alarm reset" `Quick test_alarm_reset; 330 + Alcotest.test_case "auth_failure sets alarm" `Quick 331 + test_auth_failure_sets_alarm; 332 + (* Invalid input handling *) 333 + Alcotest.test_case "invalid VCID returns error" `Quick 334 + test_invalid_vcid_returns_error; 335 + Alcotest.test_case "valid VCID 63 accepted" `Quick 336 + test_valid_vcid_63_accepted; 337 + (* Event tags *) 338 + Alcotest.test_case "event tags" `Quick test_event_tags; 339 + (* Pretty-printing *) 340 + Alcotest.test_case "pp_auth_failure_reason" `Quick 341 + test_pp_auth_failure_reason; 342 + Alcotest.test_case "pp_sa_transition" `Quick test_pp_sa_transition; 343 + Alcotest.test_case "pp_key_transition" `Quick test_pp_key_transition; 344 + Alcotest.test_case "pp_event_data" `Quick test_pp_event_data; 345 + Alcotest.test_case "pp_event" `Quick test_pp_event; 346 + ] )