Linear Feedback Shift Registers for OCaml
0
fork

Configure Feed

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

Add ocaml-lfsr: CCSDS OID LFSR pseudo-noise generator

32-cell LFSR for OID frame randomization per CCSDS 132.0-B-3 Annex D.
Polynomial x^32+x^22+x^2+x+1, verified against CCSDS test vector.
Used for TRANSEC traffic flow confidentiality (CNSSP 12 §14.c).

Generic configurable API (up to 62-bit width) with CCSDS OID preset.

+357
+23
dune-project
··· 1 + (lang dune 3.21) 2 + 3 + (name lfsr) 4 + 5 + (generate_opam_files true) 6 + 7 + (license MIT) 8 + (authors "Thomas Gazagnaire <thomas@gazagnaire.org>") 9 + (maintainers "Thomas Gazagnaire <thomas@gazagnaire.org>") 10 + 11 + (source (tangled gazagnaire.org/ocaml-lfsr)) 12 + 13 + (package 14 + (name lfsr) 15 + (synopsis "Linear Feedback Shift Registers for OCaml") 16 + (description 17 + "Configurable LFSRs (Fibonacci form) with up to 62-bit state. Includes \ 18 + presets for CCSDS OID frame randomization (32-cell, polynomial \ 19 + x^32 + x^22 + x^2 + x + 1, per CCSDS 132.0-B-3 Annex D). Used for \ 20 + TRANSEC traffic flow confidentiality in satellite links.") 21 + (depends 22 + (ocaml (>= 4.14)) 23 + (alcotest :with-test)))
+32
lfsr.opam
··· 1 + # This file is generated by dune, edit dune-project instead 2 + opam-version: "2.0" 3 + synopsis: "Linear Feedback Shift Registers for OCaml" 4 + description: 5 + "Configurable LFSRs (Fibonacci form) with up to 62-bit state. Includes presets for CCSDS OID frame randomization (32-cell, polynomial x^32 + x^22 + x^2 + x + 1, per CCSDS 132.0-B-3 Annex D). Used for TRANSEC traffic flow confidentiality in satellite links." 6 + maintainer: ["Thomas Gazagnaire <thomas@gazagnaire.org>"] 7 + authors: ["Thomas Gazagnaire <thomas@gazagnaire.org>"] 8 + license: "MIT" 9 + homepage: "https://tangled.org/gazagnaire.org/ocaml-lfsr" 10 + bug-reports: "https://tangled.org/gazagnaire.org/ocaml-lfsr/issues" 11 + depends: [ 12 + "dune" {>= "3.21"} 13 + "ocaml" {>= "4.14"} 14 + "alcotest" {with-test} 15 + "odoc" {with-doc} 16 + ] 17 + build: [ 18 + ["dune" "subst"] {dev} 19 + [ 20 + "dune" 21 + "build" 22 + "-p" 23 + name 24 + "-j" 25 + jobs 26 + "@install" 27 + "@runtest" {with-test} 28 + "@doc" {with-doc} 29 + ] 30 + ] 31 + dev-repo: "git+https://tangled.org/gazagnaire.org/ocaml-lfsr" 32 + x-maintenance-intent: ["(latest)"]
+3
lib/dune
··· 1 + (library 2 + (name lfsr) 3 + (public_name lfsr))
+75
lib/lfsr.ml
··· 1 + (*--------------------------------------------------------------------------- 2 + Copyright (c) 2026 Thomas Gazagnaire. All rights reserved. 3 + SPDX-License-Identifier: MIT 4 + ---------------------------------------------------------------------------*) 5 + 6 + (* Requires 64-bit OCaml for up to 62-bit LFSRs. *) 7 + let () = assert (Sys.int_size > 32) 8 + 9 + type t = { 10 + taps : int; 11 + width : int; 12 + mask : int; (* width-bit mask *) 13 + mutable state : int; 14 + } 15 + 16 + let create ~taps ~seed ~width = 17 + if width < 1 || width > 62 then 18 + invalid_arg (Printf.sprintf "Lfsr.create: width %d not in [1, 62]" width); 19 + let mask = if width = 62 then max_int else (1 lsl width) - 1 in 20 + { taps; width; mask; state = seed land mask } 21 + 22 + (* CCSDS 132.0-B-3 Annex D: 32-cell, polynomial D0+D1+D2+D22+D32. 23 + Fibonacci form, right-shifting, output from LSB. 24 + 25 + The polynomial D0+D1+D2+D22+D32 maps to tap positions in a 26 + right-shifting LFSR as follows: 27 + - D^0 (constant) → bit 0 (output) 28 + - D^1 → bit 31 (= n-1) 29 + - D^2 → bit 30 (= n-2) 30 + - D^22 → bit 10 (= n-22) 31 + - D^32 → register length (implicit) 32 + 33 + Tap mask: bits {0, 10, 30, 31} = 0xC0000401. 34 + Seed: all ones. 35 + 36 + Byte packing: MSB-first (first output bit goes to bit 7 of first byte). *) 37 + let ccsds_oid () = 38 + create ~taps:0xC0000401 ~seed:0xFFFFFFFF ~width:32 39 + 40 + (* Parity of an int (1 if odd number of set bits, 0 if even). *) 41 + let[@inline] parity x = 42 + let x = x lxor (x lsr 32) in 43 + let x = x lxor (x lsr 16) in 44 + let x = x lxor (x lsr 8) in 45 + let x = x lxor (x lsr 4) in 46 + let x = x lxor (x lsr 2) in 47 + let x = x lxor (x lsr 1) in 48 + x land 1 49 + 50 + let[@inline] step t = 51 + let output = t.state land 1 in 52 + let feedback = parity (t.state land t.taps) in 53 + t.state <- ((t.state lsr 1) lor (feedback lsl (t.width - 1))) land t.mask; 54 + output 55 + 56 + (* CCSDS uses MSB-first bit packing: first output bit goes to bit 7. *) 57 + let next_byte t = 58 + let b = ref 0 in 59 + for j = 7 downto 0 do 60 + b := !b lor (step t lsl j) 61 + done; 62 + !b 63 + 64 + let fill t buf ~off ~len = 65 + for i = 0 to len - 1 do 66 + Bytes.set buf (off + i) (Char.chr (next_byte t)) 67 + done 68 + 69 + let generate t n = 70 + let buf = Bytes.create n in 71 + fill t buf ~off:0 ~len:n; 72 + buf 73 + 74 + let state t = t.state 75 + let width t = t.width
+76
lib/lfsr.mli
··· 1 + (*--------------------------------------------------------------------------- 2 + Copyright (c) 2026 Thomas Gazagnaire. All rights reserved. 3 + SPDX-License-Identifier: MIT 4 + ---------------------------------------------------------------------------*) 5 + 6 + (** Linear Feedback Shift Registers. 7 + 8 + Configurable LFSRs in Fibonacci form with up to 62-bit state. The LFSR 9 + advances one bit per step; convenience functions extract bytes. 10 + 11 + {2 CCSDS OID preset} 12 + 13 + {!ccsds_oid} creates a 32-cell LFSR with polynomial 14 + {m x^{32} + x^{22} + x^2 + x + 1} for OID (Only Idle Data) frame 15 + randomization per CCSDS 132.0-B-3 Annex D. 16 + 17 + {b Test vector} (first 20 bytes): 18 + [FF FF FF FF 6D B6 D8 61 45 1F 11 F1 97 16 72 3C BE 7E 00 B1] 19 + 20 + @see <https://public.ccsds.org/Pubs/132x0b3.pdf> CCSDS 132.0-B-3 Annex D *) 21 + 22 + type t 23 + (** LFSR state. Mutable — each call to {!step}, {!next_byte}, or {!fill} 24 + advances the internal state. *) 25 + 26 + (** {1 Creation} *) 27 + 28 + val create : taps:int -> seed:int -> width:int -> t 29 + (** [create ~taps ~seed ~width] creates an LFSR with the given tap mask, 30 + initial seed, and register width (number of cells, 1–62). 31 + 32 + [taps] is a bitmask of the tap positions. The feedback bit is the XOR 33 + (parity) of all tapped bit positions. On each step the register shifts 34 + right by one and the feedback bit enters at position [width - 1]. 35 + 36 + @raise Invalid_argument if [width] is not in [1, 62]. *) 37 + 38 + val ccsds_oid : unit -> t 39 + (** [ccsds_oid ()] creates a 32-cell LFSR for CCSDS OID frame randomization. 40 + 41 + - Polynomial: [x^32 + x^22 + x^2 + x + 1] 42 + - Taps: bits 0, 10, 30, 31 ([0xC0000401]) 43 + - Seed: [0xFFFFFFFF] (all ones) 44 + - Width: 32 45 + - Byte packing: MSB-first 46 + 47 + The LFSR is initialized once and runs continuously — it is {b not} 48 + restarted between OID frames. *) 49 + 50 + (** {1 Stepping} *) 51 + 52 + val step : t -> int 53 + (** [step t] advances the LFSR by one bit and returns the output bit (0 or 1). 54 + The output bit is the LSB of the register before the shift. *) 55 + 56 + val next_byte : t -> int 57 + (** [next_byte t] advances the LFSR by 8 steps and returns the next byte 58 + of pseudo-noise (0–255). Bits are packed MSB-first (first output bit 59 + goes to bit 7), matching CCSDS convention. *) 60 + 61 + (** {1 Bulk generation} *) 62 + 63 + val fill : t -> bytes -> off:int -> len:int -> unit 64 + (** [fill t buf ~off ~len] fills [buf.\[off\]] to [buf.\[off+len-1\]] with 65 + pseudo-noise from the LFSR. *) 66 + 67 + val generate : t -> int -> bytes 68 + (** [generate t n] returns [n] bytes of pseudo-noise. *) 69 + 70 + (** {1 State} *) 71 + 72 + val state : t -> int 73 + (** [state t] returns the current register value. *) 74 + 75 + val width : t -> int 76 + (** [width t] returns the register width (number of cells). *)
+3
test/dune
··· 1 + (test 2 + (name test) 3 + (libraries lfsr alcotest))
+145
test/test.ml
··· 1 + (*--------------------------------------------------------------------------- 2 + Copyright (c) 2026 Thomas Gazagnaire. All rights reserved. 3 + SPDX-License-Identifier: MIT 4 + ---------------------------------------------------------------------------*) 5 + 6 + let hex_of_bytes b = 7 + let buf = Buffer.create (Bytes.length b * 3) in 8 + Bytes.iteri 9 + (fun i c -> 10 + if i > 0 then Buffer.add_char buf ' '; 11 + Buffer.add_string buf (Printf.sprintf "%02X" (Char.code c))) 12 + b; 13 + Buffer.contents buf 14 + 15 + let bytes_of_hex s = 16 + let s = String.concat "" (String.split_on_char ' ' s) in 17 + let len = String.length s / 2 in 18 + let buf = Bytes.create len in 19 + for i = 0 to len - 1 do 20 + Bytes.set buf i (Char.chr (int_of_string ("0x" ^ String.sub s (i * 2) 2))) 21 + done; 22 + buf 23 + 24 + (* {1 CCSDS OID test vectors} 25 + 26 + From CCSDS 132.0-B-3 Annex D: first 20 bytes of OID data pattern. 27 + Fibonacci form, seed = all ones. *) 28 + 29 + let ccsds_oid_vector = 30 + "FF FF FF FF 6D B6 D8 61 45 1F 11 F1 97 16 72 3C BE 7E 00 B1" 31 + 32 + let test_ccsds_oid_first_20 () = 33 + let t = Lfsr.ccsds_oid () in 34 + let got = Lfsr.generate t 20 in 35 + let expected = bytes_of_hex ccsds_oid_vector in 36 + Alcotest.(check string) 37 + "CCSDS OID first 20 bytes" (Bytes.to_string expected) (Bytes.to_string got) 38 + 39 + let test_ccsds_oid_first_10 () = 40 + let t = Lfsr.ccsds_oid () in 41 + let got = Lfsr.generate t 10 in 42 + let expected = bytes_of_hex "FF FF FF FF 6D B6 D8 61 45 1F" in 43 + Alcotest.(check string) 44 + "CCSDS OID first 10 bytes" (Bytes.to_string expected) (Bytes.to_string got) 45 + 46 + (* The LFSR should NOT restart between calls — state is continuous. *) 47 + let test_ccsds_oid_continuous () = 48 + let t = Lfsr.ccsds_oid () in 49 + let first = Lfsr.generate t 10 in 50 + let second = Lfsr.generate t 10 in 51 + let t2 = Lfsr.ccsds_oid () in 52 + let all = Lfsr.generate t2 20 in 53 + let combined = Bytes.cat first second in 54 + Alcotest.(check string) 55 + "continuous across calls" (Bytes.to_string all) (Bytes.to_string combined) 56 + 57 + (* fill should produce same output as generate *) 58 + let test_fill_matches_generate () = 59 + let t1 = Lfsr.ccsds_oid () in 60 + let t2 = Lfsr.ccsds_oid () in 61 + let gen = Lfsr.generate t1 100 in 62 + let buf = Bytes.create 120 in 63 + Lfsr.fill t2 buf ~off:10 ~len:100; 64 + Alcotest.(check string) 65 + "fill matches generate" 66 + (Bytes.to_string gen) 67 + (Bytes.sub_string buf 10 100) 68 + 69 + (* {1 Generic LFSR tests} *) 70 + 71 + (* A 4-bit LFSR with polynomial x^4+x+1 (taps 0,1) has period 15. *) 72 + let test_4bit_period () = 73 + let t = Lfsr.create ~taps:0x3 ~seed:0xF ~width:4 in 74 + let initial = Lfsr.state t in 75 + let period = ref 0 in 76 + let found = ref false in 77 + for _ = 1 to 20 do 78 + ignore (Lfsr.step t); 79 + incr period; 80 + if (not !found) && Lfsr.state t = initial then found := true 81 + done; 82 + (* Maximal-length 4-bit LFSR has period 2^4-1 = 15 *) 83 + let t2 = Lfsr.create ~taps:0x3 ~seed:0xF ~width:4 in 84 + for _ = 1 to 15 do 85 + ignore (Lfsr.step t2) 86 + done; 87 + Alcotest.(check int) "period 15 returns to seed" initial (Lfsr.state t2) 88 + 89 + (* Width validation *) 90 + let test_invalid_width () = 91 + Alcotest.check_raises "width 0" (Invalid_argument "Lfsr.create: width 0 not in [1, 62]") 92 + (fun () -> ignore (Lfsr.create ~taps:1 ~seed:1 ~width:0)); 93 + Alcotest.check_raises "width 63" (Invalid_argument "Lfsr.create: width 63 not in [1, 62]") 94 + (fun () -> ignore (Lfsr.create ~taps:1 ~seed:1 ~width:63)) 95 + 96 + (* step returns only 0 or 1 *) 97 + let test_step_output_range () = 98 + let t = Lfsr.ccsds_oid () in 99 + for _ = 1 to 1000 do 100 + let bit = Lfsr.step t in 101 + if bit <> 0 && bit <> 1 then 102 + Alcotest.fail (Printf.sprintf "step returned %d, expected 0 or 1" bit) 103 + done 104 + 105 + (* next_byte returns 0-255 *) 106 + let test_next_byte_range () = 107 + let t = Lfsr.ccsds_oid () in 108 + for _ = 1 to 1000 do 109 + let b = Lfsr.next_byte t in 110 + if b < 0 || b > 255 then 111 + Alcotest.fail (Printf.sprintf "next_byte returned %d" b) 112 + done 113 + 114 + (* 32-bit LFSR with maximal polynomial has period 2^32-1. Verify state 115 + doesn't degenerate to zero (which would be an absorbing state). *) 116 + let test_no_zero_state () = 117 + let t = Lfsr.ccsds_oid () in 118 + for i = 1 to 100_000 do 119 + ignore (Lfsr.step t); 120 + if Lfsr.state t = 0 then 121 + Alcotest.fail (Printf.sprintf "state became zero at step %d" i) 122 + done 123 + 124 + let () = 125 + Alcotest.run "lfsr" 126 + [ 127 + ( "ccsds-oid", 128 + [ 129 + Alcotest.test_case "first 10 bytes" `Quick test_ccsds_oid_first_10; 130 + Alcotest.test_case "first 20 bytes" `Quick test_ccsds_oid_first_20; 131 + Alcotest.test_case "continuous state" `Quick test_ccsds_oid_continuous; 132 + ] ); 133 + ( "generic", 134 + [ 135 + Alcotest.test_case "4-bit period" `Quick test_4bit_period; 136 + Alcotest.test_case "invalid width" `Quick test_invalid_width; 137 + Alcotest.test_case "step range" `Quick test_step_output_range; 138 + Alcotest.test_case "byte range" `Quick test_next_byte_range; 139 + Alcotest.test_case "no zero state" `Quick test_no_zero_state; 140 + ] ); 141 + ( "fill", 142 + [ 143 + Alcotest.test_case "fill matches generate" `Quick test_fill_matches_generate; 144 + ] ); 145 + ]