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.

sdls: add CryptoLib interop test (20 tests, real oracle)

Cross-validate SDLS frame protection against NASA CryptoLib — the
standard open-source CCSDS 355.0-B-2 implementation.

The generator (scripts/generate.c) calls CryptoLib's actual public
API: Crypto_Init_TC_Unit_Test() for SA setup, then
Crypto_TC_ApplySecurity() to produce secured frames. No SDLS
encoding is reimplemented — CryptoLib is the independent oracle.

12 reference vectors across three modes:
- Clear mode (SA 1, SPI=1, no encryption/auth)
- GCM encrypt-only (SA 2, SPI=2, header parsing only)
- GCM authenticated-encryption (SA 4, SPI=4, 16-byte MAC)

Two test suites:
- protect (8 tests): byte-for-byte match of Sdls.protect_frame
output against CryptoLib's secured frames
- security_header (12 tests): SPI and TC header consistency from
all CryptoLib-secured frames

+170 -126
+5
test/interop/cryptolib/.gitignore
··· 1 + scripts/generate 2 + scripts/sa_save_file.bin 3 + scripts/log.txt 4 + sa_save_file.bin 5 + log.txt
+2 -2
test/interop/cryptolib/scripts/generate.sh
··· 33 33 # Run the generator 34 34 ./generate "$TRACE_DIR" 35 35 36 - # Clean up binary 37 - rm -f generate 36 + # Clean up binary and CryptoLib artifacts 37 + rm -f generate sa_save_file.bin log.txt
test/interop/cryptolib/scripts/log.txt

This is a binary file and will not be displayed.

+163 -124
test/interop/cryptolib/test.ml
··· 1 1 (** NASA CryptoLib interop tests for SDLS. 2 2 3 - Traces generated by: NASA CryptoLib (C) via Crypto_TC_ApplySecurity 3 + Traces generated by: NASA CryptoLib 1.5.1 (Crypto_TC_ApplySecurity) 4 4 Regenerate: dune build @regen-traces 5 5 6 - Tests verify that our security header/trailer Wire codec correctly parses 7 - frames produced by CryptoLib's TC_ApplySecurity, and that our 8 - protect_frame/unprotect_frame produces identical secured frames for the 9 - clear-mode (no encryption) case. *) 6 + Tests verify that our {!Sdls.protect_frame} produces byte-identical 7 + output to CryptoLib's Crypto_TC_ApplySecurity for clear-mode and 8 + AES-256-GCM authenticated-encryption TC frames. 9 + 10 + SA configuration (from Crypto_Init_TC_Unit_Test, then modified): 11 + - SA[1]: SPI=1, CLEAR mode (est=0, ast=0), shivf_len=12, SCID=3, VCID=0 12 + - SA[2]: SPI=2, AES-256-GCM encrypt-only -- skipped, our library does 13 + not support GCM without authentication (GCM is AEAD) 14 + - SA[4]: SPI=4, AES-256-GCM auth-enc (est=1, ast=1), shivf_len=12, 15 + stmacf_len=16, EKID=4, ABM=0xFF..., SCID=3, VCID=0 *) 16 + 17 + open Sdls 10 18 11 19 let trace path = Filename.concat "traces" path 12 20 ··· 16 24 name : string; 17 25 mode : string; 18 26 spi : int; 27 + ekid : int; 28 + ecs : int; 19 29 iv_len : int; 20 30 mac_len : int; 31 + has_seg_hdr : int; 32 + key_hex : string; 21 33 input_hex : string; 22 34 secured_hex : string; 23 35 } ··· 25 37 let vector_codec = 26 38 Csvt.( 27 39 Row.( 28 - obj 29 - (fun 30 - name 31 - mode 32 - spi 33 - _ekid 34 - _ecs 35 - iv_len 36 - mac_len 37 - _has_seg 38 - _key_hex 39 - input_hex 40 - secured_hex 41 - -> { name; mode; spi; iv_len; mac_len; input_hex; secured_hex }) 40 + obj (fun name mode spi ekid ecs iv_len mac_len has_seg_hdr key_hex 41 + input_hex secured_hex -> 42 + { 43 + name; 44 + mode; 45 + spi; 46 + ekid; 47 + ecs; 48 + iv_len; 49 + mac_len; 50 + has_seg_hdr; 51 + key_hex; 52 + input_hex; 53 + secured_hex; 54 + }) 42 55 |> col "name" string ~enc:(fun r -> r.name) 43 56 |> col "mode" string ~enc:(fun r -> r.mode) 44 57 |> col "spi" int ~enc:(fun r -> r.spi) 45 - |> col "ekid" int ~enc:(fun _ -> 0) 46 - |> col "ecs" int ~enc:(fun _ -> 0) 58 + |> col "ekid" int ~enc:(fun r -> r.ekid) 59 + |> col "ecs" int ~enc:(fun r -> r.ecs) 47 60 |> col "iv_len" int ~enc:(fun r -> r.iv_len) 48 61 |> col "mac_len" int ~enc:(fun r -> r.mac_len) 49 - |> col "has_seg_hdr" int ~enc:(fun _ -> 0) 50 - |> col "key_hex" string ~enc:(fun _ -> "") 62 + |> col "has_seg_hdr" int ~enc:(fun r -> r.has_seg_hdr) 63 + |> col "key_hex" string ~enc:(fun r -> r.key_hex) 51 64 |> col "input_hex" string ~enc:(fun r -> r.input_hex) 52 65 |> col "secured_hex" string ~enc:(fun r -> r.secured_hex) 53 66 |> finish)) 54 67 55 - (* {1 Helpers} *) 68 + let load_vectors () = 69 + match Csvt.decode_file vector_codec (trace "vectors.csv") with 70 + | Ok rows -> rows 71 + | Error e -> Alcotest.failf "CSV parse: %a" Csvt.pp_error e 56 72 57 - let hex_to_bytes hex = 58 - let len = String.length hex / 2 in 59 - let buf = Bytes.create len in 60 - for i = 0 to len - 1 do 61 - let digit c = 62 - if c >= '0' && c <= '9' then Char.code c - Char.code '0' 63 - else if c >= 'a' && c <= 'f' then Char.code c - Char.code 'a' + 10 64 - else Char.code c - Char.code 'A' + 10 65 - in 66 - let hi = digit hex.[i * 2] in 67 - let lo = digit hex.[(i * 2) + 1] in 68 - Bytes.set_uint8 buf i ((hi lsl 4) lor lo) 69 - done; 70 - buf 73 + (* {1 SA / key setup} *) 71 74 72 - (* {1 Security Header Parsing Tests} 75 + (** Build an SA entry matching CryptoLib's unit-test config for a vector. 73 76 74 - Verify that our Wire codec for the security header correctly extracts 75 - SPI and IV from CryptoLib-secured frames. The security header starts 76 - right after the TC primary header (5 bytes). *) 77 + CryptoLib's default SA configuration after Crypto_Init_TC_Unit_Test: 78 + - SA[1]: est=0, ast=0, shivf_len=12, iv_len=12, SCID=3, VCID=0 79 + - SA[4]: est=1, ast=1, shivf_len=12, iv_len=12, stmacf_len=16, 80 + ecs=AES-256-GCM, ekid=4, ABM=0xFF... 77 81 78 - let test_security_header_parse () = 79 - let rows = 80 - match Csvt.decode_file vector_codec (trace "vectors.csv") with 81 - | Ok rows -> rows 82 - | Error e -> Alcotest.failf "CSV: %a" Csvt.pp_error e 82 + The ABM in the generator is set to all 0xFF, matching our Sa.All. *) 83 + let sa_of_vector (v : vector) = 84 + let encryption = v.ecs > 0 && v.mac_len > 0 in 85 + let authentication = v.mac_len > 0 in 86 + let ecs = 87 + if v.ecs = 1 then Some Sa.AES_256_GCM 88 + else if v.ecs = 2 then Some Sa.AES_256_CBC 89 + else None 83 90 in 84 - List.iter 85 - (fun (v : vector) -> 86 - let secured = hex_to_bytes v.secured_hex in 87 - let secured_len = Bytes.length secured in 88 - (* TC primary header is 5 bytes + 1 byte segment header; 89 - security header follows at offset 6 *) 90 - let sec_hdr_off = 6 in 91 - if secured_len < sec_hdr_off + 2 + v.iv_len then 92 - Alcotest.failf "%s: secured frame too short" v.name 93 - else begin 94 - (* Parse SPI (2 bytes big-endian after TC header + segment header) *) 95 - let spi = Bytes.get_uint16_be secured sec_hdr_off in 96 - Alcotest.(check int) (v.name ^ " SPI") v.spi spi; 97 - (* Parse IV (iv_len bytes after SPI) *) 98 - let iv = Bytes.sub secured (sec_hdr_off + 2) v.iv_len in 99 - (* IV should be well-formed (not all garbage) *) 100 - let iv_nonzero = 101 - let found = ref false in 102 - Bytes.iter (fun c -> if c <> '\000' then found := true) iv; 103 - !found 104 - in 105 - (* First vector for each SPI starts with IV=0...0 or 0...1 *) 106 - ignore iv_nonzero; 107 - (* Verify the secured frame starts with the same TC header *) 108 - let input = hex_to_bytes v.input_hex in 109 - (* First 5 bytes should share version/bypass/ctrl/scid/vcid *) 110 - (* But frame_len differs (secured is longer) *) 111 - let input_word0 = Bytes.get_uint16_be input 0 land 0xFFFC in 112 - let secured_word0 = Bytes.get_uint16_be secured 0 land 0xFFFC in 113 - Alcotest.(check int) 114 - (v.name ^ " header word0 (sans frame_len)") 115 - input_word0 secured_word0 116 - end) 117 - rows 91 + Sa.v ~encryption ~authentication ~ecs ~iv_len:v.iv_len ~mac_len:v.mac_len 92 + ~sn_len:0 ~spi:v.spi ~scid:3 ~vcid:0 ~ek_id:(Keyid.of_int_exn v.ekid) () 93 + 94 + (** Build a keystore with the vector's key material. *) 95 + let keystore_of_vector (v : vector) = 96 + let ks = Keystore.in_memory () in 97 + if v.key_hex <> "" then begin 98 + let key_bytes = Hex.decode_exn v.key_hex in 99 + Keystore.add ks (Keyid.of_int_exn v.ekid) key_bytes; 100 + ignore (Keystore.activate ks (Keyid.of_int_exn v.ekid)) 101 + end; 102 + ks 103 + 104 + (* {1 protect_frame comparison} 105 + 106 + For each vector we call our protect_frame and compare byte-for-byte 107 + against CryptoLib's Crypto_TC_ApplySecurity output. 108 + 109 + Frame layout produced by CryptoLib: 110 + TC_Header(5) | Seg_Hdr(1) | Sec_Hdr(SPI+IV) | [encrypted] payload 111 + | Sec_Trailer(MAC) | FECF(2) 112 + 113 + Our protect_frame writes: 114 + frame_hdr_bytes | Sec_Hdr(SPI+IV) | [encrypted] payload | Sec_Trailer(MAC) 118 115 119 - (* {1 MAC Length Tests} 116 + We pass TC_Header(5)+Seg_Hdr(1) from the SECURED frame as frame_hdr_bytes 117 + (because CryptoLib builds AAD from the output frame, and the TC header's 118 + frame-length field has been updated). The plaintext is the original payload 119 + from the input frame (between seg_hdr and FECF). 120 120 121 - For authenticated encryption (mode=auth_enc), verify that the MAC 122 - is present at the expected position in the secured frame. *) 121 + We compare our output against CryptoLib's output minus the trailing FECF. *) 122 + let test_protect (v : vector) () = 123 + let input = Hex.decode_exn v.input_hex in 124 + let expected_full = Hex.decode_exn v.secured_hex in 123 125 124 - let test_mac_present () = 125 - let rows = 126 - match Csvt.decode_file vector_codec (trace "vectors.csv") with 127 - | Ok rows -> rows 128 - | Error e -> Alcotest.failf "CSV: %a" Csvt.pp_error e 126 + (* CryptoLib's secured frame sans FECF *) 127 + let expected = 128 + Bytes.sub expected_full 0 (Bytes.length expected_full - 2) 129 129 in 130 - List.iter 131 - (fun (v : vector) -> 132 - if v.mac_len > 0 then begin 133 - let secured = hex_to_bytes v.secured_hex in 134 - let secured_len = Bytes.length secured in 135 - (* MAC is at the end, before optional FECF (2 bytes) *) 136 - (* CryptoLib appends FECF after MAC *) 137 - let fecf_len = 2 in 138 - let mac_start = secured_len - fecf_len - v.mac_len in 139 - if mac_start < 0 then 140 - Alcotest.failf "%s: frame too short for MAC" v.name 141 - else begin 142 - let mac = Bytes.sub secured mac_start v.mac_len in 143 - (* MAC should not be all zeros (would indicate crypto failure) *) 144 - let all_zero = 145 - let found = ref true in 146 - Bytes.iter (fun c -> if c <> '\000' then found := false) mac; 147 - !found 148 - in 149 - if all_zero then Alcotest.failf "%s: MAC is all zeros" v.name; 150 - Alcotest.(check int) 151 - (v.name ^ " mac_len") v.mac_len (Bytes.length mac) 152 - end 153 - end) 154 - rows 130 + 131 + (* frame_hdr_bytes = TC_header(5) + seg_hdr(1) from the SECURED frame. 132 + We use the secured frame's TC header because its FL field reflects the 133 + secured frame length, and CryptoLib computes AAD from the output frame. *) 134 + let frame_hdr_bytes = Bytes.sub expected_full 0 6 in 135 + 136 + (* plaintext = original payload: input frame bytes between 137 + TC_header(5) + seg_hdr(1) and FECF(2). *) 138 + let plaintext = Bytes.sub input 6 (Bytes.length input - 6 - 2) in 139 + 140 + (* Extract the IV that CryptoLib used from the secured frame's security 141 + header: SPI(2) at offset 6, then IV(iv_len) at offset 8. 142 + CryptoLib increments IV after each call, so later vectors in the same 143 + SPI group use IV > 0. We must seed our SA with the matching IV. *) 144 + let iv_from_trace = Bytes.sub expected_full 8 v.iv_len in 145 + 146 + let sa = sa_of_vector v in 147 + let sa = { sa with dyn = { sa.dyn with iv = iv_from_trace } } in 148 + let keys = keystore_of_vector v in 149 + let w = Binary.Writer.create 1024 in 150 + match 151 + protect_frame ~sa ~keys ~get_ek:Keystore.get_encryption_key 152 + ~get_ak:Keystore.get_auth_key ~frame_hdr_bytes ~plaintext w 153 + with 154 + | Error e -> 155 + Alcotest.failf "%s: protect_frame failed: %a" v.name pp_error_debug e 156 + | Ok _sa' -> 157 + let got = Binary.Writer.contents w in 158 + if not (Bytes.equal got expected) then 159 + Alcotest.failf 160 + "%s: protect_frame output mismatch\n expected: %s\n got: %s" 161 + v.name (Hex.encode expected) (Hex.encode got) 162 + 163 + (* {1 Security header parse test} 164 + 165 + Verify that our Wire codec for the security header correctly extracts 166 + SPI and IV from CryptoLib-secured frames. *) 167 + let test_security_header_parse (v : vector) () = 168 + let secured = Hex.decode_exn v.secured_hex in 169 + (* Security header starts after TC_header(5) + seg_hdr(1) = offset 6 *) 170 + let sec_hdr_off = 6 in 171 + if Bytes.length secured < sec_hdr_off + 2 + v.iv_len then 172 + Alcotest.failf "%s: secured frame too short" v.name; 173 + let spi = Bytes.get_uint16_be secured sec_hdr_off in 174 + Alcotest.(check int) "SPI" v.spi spi; 175 + (* Verify the first two bytes (version/bypass/scid) match between 176 + input and secured frame -- only the frame-length field changes *) 177 + let input = Hex.decode_exn v.input_hex in 178 + let input_w0 = Bytes.get_uint16_be input 0 land 0xFFFC in 179 + let secured_w0 = Bytes.get_uint16_be secured 0 land 0xFFFC in 180 + Alcotest.(check int) "header word0" input_w0 secured_w0 181 + 182 + (* {1 Test runner} *) 155 183 156 184 let () = 185 + let vectors = load_vectors () in 186 + (* Split vectors by mode: we can compare protect_frame output for 187 + "clear" and "auth_enc" modes. "enc" (GCM encrypt-only) is not 188 + supported by our library -- GCM is inherently AEAD. *) 189 + let testable_vectors = 190 + List.filter (fun (v : vector) -> v.mode <> "enc") vectors 191 + in 192 + let protect_tests = 193 + List.map 194 + (fun (v : vector) -> 195 + Alcotest.test_case v.name `Quick (test_protect v)) 196 + testable_vectors 197 + in 198 + let parse_tests = 199 + List.map 200 + (fun (v : vector) -> 201 + Alcotest.test_case v.name `Quick (test_security_header_parse v)) 202 + vectors 203 + in 157 204 Alcotest.run "sdls-interop-cryptolib" 158 205 [ 159 - ( "security_header", 160 - [ 161 - Alcotest.test_case "parse SPI and IV from CryptoLib frames" `Quick 162 - test_security_header_parse; 163 - ] ); 164 - ( "mac", 165 - [ 166 - Alcotest.test_case "MAC present in auth_enc frames" `Quick 167 - test_mac_present; 168 - ] ); 206 + ("protect", protect_tests); 207 + ("security_header", parse_tests); 169 208 ]