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.

Fix turbo decoder, add fuzzers for SDLS/RS/FlexACM

- Fix BCJR extrinsic LLR double-subtraction in turbo decoder;
all 7 turbo roundtrip tests now pass (39 total tm-sync tests)
- ocaml-sdls/fuzz: SA lifecycle, key state, EP PDU roundtrip (13 tests)
- ocaml-reed-solomon/fuzz: encode/decode with random errors (6 tests)
- ocaml-flexacm/fuzz: mode lookup, SNR monotonicity (6 tests)

+810
+22
fuzz/dune
··· 1 + (executable 2 + (name fuzz) 3 + (modules fuzz fuzz_sdls) 4 + (libraries sdls alcobar)) 5 + 6 + (rule 7 + (alias runtest) 8 + (enabled_if 9 + (<> %{profile} afl)) 10 + (deps fuzz.exe) 11 + (action 12 + (run %{exe:fuzz.exe}))) 13 + 14 + (rule 15 + (alias fuzz) 16 + (enabled_if 17 + (= %{profile} afl)) 18 + (deps fuzz.exe) 19 + (action 20 + (progn 21 + (run %{exe:fuzz.exe} --gen-corpus corpus) 22 + (run afl-fuzz -V 60 -i corpus -o _fuzz -- %{exe:fuzz.exe} @@))))
+1
fuzz/fuzz.ml
··· 1 + let () = Alcobar.run "sdls" [ Fuzz_sdls.suite ]
+254
fuzz/fuzz_sdls.ml
··· 1 + (*--------------------------------------------------------------------------- 2 + Copyright (c) 2025 Thomas Gazagnaire. All rights reserved. 3 + SPDX-License-Identifier: ISC 4 + ---------------------------------------------------------------------------*) 5 + 6 + open Alcobar 7 + open Sdls 8 + 9 + (** {1 SA Roundtrip} *) 10 + 11 + (** SA config serialization roundtrip: write then read should recover the same 12 + config. *) 13 + let test_sa_config_roundtrip spi_raw scid_raw vcid_raw iv_len_raw mac_len_raw = 14 + let spi = spi_raw land 0xFFFF in 15 + let scid = scid_raw land 0x3FF in 16 + let vcid = vcid_raw land 0x3F in 17 + let iv_len = iv_len_raw mod 17 in 18 + let mac_len = mac_len_raw mod 17 in 19 + let cfg = 20 + Sa.config ~spi ~scid ~vcid ~iv_len ~mac_len ~sn_len:4 ~arsnw:16 () 21 + in 22 + let w = Binary.Writer.create 256 in 23 + Sa.write_config w cfg; 24 + let buf = Binary.Writer.contents w in 25 + let r = Binary.Reader.of_bytes buf in 26 + match Sa.read_config r with 27 + | None -> fail "SA config read failed" 28 + | Some cfg' -> 29 + check_eq ~pp:pp_int cfg.spi cfg'.spi; 30 + check_eq ~pp:pp_int (fst cfg.gvcid) (fst cfg'.gvcid); 31 + check_eq ~pp:pp_int (snd cfg.gvcid) (snd cfg'.gvcid); 32 + check_eq ~pp:pp_int cfg.iv_len cfg'.iv_len; 33 + check_eq ~pp:pp_int cfg.mac_len cfg'.mac_len 34 + 35 + (** SA dyn serialization roundtrip. *) 36 + let test_sa_dyn_roundtrip spi_raw iv_len_raw = 37 + let spi = spi_raw land 0xFFFF in 38 + let iv_len = iv_len_raw mod 17 in 39 + let cfg = Sa.config ~spi ~scid:0 ~vcid:0 ~iv_len ~sn_len:4 ~arsnw:8 () in 40 + let d = Sa.dyn ~config:cfg () in 41 + let w = Binary.Writer.create 256 in 42 + Sa.write_dyn w d; 43 + let buf = Binary.Writer.contents w in 44 + let r = Binary.Reader.of_bytes buf in 45 + match Sa.read_dyn r with 46 + | None -> fail "SA dyn read failed" 47 + | Some d' -> 48 + check_eq ~pp:pp_int 49 + (Sa.int_of_state d.lifecycle) 50 + (Sa.int_of_state d'.lifecycle); 51 + check_eq ~pp:pp_int (Keyid.to_int d.ek_id) (Keyid.to_int d'.ek_id); 52 + check_eq ~pp:pp_int (Keyid.to_int d.ak_id) (Keyid.to_int d'.ak_id) 53 + 54 + (** {1 Key Lifecycle} *) 55 + 56 + (** Key lifecycle: empty -> update -> activate -> deactivate -> destroy. *) 57 + let test_key_lifecycle kid_raw alg_raw material_bytes = 58 + let kid = kid_raw land 0xFFFF in 59 + let alg = alg_raw land 0xFF in 60 + let material = 61 + let len = String.length material_bytes in 62 + if len < 32 then 63 + Bytes.of_string (material_bytes ^ String.make (32 - len) '\x00') 64 + else Bytes.of_string (String.sub material_bytes 0 32) 65 + in 66 + let k = Key.empty ~kid ~algorithm:alg in 67 + (* Empty key should not be usable *) 68 + if Key.is_usable k then fail "empty key should not be usable"; 69 + if Key.state k <> Key.Empty then fail "initial state should be Empty"; 70 + (* Update with material *) 71 + match Key.update ~material k with 72 + | Error e -> failf "update failed: %a" Key.pp_error e 73 + | Ok k -> ( 74 + if Key.state k <> Key.Pending then 75 + fail "after update state should be Pending"; 76 + (* Activate *) 77 + match Key.activate k with 78 + | Error e -> failf "activate failed: %a" Key.pp_error e 79 + | Ok k -> ( 80 + if Key.state k <> Key.Active then 81 + fail "after activate state should be Active"; 82 + if not (Key.is_usable k) then fail "active key should be usable"; 83 + (* Deactivate *) 84 + match Key.deactivate k with 85 + | Error e -> failf "deactivate failed: %a" Key.pp_error e 86 + | Ok k -> ( 87 + if Key.is_usable k then fail "deprecated key should not be usable"; 88 + (* Destroy *) 89 + match Key.destroy k with 90 + | Error e -> failf "destroy failed: %a" Key.pp_error e 91 + | Ok k -> 92 + if Key.state k <> Key.Zeroized then 93 + fail "after destroy state should be Zeroized"; 94 + if Key.get_material k <> None then 95 + fail "destroyed key should have no material"))) 96 + 97 + (** Key: invalid state transitions should return errors, not crash. *) 98 + let test_key_invalid_transitions kid_raw = 99 + let kid = kid_raw land 0xFFFF in 100 + let k = Key.empty ~kid ~algorithm:0 in 101 + (* Cannot activate an empty key *) 102 + (match Key.activate k with 103 + | Error _ -> () 104 + | Ok _ -> fail "activate empty key should fail"); 105 + (* Cannot deactivate an empty key *) 106 + (match Key.deactivate k with 107 + | Error _ -> () 108 + | Ok _ -> fail "deactivate empty key should fail"); 109 + (* Cannot destroy an empty key directly (depends on implementation) *) 110 + let _ = Key.destroy k in 111 + () 112 + 113 + (** {1 EP PDU Roundtrip} *) 114 + 115 + (** EP header encode/decode roundtrip. *) 116 + let test_ep_header_roundtrip is_reply user_flag sg_raw pid_raw pdu_len_raw = 117 + let sg_int = sg_raw mod 4 in 118 + match Ep.service_group_of_int sg_int with 119 + | None -> bad_test () 120 + | Some service_group -> ( 121 + let procedure_id = pid_raw mod 16 in 122 + let pdu_len = pdu_len_raw land 0xFFFF in 123 + let hdr = 124 + Ep.{ is_reply; user_flag; service_group; procedure_id; pdu_len } 125 + in 126 + let buf = Ep.encode_header hdr in 127 + match Ep.decode_header buf 0 with 128 + | Error `Truncated -> fail "unexpected truncation" 129 + | Error `Invalid_sg -> fail "unexpected invalid service group" 130 + | Ok hdr' -> 131 + check_eq ~pp:pp_bool hdr.is_reply hdr'.is_reply; 132 + check_eq ~pp:pp_bool hdr.user_flag hdr'.user_flag; 133 + check_eq ~pp:pp_int 134 + (Ep.int_of_service_group hdr.service_group) 135 + (Ep.int_of_service_group hdr'.service_group); 136 + check_eq ~pp:pp_int hdr.procedure_id hdr'.procedure_id; 137 + check_eq ~pp:pp_int hdr.pdu_len hdr'.pdu_len) 138 + 139 + (** EP header decode does not crash on arbitrary bytes. *) 140 + let test_ep_header_no_crash input = 141 + let buf = Bytes.of_string input in 142 + let _ = Ep.decode_header buf 0 in 143 + () 144 + 145 + (** EP header decode string does not crash on arbitrary strings. *) 146 + let test_ep_header_string_no_crash input = 147 + let _ = Ep.decode_header_string input 0 in 148 + () 149 + 150 + (** MC status reply roundtrip. *) 151 + let test_mc_status_roundtrip operational key_count_raw sa_count_raw = 152 + let key_count = key_count_raw land 0xFFFF in 153 + let sa_count = sa_count_raw land 0xFFFF in 154 + let reply = Ep.{ operational; key_count; sa_count } in 155 + let buf = Ep.encode_mc_status_reply reply in 156 + match Ep.decode_mc_status_reply buf 0 with 157 + | Error `Truncated -> fail "unexpected truncation" 158 + | Ok reply' -> 159 + check_eq ~pp:pp_bool reply.operational reply'.operational; 160 + check_eq ~pp:pp_int reply.key_count reply'.key_count; 161 + check_eq ~pp:pp_int reply.sa_count reply'.sa_count 162 + 163 + (** {1 SA Management} *) 164 + 165 + (** SA start/stop roundtrip: keyed -> start -> stop -> keyed. *) 166 + let test_sa_start_stop spi_raw = 167 + let spi = spi_raw land 0xFFFF in 168 + let ek_id = Keyid.of_int_exn 1 in 169 + let ak_id = Keyid.of_int_exn 2 in 170 + let sa = Sa.keyed ~spi ~scid:0 ~vcid:0 ~ek_id ~ak_id in 171 + match Sa.start sa with 172 + | Error e -> failf "start failed: %a" Sa.pp_mgmt_error e 173 + | Ok sa -> ( 174 + if sa.dyn.lifecycle <> Operational then 175 + fail "after start should be Operational"; 176 + match Sa.stop sa with 177 + | Error e -> failf "stop failed: %a" Sa.pp_mgmt_error e 178 + | Ok sa -> 179 + if sa.dyn.lifecycle <> Keyed then fail "after stop should be Keyed") 180 + 181 + (** SA rekey updates key IDs. *) 182 + let test_sa_rekey spi_raw ek_raw ak_raw = 183 + let spi = spi_raw land 0xFFFF in 184 + let ek_id = Keyid.of_int_exn (ek_raw land 0xFFFF) in 185 + let ak_id = Keyid.of_int_exn (ak_raw land 0xFFFF) in 186 + let sa = 187 + Sa.keyed ~spi ~scid:0 ~vcid:0 ~ek_id:(Keyid.of_int_exn 1) 188 + ~ak_id:(Keyid.of_int_exn 2) 189 + in 190 + let sa' = Sa.rekey ~ek_id ~ak_id sa in 191 + check_eq ~pp:pp_int (Keyid.to_int ek_id) (Keyid.to_int sa'.dyn.ek_id); 192 + check_eq ~pp:pp_int (Keyid.to_int ak_id) (Keyid.to_int sa'.dyn.ak_id) 193 + 194 + (** SA expire transitions to unkeyed state and clears keys. *) 195 + let test_sa_expire spi_raw = 196 + let spi = spi_raw land 0xFFFF in 197 + let ek_id = Keyid.of_int_exn 1 in 198 + let ak_id = Keyid.of_int_exn 2 in 199 + let sa = Sa.keyed ~spi ~scid:0 ~vcid:0 ~ek_id ~ak_id in 200 + let sa' = Sa.expire sa in 201 + if sa'.dyn.lifecycle <> Unkeyed then fail "after expire should be Unkeyed" 202 + 203 + (** SA start from unkeyed should fail. *) 204 + let test_sa_start_unkeyed spi_raw = 205 + let spi = spi_raw land 0xFFFF in 206 + let sa = Sa.unkeyed ~spi ~scid:0 ~vcid:0 in 207 + match Sa.start sa with 208 + | Error _ -> () 209 + | Ok _ -> fail "start unkeyed SA should fail" 210 + 211 + (** SA IV increment roundtrip. *) 212 + let test_sa_iv_increment iv_bytes = 213 + let len = String.length iv_bytes in 214 + if len < 1 || len > 16 then bad_test () 215 + else begin 216 + let iv = Bytes.of_string iv_bytes in 217 + if Sa.is_iv_max iv then begin 218 + (* Incrementing a max IV should wrap or return None *) 219 + match Sa.increment_iv_safe iv with 220 + | None -> () 221 + | Some _ -> () 222 + end 223 + else begin 224 + let iv' = Sa.increment_iv iv in 225 + (* After increment, the new IV should be strictly greater *) 226 + let cmp = Sa.cmp_be iv' iv in 227 + if cmp <= 0 then fail "incremented IV should be greater" 228 + end 229 + end 230 + 231 + let suite = 232 + ( "sdls", 233 + [ 234 + test_case "SA config roundtrip" 235 + [ uint16; uint16; uint8; uint8; uint8 ] 236 + test_sa_config_roundtrip; 237 + test_case "SA dyn roundtrip" [ uint16; uint8 ] test_sa_dyn_roundtrip; 238 + test_case "key lifecycle" [ uint16; uint8; bytes ] test_key_lifecycle; 239 + test_case "key invalid transitions" [ uint16 ] 240 + test_key_invalid_transitions; 241 + test_case "EP header roundtrip" 242 + [ bool; bool; uint8; uint8; uint16 ] 243 + test_ep_header_roundtrip; 244 + test_case "EP header no crash" [ bytes ] test_ep_header_no_crash; 245 + test_case "EP header string no crash" [ bytes ] 246 + test_ep_header_string_no_crash; 247 + test_case "MC status roundtrip" [ bool; uint16; uint16 ] 248 + test_mc_status_roundtrip; 249 + test_case "SA start/stop" [ uint16 ] test_sa_start_stop; 250 + test_case "SA rekey" [ uint16; uint16; uint16 ] test_sa_rekey; 251 + test_case "SA expire" [ uint16 ] test_sa_expire; 252 + test_case "SA start unkeyed" [ uint16 ] test_sa_start_unkeyed; 253 + test_case "SA IV increment" [ bytes ] test_sa_iv_increment; 254 + ] )
+533
test/test_crypto.ml
··· 1 + (** Tests for Crypto module. *) 2 + 3 + open Sdls 4 + 5 + (** Helper to convert hex string to bytes *) 6 + let bytes_of_hex s = 7 + let len = String.length s / 2 in 8 + let b = Bytes.create len in 9 + for i = 0 to len - 1 do 10 + let hex = String.sub s (i * 2) 2 in 11 + Bytes.set b i (Char.chr (int_of_string ("0x" ^ hex))) 12 + done; 13 + b 14 + 15 + (** {1 AES-CCM Tests} *) 16 + 17 + let test_ccm_roundtrip () = 18 + let key = 19 + Bytes.of_string 20 + "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f" 21 + in 22 + let nonce = 23 + Bytes.of_string "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b" 24 + in 25 + let aad = Bytes.of_string "additional data" in 26 + let plaintext = Bytes.of_string "secret message" in 27 + match Crypto.encrypt_aes_ccm ~key ~nonce ~aad plaintext with 28 + | Error e -> 29 + Alcotest.fail (Format.asprintf "encrypt failed: %a" Crypto.pp_error e) 30 + | Ok ciphertext_tag -> ( 31 + match Crypto.decrypt_aes_ccm ~key ~nonce ~aad ciphertext_tag with 32 + | Error e -> 33 + Alcotest.fail (Format.asprintf "decrypt failed: %a" Crypto.pp_error e) 34 + | Ok decrypted -> 35 + Alcotest.(check string) 36 + "roundtrip" "secret message" 37 + (Bytes.to_string decrypted)) 38 + 39 + let test_ccm_auth_failure () = 40 + let key = 41 + Bytes.of_string 42 + "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f" 43 + in 44 + let nonce = 45 + Bytes.of_string "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b" 46 + in 47 + let aad = Bytes.of_string "additional data" in 48 + let plaintext = Bytes.of_string "secret" in 49 + match Crypto.encrypt_aes_ccm ~key ~nonce ~aad plaintext with 50 + | Error _ -> Alcotest.fail "encrypt should not fail" 51 + | Ok ciphertext_tag -> ( 52 + (* Corrupt the ciphertext *) 53 + let corrupted = Bytes.copy ciphertext_tag in 54 + Bytes.set corrupted 0 55 + (Char.chr (Char.code (Bytes.get corrupted 0) lxor 0xFF)); 56 + match Crypto.decrypt_aes_ccm ~key ~nonce ~aad corrupted with 57 + | Ok _ -> Alcotest.fail "decrypt should fail on corrupted data" 58 + | Error Crypto.Auth_failure -> () 59 + | Error e -> 60 + Alcotest.fail 61 + (Format.asprintf "unexpected error: %a" Crypto.pp_error e)) 62 + 63 + (* RFC 3610 vectors use shorter tags (8/10). With CCM-16 policy, these must fail. *) 64 + let test_ccm_kat_rfc3610_truncated_tag () = 65 + (* Source: RFC 3610, Packet Vector #1 (M=8). *) 66 + let key = bytes_of_hex "c0c1c2c3c4c5c6c7c8c9cacbcccdcecf" in 67 + let nonce = bytes_of_hex "00000003020100a0a1a2a3a4a5" in 68 + let aad = bytes_of_hex "0001020304050607" in 69 + let ct = bytes_of_hex "588c979a61c663d2f066d0c2c0f989806d5f6b61dac384" in 70 + let tag8 = bytes_of_hex "17e8d12cfdf926e0" in 71 + let ciphertext_tag = Bytes.cat ct tag8 in 72 + match Crypto.decrypt_aes_ccm ~key ~nonce ~aad ciphertext_tag with 73 + | Ok _ -> Alcotest.fail "CCM-16 should reject RFC3610 M=8 vector" 74 + | Error Crypto.Auth_failure -> () 75 + | Error e -> 76 + Alcotest.fail (Format.asprintf "unexpected error: %a" Crypto.pp_error e) 77 + 78 + let test_ccm_kat_rfc3610_truncated_tag_10 () = 79 + (* Source: RFC 3610, Packet Vector with M=10. *) 80 + let key = bytes_of_hex "c0c1c2c3c4c5c6c7c8c9cacbcccdcecf" in 81 + let nonce = bytes_of_hex "00000009080706a0a1a2a3a4a5" in 82 + let aad = bytes_of_hex "0001020304050607" in 83 + let ct = bytes_of_hex "0135d1b2c95f41d5d1d4fec185d166b8094e999dfed96c" in 84 + let tag10 = bytes_of_hex "048c56602c97acbb7490" in 85 + let ciphertext_tag = Bytes.cat ct tag10 in 86 + match Crypto.decrypt_aes_ccm ~key ~nonce ~aad ciphertext_tag with 87 + | Ok _ -> Alcotest.fail "CCM-16 should reject RFC3610 M=10 vector" 88 + | Error Crypto.Auth_failure -> () 89 + | Error e -> 90 + Alcotest.fail (Format.asprintf "unexpected error: %a" Crypto.pp_error e) 91 + 92 + let test_ccm_invalid_key () = 93 + let key = Bytes.of_string "short" in 94 + let nonce = 95 + Bytes.of_string "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b" 96 + in 97 + match Crypto.encrypt_aes_ccm ~key ~nonce ~aad:Bytes.empty Bytes.empty with 98 + | Error Crypto.Invalid_key_length -> () 99 + | _ -> Alcotest.fail "should reject invalid key length" 100 + 101 + (** {1 AES-CCM Known Answer Tests} 102 + 103 + Positive CCM-16 vectors sourced from NIST SP 800-38C CAVS via OpenSSL: 104 + test/recipes/30-test_evp_data/evpciph_aes_ccm_cavs.txt 105 + @see <https://csrc.nist.gov/publications/detail/sp/800-38c/final> 106 + @see <https://github.com/openssl/openssl/blob/master/test/recipes/30-test_evp_data/evpciph_aes_ccm_cavs.txt> 107 + *) 108 + 109 + let test_ccm_kat_sp800_38c_case1 () = 110 + (* Key = 197afb02ffbd8f699dacae87094d5243 111 + IV = 5a8aa485c316e9 (7 bytes) 112 + AAD = empty 113 + PT = 3796cf51b8726652a4204733b8fbb047cf00fb91a9837e22 (24 bytes) 114 + CT = 24ab9eeb0e5508cae80074f1070ee188a637171860881f1f 115 + TAG= 2d9a3fbc210595b7b8b1b41523111a8e (16 bytes) *) 116 + let key = bytes_of_hex "197afb02ffbd8f699dacae87094d5243" in 117 + let nonce = bytes_of_hex "5a8aa485c316e9" in 118 + let aad = Bytes.empty in 119 + let pt = bytes_of_hex "3796cf51b8726652a4204733b8fbb047cf00fb91a9837e22" in 120 + let expected = 121 + bytes_of_hex 122 + "24ab9eeb0e5508cae80074f1070ee188a637171860881f1f2d9a3fbc210595b7b8b1b41523111a8e" 123 + in 124 + match Crypto.encrypt_aes_ccm ~key ~nonce ~aad pt with 125 + | Error e -> 126 + Alcotest.fail (Format.asprintf "ccm encrypt failed: %a" Crypto.pp_error e) 127 + | Ok out -> ( 128 + Alcotest.(check bytes) "ct||tag" expected out; 129 + match Crypto.decrypt_aes_ccm ~key ~nonce ~aad out with 130 + | Error e -> 131 + Alcotest.fail 132 + (Format.asprintf "ccm decrypt failed: %a" Crypto.pp_error e) 133 + | Ok pt' -> Alcotest.(check bytes) "pt" pt pt') 134 + 135 + let test_ccm_kat_sp800_38c_case2 () = 136 + (* Key = 197afb02ffbd8f699dacae87094d5243 137 + IV = 123a0beace4e39 (7 bytes) 138 + AAD = empty 139 + PT = 9d033e3b66efed1467868f382417c80594877a28bc97f406 (24 bytes) 140 + CT = d43035cdb5a1868aa430e8b41a1dc57a639087238e38bd62 141 + TAG= 8feeda2e8f249dd93a8358def7639875 (16 bytes) *) 142 + let key = bytes_of_hex "197afb02ffbd8f699dacae87094d5243" in 143 + let nonce = bytes_of_hex "123a0beace4e39" in 144 + let aad = Bytes.empty in 145 + let pt = bytes_of_hex "9d033e3b66efed1467868f382417c80594877a28bc97f406" in 146 + let expected = 147 + bytes_of_hex 148 + "d43035cdb5a1868aa430e8b41a1dc57a639087238e38bd628feeda2e8f249dd93a8358def7639875" 149 + in 150 + match Crypto.encrypt_aes_ccm ~key ~nonce ~aad pt with 151 + | Error e -> 152 + Alcotest.fail (Format.asprintf "ccm encrypt failed: %a" Crypto.pp_error e) 153 + | Ok out -> ( 154 + Alcotest.(check bytes) "ct||tag" expected out; 155 + match Crypto.decrypt_aes_ccm ~key ~nonce ~aad out with 156 + | Error e -> 157 + Alcotest.fail 158 + (Format.asprintf "ccm decrypt failed: %a" Crypto.pp_error e) 159 + | Ok pt' -> Alcotest.(check bytes) "pt" pt pt') 160 + 161 + let test_ccm_kat_sp800_38c_case3 () = 162 + (* Key = 90929a4b0ac65b350ad1591611fe4829 163 + IV = 24b7a65391f88bea38fcd54a9a (13 bytes) 164 + AAD = empty 165 + PT = 43419715cef9a48dc7280bc035082a6581afd1d82bee9d1a (24 bytes) 166 + CT = 9fa846ef8d198c538f84f856bab8f7f9c3bed90b53acb6a3 167 + TAG= 2658e077687315eaf11458bdf6e3c36a (16 bytes) *) 168 + let key = bytes_of_hex "90929a4b0ac65b350ad1591611fe4829" in 169 + let nonce = bytes_of_hex "24b7a65391f88bea38fcd54a9a" in 170 + let aad = Bytes.empty in 171 + let pt = bytes_of_hex "43419715cef9a48dc7280bc035082a6581afd1d82bee9d1a" in 172 + let expected = 173 + bytes_of_hex 174 + "9fa846ef8d198c538f84f856bab8f7f9c3bed90b53acb6a32658e077687315eaf11458bdf6e3c36a" 175 + in 176 + match Crypto.encrypt_aes_ccm ~key ~nonce ~aad pt with 177 + | Error e -> 178 + Alcotest.fail (Format.asprintf "ccm encrypt failed: %a" Crypto.pp_error e) 179 + | Ok out -> ( 180 + Alcotest.(check bytes) "ct||tag" expected out; 181 + match Crypto.decrypt_aes_ccm ~key ~nonce ~aad out with 182 + | Error e -> 183 + Alcotest.fail 184 + (Format.asprintf "ccm decrypt failed: %a" Crypto.pp_error e) 185 + | Ok pt' -> Alcotest.(check bytes) "pt" pt pt') 186 + 187 + let test_ccm_kat_sp800_38c_aad_case1 () = 188 + (* Source: NIST SP 800-38C CAVS via OpenSSL (with AAD) 189 + Key = a7aa635ea51b0bb20a092bd5573e728c 190 + IV = 5a8aa485c316e9 (7 bytes) 191 + AAD = 3796cf51b8726652a4204733b8fbb047cf00fb91a9837e22ec22b1a268f88e2c 192 + PT = a265480ca88d5f536db0dc6abc40faf0d05be7a966977768 193 + CT = b351ab96b2e45515254558d5212673ee6c776d42dbca3b51 194 + TAG = 2cf3a20b7fd7c49e6e79bef475c2906f *) 195 + let key = bytes_of_hex "a7aa635ea51b0bb20a092bd5573e728c" in 196 + let nonce = bytes_of_hex "5a8aa485c316e9" in 197 + let aad = 198 + bytes_of_hex 199 + "3796cf51b8726652a4204733b8fbb047cf00fb91a9837e22ec22b1a268f88e2c" 200 + in 201 + let pt = bytes_of_hex "a265480ca88d5f536db0dc6abc40faf0d05be7a966977768" in 202 + let expected = 203 + bytes_of_hex 204 + "b351ab96b2e45515254558d5212673ee6c776d42dbca3b512cf3a20b7fd7c49e6e79bef475c2906f" 205 + in 206 + match Crypto.encrypt_aes_ccm ~key ~nonce ~aad pt with 207 + | Error e -> 208 + Alcotest.fail (Format.asprintf "ccm encrypt failed: %a" Crypto.pp_error e) 209 + | Ok out -> ( 210 + Alcotest.(check bytes) "ct||tag" expected out; 211 + match Crypto.decrypt_aes_ccm ~key ~nonce ~aad out with 212 + | Error e -> 213 + Alcotest.fail 214 + (Format.asprintf "ccm decrypt failed: %a" Crypto.pp_error e) 215 + | Ok pt' -> Alcotest.(check bytes) "pt" pt pt') 216 + 217 + let test_ccm_kat_sp800_38c_aad_case2 () = 218 + (* Source: NIST SP 800-38C CAVS via OpenSSL (with AAD) 219 + Key = 26511fb51fcfa75cb4b44da75a6e5a0e 220 + IV = 5a8aa485c316e9403aff859fbb (13 bytes) 221 + AAD = a16a2e741f1cd9717285b6d882c1fc53655e9773761ad697a7ee6410184c7982 222 + PT = 8739b4bea1a099fe547499cbc6d1b13d849b8084c9b6acc5 223 + CT = 50038b5fdd364ee747b70d00bd36840ece4ea19998123375 224 + TAG = c0a458bfcafa3b2609afe0f825cbf503 *) 225 + let key = bytes_of_hex "26511fb51fcfa75cb4b44da75a6e5a0e" in 226 + let nonce = bytes_of_hex "5a8aa485c316e9403aff859fbb" in 227 + let aad = 228 + bytes_of_hex 229 + "a16a2e741f1cd9717285b6d882c1fc53655e9773761ad697a7ee6410184c7982" 230 + in 231 + let pt = bytes_of_hex "8739b4bea1a099fe547499cbc6d1b13d849b8084c9b6acc5" in 232 + let expected = 233 + bytes_of_hex 234 + "50038b5fdd364ee747b70d00bd36840ece4ea19998123375c0a458bfcafa3b2609afe0f825cbf503" 235 + in 236 + match Crypto.encrypt_aes_ccm ~key ~nonce ~aad pt with 237 + | Error e -> 238 + Alcotest.fail (Format.asprintf "ccm encrypt failed: %a" Crypto.pp_error e) 239 + | Ok out -> ( 240 + Alcotest.(check bytes) "ct||tag" expected out; 241 + match Crypto.decrypt_aes_ccm ~key ~nonce ~aad out with 242 + | Error e -> 243 + Alcotest.fail 244 + (Format.asprintf "ccm decrypt failed: %a" Crypto.pp_error e) 245 + | Ok pt' -> Alcotest.(check bytes) "pt" pt pt') 246 + 247 + (** {1 Additional Negative Tests} *) 248 + 249 + let test_ccm_aad_mismatch_auth_failure () = 250 + (* Based on SP800-38C case1; flip AAD to force failure. *) 251 + let key = bytes_of_hex "a7aa635ea51b0bb20a092bd5573e728c" in 252 + let nonce = bytes_of_hex "5a8aa485c316e9" in 253 + let aad = 254 + bytes_of_hex 255 + "3796cf51b8726652a4204733b8fbb047cf00fb91a9837e22ec22b1a268f88e2c" 256 + in 257 + let pt = bytes_of_hex "a265480ca88d5f536db0dc6abc40faf0d05be7a966977768" in 258 + match Crypto.encrypt_aes_ccm ~key ~nonce ~aad pt with 259 + | Error e -> 260 + Alcotest.fail (Format.asprintf "ccm encrypt failed: %a" Crypto.pp_error e) 261 + | Ok out -> ( 262 + let bad_aad = Bytes.copy aad in 263 + Bytes.set bad_aad 0 (Char.chr (Char.code (Bytes.get bad_aad 0) lxor 1)); 264 + match Crypto.decrypt_aes_ccm ~key ~nonce ~aad:bad_aad out with 265 + | Ok _ -> Alcotest.fail "ccm decrypt should fail with bad AAD" 266 + | Error Crypto.Auth_failure -> () 267 + | Error e -> 268 + Alcotest.fail 269 + (Format.asprintf "unexpected error: %a" Crypto.pp_error e)) 270 + 271 + let test_ccm_invalid_nonce_length_short () = 272 + let key = 273 + Bytes.of_string 274 + "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f" 275 + in 276 + let nonce = Bytes.of_string "\x00\x01\x02\x03\x04\x05" in 277 + (* 6 bytes, invalid *) 278 + match Crypto.encrypt_aes_ccm ~key ~nonce ~aad:Bytes.empty Bytes.empty with 279 + | Error Crypto.Invalid_nonce_length -> () 280 + | _ -> Alcotest.fail "should reject invalid CCM nonce length" 281 + 282 + let test_ccm_invalid_nonce_length_long () = 283 + let key = 284 + Bytes.of_string 285 + "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f" 286 + in 287 + let nonce = Bytes.make 14 '\x00' in 288 + (* 14 bytes, invalid *) 289 + match Crypto.encrypt_aes_ccm ~key ~nonce ~aad:Bytes.empty Bytes.empty with 290 + | Error Crypto.Invalid_nonce_length -> () 291 + | _ -> Alcotest.fail "should reject invalid CCM nonce length" 292 + 293 + let test_gcm_invalid_nonce_length_short () = 294 + let key = 295 + Bytes.of_string 296 + "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f" 297 + in 298 + let nonce = Bytes.of_string "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a" in 299 + (* 11 bytes *) 300 + match Crypto.encrypt_aes_gcm ~key ~nonce ~aad:Bytes.empty Bytes.empty with 301 + | Error Crypto.Invalid_nonce_length -> () 302 + | _ -> Alcotest.fail "should reject invalid GCM nonce length" 303 + 304 + let test_gcm_invalid_nonce_length_long () = 305 + let key = 306 + Bytes.of_string 307 + "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f" 308 + in 309 + let nonce = Bytes.make 13 '\x00' in 310 + (* 13 bytes *) 311 + match Crypto.encrypt_aes_gcm ~key ~nonce ~aad:Bytes.empty Bytes.empty with 312 + | Error Crypto.Invalid_nonce_length -> () 313 + | _ -> Alcotest.fail "should reject invalid GCM nonce length" 314 + 315 + let test_gcm_short_ciphertext_tag () = 316 + let key = 317 + Bytes.of_string 318 + "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f" 319 + in 320 + let nonce = 321 + Bytes.of_string "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b" 322 + in 323 + let aad = Bytes.empty in 324 + match 325 + Crypto.decrypt_aes_gcm ~key ~nonce ~aad (Bytes.of_string "\x00\x01\x02") 326 + with 327 + | Error Crypto.Auth_failure -> () 328 + | _ -> Alcotest.fail "GCM should fail on too-short input" 329 + 330 + let test_ccm_short_ciphertext_tag () = 331 + let key = 332 + Bytes.of_string 333 + "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f" 334 + in 335 + let nonce = 336 + Bytes.of_string "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b" 337 + in 338 + let aad = Bytes.empty in 339 + match 340 + Crypto.decrypt_aes_ccm ~key ~nonce ~aad (Bytes.of_string "\x00\x01\x02") 341 + with 342 + | Error Crypto.Auth_failure -> () 343 + | _ -> Alcotest.fail "CCM should fail on too-short input" 344 + 345 + (** {1 AES-GCM Tests} *) 346 + 347 + let test_gcm_roundtrip () = 348 + let key = 349 + Bytes.of_string 350 + "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f" 351 + in 352 + let nonce = 353 + Bytes.of_string "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b" 354 + in 355 + let aad = Bytes.of_string "header" in 356 + let plaintext = Bytes.of_string "payload data" in 357 + match Crypto.encrypt_aes_gcm ~key ~nonce ~aad plaintext with 358 + | Error e -> 359 + Alcotest.fail (Format.asprintf "encrypt failed: %a" Crypto.pp_error e) 360 + | Ok ciphertext_tag -> ( 361 + match Crypto.decrypt_aes_gcm ~key ~nonce ~aad ciphertext_tag with 362 + | Error e -> 363 + Alcotest.fail (Format.asprintf "decrypt failed: %a" Crypto.pp_error e) 364 + | Ok decrypted -> 365 + Alcotest.(check string) 366 + "roundtrip" "payload data" 367 + (Bytes.to_string decrypted)) 368 + 369 + (** {1 AES-GCM Known Answer Tests} 370 + 371 + Test vectors from NIST SP 800-38D (GCM) -- Section F.5. 372 + @see <https://csrc.nist.gov/publications/detail/sp/800-38d/final> 373 + NIST SP 800-38D *) 374 + 375 + (** F.5.2 Test Case 2: 128-bit key, 96-bit IV, empty AAD, 16-byte PT. key=0^128, 376 + IV=0^96, PT=0^128 CT=0388dace60b6a392f328c2b971b2fe78, 377 + TAG=ab6e47d42cec13bdf53a67b21257bddf *) 378 + let test_gcm_vector_empty_aad () = 379 + let key = bytes_of_hex "00000000000000000000000000000000" in 380 + let nonce = bytes_of_hex "000000000000000000000000" in 381 + let aad = Bytes.empty in 382 + let pt = bytes_of_hex "00000000000000000000000000000000" in 383 + let expected = 384 + bytes_of_hex 385 + "0388dace60b6a392f328c2b971b2fe78ab6e47d42cec13bdf53a67b21257bddf" 386 + in 387 + match Crypto.encrypt_aes_gcm ~key ~nonce ~aad pt with 388 + | Error e -> 389 + Alcotest.fail (Format.asprintf "gcm encrypt failed: %a" Crypto.pp_error e) 390 + | Ok out -> Alcotest.(check bytes) "nist gcm tc2" expected out 391 + 392 + (** F.5.2 Test Case 3: 128-bit key, 96-bit IV, non-empty AAD, multi-block PT. 393 + key=feffe9928665731c6d6a8f9467308308 IV=cafebabefacedbaddecaf888 394 + AAD=feedfacedeadbeeffeedfacedeadbeefabaddad2 395 + PT=d9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a72 396 + 1c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b39 397 + CT=42831ec2217774244b7221b784d0d49ce3aa212f2c02a4e035c17e2329aca12e 398 + 21d514b25466931c7d8f6a5aac84aa051ba30b396a0aac973d58e091 399 + TAG=5bc94fbc3221a5db94fae95ae7121a47 *) 400 + let test_gcm_vector_with_aad () = 401 + let key = bytes_of_hex "feffe9928665731c6d6a8f9467308308" in 402 + let nonce = bytes_of_hex "cafebabefacedbaddecaf888" in 403 + let aad = bytes_of_hex "feedfacedeadbeeffeedfacedeadbeefabaddad2" in 404 + let pt = 405 + bytes_of_hex 406 + ("d9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a72" 407 + ^ "1c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b39") 408 + in 409 + let expected = 410 + bytes_of_hex 411 + ("42831ec2217774244b7221b784d0d49ce3aa212f2c02a4e035c17e2329aca12e" 412 + ^ "21d514b25466931c7d8f6a5aac84aa051ba30b396a0aac973d58e091" 413 + ^ "5bc94fbc3221a5db94fae95ae7121a47") 414 + in 415 + match Crypto.encrypt_aes_gcm ~key ~nonce ~aad pt with 416 + | Error e -> 417 + Alcotest.fail (Format.asprintf "gcm encrypt failed: %a" Crypto.pp_error e) 418 + | Ok out -> Alcotest.(check bytes) "nist gcm tc3" expected out 419 + 420 + (** GCM auth should fail if AAD is modified (same NIST TC2 params). *) 421 + let test_gcm_aad_auth_failure () = 422 + let key = bytes_of_hex "00000000000000000000000000000000" in 423 + let nonce = bytes_of_hex "000000000000000000000000" in 424 + let aad = Bytes.empty in 425 + let pt = bytes_of_hex "00000000000000000000000000000000" in 426 + match Crypto.encrypt_aes_gcm ~key ~nonce ~aad pt with 427 + | Error _ -> Alcotest.fail "gcm encrypt should succeed" 428 + | Ok ct_tag -> ( 429 + let bad_aad = Bytes.of_string "x" in 430 + match Crypto.decrypt_aes_gcm ~key ~nonce ~aad:bad_aad ct_tag with 431 + | Ok _ -> Alcotest.fail "gcm decrypt should fail with bad AAD" 432 + | Error Crypto.Auth_failure -> () 433 + | Error e -> 434 + Alcotest.fail 435 + (Format.asprintf "unexpected error: %a" Crypto.pp_error e)) 436 + 437 + (** {1 CMAC Tests} *) 438 + 439 + let test_cmac_consistency () = 440 + let key = 441 + Bytes.of_string 442 + "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f" 443 + in 444 + let data = Bytes.of_string "message to authenticate" in 445 + match Crypto.cmac ~key data with 446 + | Error e -> 447 + Alcotest.fail (Format.asprintf "cmac failed: %a" Crypto.pp_error e) 448 + | Ok mac1 -> ( 449 + match Crypto.cmac ~key data with 450 + | Error _ -> Alcotest.fail "second cmac failed" 451 + | Ok mac2 -> 452 + Alcotest.(check int) "mac length" 16 (Bytes.length mac1); 453 + Alcotest.(check bytes) "deterministic" mac1 mac2) 454 + 455 + let test_cmac_verify () = 456 + let key = 457 + Bytes.of_string 458 + "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f" 459 + in 460 + let data = Bytes.of_string "message" in 461 + match Crypto.cmac ~key data with 462 + | Error _ -> Alcotest.fail "cmac failed" 463 + | Ok mac -> ( 464 + match Crypto.cmac_verify ~key ~mac data with 465 + | Error _ -> Alcotest.fail "verify failed" 466 + | Ok valid -> Alcotest.(check bool) "valid mac" true valid) 467 + 468 + let test_cmac_verify_bad_mac () = 469 + let key = 470 + Bytes.of_string 471 + "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f" 472 + in 473 + let data = Bytes.of_string "message" in 474 + let bad_mac = Bytes.make 16 '\x00' in 475 + match Crypto.cmac_verify ~key ~mac:bad_mac data with 476 + | Error _ -> Alcotest.fail "verify should not error" 477 + | Ok valid -> Alcotest.(check bool) "invalid mac" false valid 478 + 479 + let test_cmac_empty () = 480 + let key = 481 + Bytes.of_string 482 + "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f" 483 + in 484 + match Crypto.cmac ~key Bytes.empty with 485 + | Error e -> 486 + Alcotest.fail (Format.asprintf "cmac empty failed: %a" Crypto.pp_error e) 487 + | Ok mac -> Alcotest.(check int) "mac length" 16 (Bytes.length mac) 488 + 489 + let suite = 490 + ( "crypto", 491 + [ 492 + Alcotest.test_case "ccm roundtrip" `Quick test_ccm_roundtrip; 493 + Alcotest.test_case "ccm auth failure" `Quick test_ccm_auth_failure; 494 + Alcotest.test_case "ccm invalid key" `Quick test_ccm_invalid_key; 495 + Alcotest.test_case "ccm KAT SP800-38C #1" `Quick 496 + test_ccm_kat_sp800_38c_case1; 497 + Alcotest.test_case "ccm KAT SP800-38C #2" `Quick 498 + test_ccm_kat_sp800_38c_case2; 499 + Alcotest.test_case "ccm KAT SP800-38C #3" `Quick 500 + test_ccm_kat_sp800_38c_case3; 501 + Alcotest.test_case "ccm KAT SP800-38C AAD #1" `Quick 502 + test_ccm_kat_sp800_38c_aad_case1; 503 + Alcotest.test_case "ccm KAT SP800-38C AAD #2" `Quick 504 + test_ccm_kat_sp800_38c_aad_case2; 505 + Alcotest.test_case "ccm KAT RFC3610 M=8 rejected" `Quick 506 + test_ccm_kat_rfc3610_truncated_tag; 507 + Alcotest.test_case "ccm KAT RFC3610 M=10 rejected" `Quick 508 + test_ccm_kat_rfc3610_truncated_tag_10; 509 + Alcotest.test_case "ccm AAD mismatch fails" `Quick 510 + test_ccm_aad_mismatch_auth_failure; 511 + Alcotest.test_case "ccm invalid nonce short" `Quick 512 + test_ccm_invalid_nonce_length_short; 513 + Alcotest.test_case "ccm invalid nonce long" `Quick 514 + test_ccm_invalid_nonce_length_long; 515 + Alcotest.test_case "gcm invalid nonce short" `Quick 516 + test_gcm_invalid_nonce_length_short; 517 + Alcotest.test_case "gcm invalid nonce long" `Quick 518 + test_gcm_invalid_nonce_length_long; 519 + Alcotest.test_case "gcm short input fails" `Quick 520 + test_gcm_short_ciphertext_tag; 521 + Alcotest.test_case "ccm short input fails" `Quick 522 + test_ccm_short_ciphertext_tag; 523 + Alcotest.test_case "gcm roundtrip" `Quick test_gcm_roundtrip; 524 + Alcotest.test_case "gcm KAT empty AAD (NIST)" `Quick 525 + test_gcm_vector_empty_aad; 526 + Alcotest.test_case "gcm KAT with AAD (NIST)" `Quick 527 + test_gcm_vector_with_aad; 528 + Alcotest.test_case "gcm AAD auth failure" `Quick test_gcm_aad_auth_failure; 529 + Alcotest.test_case "cmac consistency" `Quick test_cmac_consistency; 530 + Alcotest.test_case "cmac verify" `Quick test_cmac_verify; 531 + Alcotest.test_case "cmac verify bad" `Quick test_cmac_verify_bad_mac; 532 + Alcotest.test_case "cmac empty" `Quick test_cmac_empty; 533 + ] )