swim protocol in ocaml interoperable with membership lib and serf cli

feat(crypto): memberlist-compatible encryption format

- Use encryption version 1 (no PKCS7 padding) for outgoing messages
- Support decrypting both version 0 (with PKCS7) and version 1
- Add --encrypt flag to interop_test for encrypted testing
- Add test_interop_encrypted.sh for encrypted interop testing

Verified: OCaml SWIM ↔ Go memberlist encrypted ping/ack works.

+11 -5
bin/interop_test.ml
··· 2 2 3 3 external env_cast : 'a -> 'b = "%identity" 4 4 5 + let test_key = 6 + "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f" 7 + 5 8 let () = 9 + let use_encryption = 10 + Array.length Sys.argv > 1 && Sys.argv.(1) = "--encrypt" 11 + in 6 12 Eio_main.run @@ fun env -> 7 13 let env = env_cast env in 8 14 Eio.Switch.run @@ fun sw -> ··· 14 20 node_name = Some "ocaml-node"; 15 21 protocol_interval = 1.0; 16 22 probe_timeout = 0.5; 17 - secret_key = String.make 16 '\x00'; 23 + secret_key = test_key; 18 24 cluster_name = ""; 19 - (* Empty for memberlist compatibility - it uses Label instead *) 20 - encryption_enabled = false; 25 + encryption_enabled = use_encryption; 21 26 } 22 27 in 23 28 let env_wrap = { stdenv = env; sw } in ··· 26 31 Printf.eprintf "Error: Invalid encryption key\n"; 27 32 exit 1 28 33 | Ok cluster -> 29 - Printf.printf "OCaml SWIM node started on 127.0.0.1:%d\n%!" 30 - config.bind_port; 34 + Printf.printf 35 + "OCaml SWIM node started on 127.0.0.1:%d (encryption=%b)\n%!" 36 + config.bind_port config.encryption_enabled; 31 37 Swim.Cluster.start cluster; 32 38 33 39 let go_node =
+16 -3
lib/crypto.ml
··· 1 1 let nonce_size = 12 2 2 let tag_size = 16 3 3 let version_size = 1 4 - let encryption_version = 0 4 + let encryption_version = 1 5 5 let key_size = 16 6 6 let overhead = version_size + nonce_size + tag_size 7 7 ··· 32 32 (String.length ciphertext); 33 33 result 34 34 35 + let pkcs7_unpad data block_size = 36 + let len = String.length data in 37 + if len = 0 then data 38 + else 39 + let padding = Char.code data.[len - 1] in 40 + if padding > 0 && padding <= block_size && padding <= len then 41 + String.sub data 0 (len - padding) 42 + else data 43 + 35 44 let decrypt ~key data = 36 45 if Cstruct.length data < overhead then Error `Too_short 37 46 else 38 47 let version = Cstruct.get_uint8 data 0 in 39 - if version <> encryption_version then Error `Unsupported_version 48 + if version > 1 then Error `Unsupported_version 40 49 else 41 50 let nonce = 42 51 Cstruct.to_string (Cstruct.sub data version_size nonce_size) ··· 50 59 match 51 60 Mirage_crypto.AES.GCM.authenticate_decrypt ~key ~nonce ciphertext 52 61 with 53 - | Some plaintext -> Ok (Cstruct.of_string plaintext) 62 + | Some plaintext -> 63 + let plaintext = 64 + if version = 0 then pkcs7_unpad plaintext 16 else plaintext 65 + in 66 + Ok (Cstruct.of_string plaintext) 54 67 | None -> Error `Decryption_failed
+21
test_interop_encrypted.sh
··· 1 + #!/bin/bash 2 + set -e 3 + 4 + # Test key: 16 bytes (0x00-0x0f) in hex 5 + TEST_KEY="000102030405060708090a0b0c0d0e0f" 6 + 7 + echo "Starting Go memberlist server WITH encryption..." 8 + cd /home/gdiazlo/data/src/swim/interop 9 + ./memberlist-server -name go-node -port 7946 -key "$TEST_KEY" & 10 + GO_PID=$! 11 + sleep 2 12 + 13 + echo "Starting OCaml SWIM client WITH encryption..." 14 + cd /home/gdiazlo/data/src/swim 15 + timeout 25 ./_build/default/bin/interop_test.exe --encrypt || true 16 + 17 + echo "Killing Go server..." 18 + kill $GO_PID 2>/dev/null || true 19 + wait $GO_PID 2>/dev/null || true 20 + 21 + echo "Done"