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 8 new CCSDS/RFC protocol packages

- ocaml-rice: CCSDS 121.0-B lossless compression (Rice/Golomb)
- ocaml-udpcl: RFC 7122 UDP convergence layer for Bundle Protocol
- ocaml-erasure: CCSDS 131.5-B erasure correcting codes (GF(2^8))
- ocaml-short-ldpc: CCSDS 131.4-B short block-length LDPC
- ocaml-opm: CCSDS 502.0-B Orbit Parameter Message (KVN)
- ocaml-aem: CCSDS 504.0-B Attitude Ephemeris Message (KVN)
- ocaml-tdm: CCSDS 503.0-B Tracking Data Message (KVN)
- ocaml-rdm: CCSDS 508.1-B Re-entry Data Message (KVN)

+252
+251
test/test_frame.ml
··· 1 + (** End-to-end frame protection integration tests for protect_frame / 2 + unprotect_frame. 3 + 4 + These tests exercise the full SDLS pipeline: SA setup, keystore 5 + provisioning, frame protection, and unprotection -- covering AES-256-GCM 6 + (authenticated encryption) and HMAC-SHA-256 (authentication only). *) 7 + 8 + open Sdls 9 + 10 + (** {1 Helpers} *) 11 + 12 + let fail_error msg e = Alcotest.fail (Format.asprintf "%s: %a" msg pp_error e) 13 + 14 + (** Create an in-memory keystore with a 256-bit key at [id]. *) 15 + let make_keystore ~id key_bytes = 16 + let ks = Keystore.in_memory () in 17 + Keystore.add ks (Keyid.of_int_exn id) key_bytes; 18 + ignore (Keystore.activate ks (Keyid.of_int_exn id)); 19 + ks 20 + 21 + (** A fixed 256-bit (32-byte) key. *) 22 + let key_256 = 23 + Bytes.of_string 24 + "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f" 25 + 26 + (** A fake 6-byte frame header (e.g. a TM/TC primary header). *) 27 + let frame_hdr = Bytes.of_string "\xAA\xBB\xCC\xDD\xEE\xFF" 28 + 29 + let frame_hdr_len = Bytes.length frame_hdr 30 + 31 + (** Protect a frame, returning [(protected_bytes, updated_sa)]. *) 32 + let do_protect ~sa ~keys plaintext = 33 + let w = Binary.Writer.create 1024 in 34 + match 35 + protect_frame ~sa ~keys ~get_ek:Keystore.get_encryption_key 36 + ~get_ak:Keystore.get_auth_key ~frame_hdr_bytes:frame_hdr ~plaintext w 37 + with 38 + | Error e -> fail_error "protect_frame failed" e 39 + | Ok sa' -> (Binary.Writer.contents w, sa') 40 + 41 + (** Unprotect a protected frame, returning [(plaintext, updated_sa)]. *) 42 + let do_unprotect ~sa ~keys protected = 43 + let total_len = Bytes.length protected in 44 + let r = Binary.Reader.of_bytes ~off:frame_hdr_len protected in 45 + match 46 + unprotect_frame ~sa ~keys ~get_ek:Keystore.get_encryption_key 47 + ~get_ak:Keystore.get_auth_key ~frame_hdr_len ~frame_hdr_bytes:frame_hdr 48 + ~total_len r 49 + with 50 + | Error e -> fail_error "unprotect_frame failed" e 51 + | Ok (pt, sa') -> (pt, sa') 52 + 53 + (** {1 AES-256-GCM Tests} *) 54 + 55 + let test_gcm_roundtrip () = 56 + let keys = make_keystore ~id:1 key_256 in 57 + let sa = 58 + Sa.v ~encryption:true ~authentication:true ~ecs:(Some Sa.AES_256_GCM) 59 + ~iv_len:12 ~mac_len:16 ~sn_len:0 ~ek_id:(Keyid.of_int_exn 1) ~spi:1 60 + ~scid:0 ~vcid:0 () 61 + in 62 + let plaintext = Bytes.of_string "Hello, CCSDS!" in 63 + let protected, _sa' = do_protect ~sa ~keys plaintext in 64 + (* Security header: 2 (SPI) + 12 (IV) + 0 (SN) = 14 bytes *) 65 + let sec_hdr_len = 2 + 12 in 66 + let expected_len = 67 + frame_hdr_len + sec_hdr_len + Bytes.length plaintext + 16 68 + in 69 + Alcotest.(check int) 70 + "protected frame length" expected_len (Bytes.length protected); 71 + (* Check frame header preserved *) 72 + Alcotest.(check bytes) 73 + "frame header" frame_hdr 74 + (Bytes.sub protected 0 frame_hdr_len); 75 + (* Check SPI in security header *) 76 + let spi_val = Bytes.get_uint16_be protected frame_hdr_len in 77 + Alcotest.(check int) "SPI" 1 spi_val; 78 + (* Unprotect and verify plaintext recovery *) 79 + let recovered, _sa'' = do_unprotect ~sa ~keys protected in 80 + Alcotest.(check bytes) "recovered plaintext" plaintext recovered 81 + 82 + let test_gcm_ciphertext_differs () = 83 + let keys = make_keystore ~id:1 key_256 in 84 + let sa = 85 + Sa.v ~encryption:true ~authentication:true ~ecs:(Some Sa.AES_256_GCM) 86 + ~iv_len:12 ~mac_len:16 ~sn_len:0 ~ek_id:(Keyid.of_int_exn 1) ~spi:1 87 + ~scid:0 ~vcid:0 () 88 + in 89 + let plaintext = Bytes.of_string "Secret payload" in 90 + let protected, _sa' = do_protect ~sa ~keys plaintext in 91 + (* The ciphertext portion must differ from plaintext *) 92 + let sec_hdr_len = 2 + 12 in 93 + let ct_offset = frame_hdr_len + sec_hdr_len in 94 + let ct = Bytes.sub protected ct_offset (Bytes.length plaintext) in 95 + Alcotest.(check bool) 96 + "ciphertext differs from plaintext" true 97 + (not (Bytes.equal ct plaintext)) 98 + 99 + (** {1 HMAC-SHA-256 Authentication-Only Tests} *) 100 + 101 + let test_hmac_sha256_auth_only_roundtrip () = 102 + let keys = make_keystore ~id:2 key_256 in 103 + let sa = 104 + Sa.v ~encryption:false ~authentication:true ~ecs:None 105 + ~acs:(Some Sa.HMAC_SHA_256) ~iv_len:0 ~mac_len:16 ~sn_len:0 106 + ~ak_id:(Keyid.of_int_exn 2) ~spi:2 ~scid:0 ~vcid:0 () 107 + in 108 + let plaintext = Bytes.of_string "Authenticated payload" in 109 + let protected, _sa' = do_protect ~sa ~keys plaintext in 110 + (* Security header: 2 (SPI) + 0 (IV) + 0 (SN) = 2 bytes *) 111 + let sec_hdr_len = 2 in 112 + let expected_len = 113 + frame_hdr_len + sec_hdr_len + Bytes.length plaintext + 16 114 + in 115 + Alcotest.(check int) 116 + "protected frame length" expected_len (Bytes.length protected); 117 + (* Auth-only: plaintext is in the clear *) 118 + let data_offset = frame_hdr_len + sec_hdr_len in 119 + let data_in_frame = 120 + Bytes.sub protected data_offset (Bytes.length plaintext) 121 + in 122 + Alcotest.(check bytes) "plaintext in the clear" plaintext data_in_frame; 123 + (* Unprotect and verify *) 124 + let recovered, _sa'' = do_unprotect ~sa ~keys protected in 125 + Alcotest.(check bytes) "recovered plaintext" plaintext recovered 126 + 127 + (** {1 Tamper Detection Tests} *) 128 + 129 + let test_gcm_mac_tamper () = 130 + let keys = make_keystore ~id:1 key_256 in 131 + let sa = 132 + Sa.v ~encryption:true ~authentication:true ~ecs:(Some Sa.AES_256_GCM) 133 + ~iv_len:12 ~mac_len:16 ~sn_len:0 ~ek_id:(Keyid.of_int_exn 1) ~spi:1 134 + ~scid:0 ~vcid:0 () 135 + in 136 + let plaintext = Bytes.of_string "tamper test" in 137 + let protected, _sa' = do_protect ~sa ~keys plaintext in 138 + (* Flip a bit in the MAC (last 16 bytes) *) 139 + let corrupted = Bytes.copy protected in 140 + let last = Bytes.length corrupted - 1 in 141 + Bytes.set corrupted last 142 + (Char.chr (Char.code (Bytes.get corrupted last) lxor 0xFF)); 143 + let total_len = Bytes.length corrupted in 144 + let r = Binary.Reader.of_bytes ~off:frame_hdr_len corrupted in 145 + match 146 + unprotect_frame ~sa ~keys ~get_ek:Keystore.get_encryption_key 147 + ~get_ak:Keystore.get_auth_key ~frame_hdr_len ~frame_hdr_bytes:frame_hdr 148 + ~total_len r 149 + with 150 + | Error Auth_failure -> () 151 + | Error e -> 152 + Alcotest.fail 153 + (Format.asprintf "expected Auth_failure, got: %a" pp_error e) 154 + | Ok _ -> Alcotest.fail "should detect corrupted MAC" 155 + 156 + let test_hmac_sha256_mac_tamper () = 157 + let keys = make_keystore ~id:2 key_256 in 158 + let sa = 159 + Sa.v ~encryption:false ~authentication:true ~ecs:None 160 + ~acs:(Some Sa.HMAC_SHA_256) ~iv_len:0 ~mac_len:16 ~sn_len:0 161 + ~ak_id:(Keyid.of_int_exn 2) ~spi:2 ~scid:0 ~vcid:0 () 162 + in 163 + let plaintext = Bytes.of_string "auth tamper test" in 164 + let protected, _sa' = do_protect ~sa ~keys plaintext in 165 + (* Flip a bit in the MAC *) 166 + let corrupted = Bytes.copy protected in 167 + let last = Bytes.length corrupted - 1 in 168 + Bytes.set corrupted last 169 + (Char.chr (Char.code (Bytes.get corrupted last) lxor 0xFF)); 170 + let total_len = Bytes.length corrupted in 171 + let r = Binary.Reader.of_bytes ~off:frame_hdr_len corrupted in 172 + match 173 + unprotect_frame ~sa ~keys ~get_ek:Keystore.get_encryption_key 174 + ~get_ak:Keystore.get_auth_key ~frame_hdr_len ~frame_hdr_bytes:frame_hdr 175 + ~total_len r 176 + with 177 + | Error Auth_failure -> () 178 + | Error e -> 179 + Alcotest.fail 180 + (Format.asprintf "expected Auth_failure, got: %a" pp_error e) 181 + | Ok _ -> Alcotest.fail "should detect corrupted HMAC" 182 + 183 + (** {1 IV Increment Tests} *) 184 + 185 + let test_iv_increment_after_protect () = 186 + let keys = make_keystore ~id:1 key_256 in 187 + let sa = 188 + Sa.v ~encryption:true ~authentication:true ~ecs:(Some Sa.AES_256_GCM) 189 + ~iv_len:12 ~mac_len:16 ~sn_len:0 ~ek_id:(Keyid.of_int_exn 1) ~spi:1 190 + ~scid:0 ~vcid:0 () 191 + in 192 + let iv_before = Bytes.copy sa.dyn.iv in 193 + let plaintext = Bytes.of_string "frame 1" in 194 + let _protected1, sa1 = do_protect ~sa ~keys plaintext in 195 + (* IV must have changed *) 196 + Alcotest.(check bool) 197 + "IV changed after first protect" true 198 + (not (Bytes.equal iv_before sa1.dyn.iv)); 199 + (* Protect a second frame with the updated SA *) 200 + let iv_after_first = Bytes.copy sa1.dyn.iv in 201 + let _protected2, sa2 = do_protect ~sa:sa1 ~keys plaintext in 202 + Alcotest.(check bool) 203 + "IV changed after second protect" true 204 + (not (Bytes.equal iv_after_first sa2.dyn.iv)); 205 + (* The two protected frames must differ (different IVs -> different 206 + ciphertexts even for same plaintext) *) 207 + Alcotest.(check bool) 208 + "different ciphertexts for same plaintext" true 209 + (not (Bytes.equal _protected1 _protected2)) 210 + 211 + let test_iv_in_security_header () = 212 + let keys = make_keystore ~id:1 key_256 in 213 + let sa = 214 + Sa.v ~encryption:true ~authentication:true ~ecs:(Some Sa.AES_256_GCM) 215 + ~iv_len:12 ~mac_len:16 ~sn_len:0 ~ek_id:(Keyid.of_int_exn 1) ~spi:1 216 + ~scid:0 ~vcid:0 () 217 + in 218 + let iv0 = Bytes.copy sa.dyn.iv in 219 + let plaintext = Bytes.of_string "iv test" in 220 + let protected, sa1 = do_protect ~sa ~keys plaintext in 221 + (* Extract IV from security header: starts at frame_hdr_len + 2 (SPI) *) 222 + let iv_in_frame = Bytes.sub protected (frame_hdr_len + 2) 12 in 223 + Alcotest.(check bytes) 224 + "IV in frame matches SA IV before protect" iv0 iv_in_frame; 225 + (* The SA IV should now be iv0+1 *) 226 + let expected_next = Sa.increment_iv iv0 in 227 + Alcotest.(check bytes) "SA IV incremented" expected_next sa1.dyn.iv 228 + 229 + (** {1 Suite} *) 230 + 231 + let suite = 232 + ( "frame", 233 + [ 234 + (* AES-256-GCM roundtrip *) 235 + Alcotest.test_case "GCM protect/unprotect roundtrip" `Quick 236 + test_gcm_roundtrip; 237 + Alcotest.test_case "GCM ciphertext differs from plaintext" `Quick 238 + test_gcm_ciphertext_differs; 239 + (* HMAC-SHA-256 auth-only *) 240 + Alcotest.test_case "HMAC-SHA-256 auth-only roundtrip" `Quick 241 + test_hmac_sha256_auth_only_roundtrip; 242 + (* Tamper detection *) 243 + Alcotest.test_case "GCM MAC tamper detected" `Quick test_gcm_mac_tamper; 244 + Alcotest.test_case "HMAC-SHA-256 MAC tamper detected" `Quick 245 + test_hmac_sha256_mac_tamper; 246 + (* IV management *) 247 + Alcotest.test_case "IV increments after each protect" `Quick 248 + test_iv_increment_after_protect; 249 + Alcotest.test_case "IV appears in security header" `Quick 250 + test_iv_in_security_header; 251 + ] )
+1
test/test_sdls.ml
··· 10 10 Test_keystore.suite; 11 11 Test_key.suite; 12 12 Test_security.suite; 13 + Test_frame.suite; 13 14 ]