lol

rosenpass: refactor, add module and test (#254813)

authored by

Lorenz Leutgeb and committed by
GitHub
cc6c2d32 924c6826

+507 -52
+2
nixos/doc/manual/release-notes/rl-2311.section.md
··· 121 121 122 122 - [Soft Serve](https://github.com/charmbracelet/soft-serve), a tasty, self-hostable Git server for the command line. Available as [services.soft-serve](#opt-services.soft-serve.enable). 123 123 124 + - [Rosenpass](https://rosenpass.eu/), a service for post-quantum-secure VPNs with WireGuard. Available as [services.rosenpass](#opt-services.rosenpass.enable). 125 + 124 126 ## Backward Incompatibilities {#sec-release-23.11-incompatibilities} 125 127 126 128 - `network-online.target` has been fixed to no longer time out for systems with `networking.useDHCP = true` and `networking.useNetworkd = true`.
+1
nixos/modules/module-list.nix
··· 1047 1047 ./services/networking/redsocks.nix 1048 1048 ./services/networking/resilio.nix 1049 1049 ./services/networking/robustirc-bridge.nix 1050 + ./services/networking/rosenpass.nix 1050 1051 ./services/networking/routedns.nix 1051 1052 ./services/networking/rpcbind.nix 1052 1053 ./services/networking/rxe.nix
+233
nixos/modules/services/networking/rosenpass.nix
··· 1 + { config 2 + , lib 3 + , options 4 + , pkgs 5 + , ... 6 + }: 7 + let 8 + inherit (lib) 9 + attrValues 10 + concatLines 11 + concatMap 12 + filter 13 + filterAttrsRecursive 14 + flatten 15 + getExe 16 + mdDoc 17 + mkIf 18 + optional 19 + ; 20 + 21 + cfg = config.services.rosenpass; 22 + opt = options.services.rosenpass; 23 + settingsFormat = pkgs.formats.toml { }; 24 + in 25 + { 26 + options.services.rosenpass = 27 + let 28 + inherit (lib) 29 + literalExpression 30 + mdDoc 31 + mkOption 32 + ; 33 + inherit (lib.types) 34 + enum 35 + listOf 36 + nullOr 37 + path 38 + str 39 + submodule 40 + ; 41 + in 42 + { 43 + enable = lib.mkEnableOption (mdDoc "Rosenpass"); 44 + 45 + package = lib.mkPackageOption pkgs "rosenpass" { }; 46 + 47 + defaultDevice = mkOption { 48 + type = nullOr str; 49 + description = mdDoc "Name of the network interface to use for all peers by default."; 50 + example = "wg0"; 51 + }; 52 + 53 + settings = mkOption { 54 + type = submodule { 55 + freeformType = settingsFormat.type; 56 + 57 + options = { 58 + public_key = mkOption { 59 + type = path; 60 + description = mdDoc "Path to a file containing the public key of the local Rosenpass peer. Generate this by running {command}`rosenpass gen-keys`."; 61 + }; 62 + 63 + secret_key = mkOption { 64 + type = path; 65 + description = mdDoc "Path to a file containing the secret key of the local Rosenpass peer. Generate this by running {command}`rosenpass gen-keys`."; 66 + }; 67 + 68 + listen = mkOption { 69 + type = listOf str; 70 + description = mdDoc "List of local endpoints to listen for connections."; 71 + default = [ ]; 72 + example = literalExpression "[ \"0.0.0.0:10000\" ]"; 73 + }; 74 + 75 + verbosity = mkOption { 76 + type = enum [ "Verbose" "Quiet" ]; 77 + default = "Quiet"; 78 + description = mdDoc "Verbosity of output produced by the service."; 79 + }; 80 + 81 + peers = 82 + let 83 + peer = submodule { 84 + freeformType = settingsFormat.type; 85 + 86 + options = { 87 + public_key = mkOption { 88 + type = path; 89 + description = mdDoc "Path to a file containing the public key of the remote Rosenpass peer."; 90 + }; 91 + 92 + endpoint = mkOption { 93 + type = nullOr str; 94 + default = null; 95 + description = mdDoc "Endpoint of the remote Rosenpass peer."; 96 + }; 97 + 98 + device = mkOption { 99 + type = str; 100 + default = cfg.defaultDevice; 101 + defaultText = literalExpression "config.${opt.defaultDevice}"; 102 + description = mdDoc "Name of the local WireGuard interface to use for this peer."; 103 + }; 104 + 105 + peer = mkOption { 106 + type = str; 107 + description = mdDoc "WireGuard public key corresponding to the remote Rosenpass peer."; 108 + }; 109 + }; 110 + }; 111 + in 112 + mkOption { 113 + type = listOf peer; 114 + description = mdDoc "List of peers to exchange keys with."; 115 + default = [ ]; 116 + }; 117 + }; 118 + }; 119 + default = { }; 120 + description = mdDoc "Configuration for Rosenpass, see <https://rosenpass.eu/> for further information."; 121 + }; 122 + }; 123 + 124 + config = mkIf cfg.enable { 125 + warnings = 126 + let 127 + # NOTE: In the descriptions below, we tried to refer to e.g. 128 + # options.systemd.network.netdevs."<name>".wireguardPeers.*.PublicKey 129 + # directly, but don't know how to traverse "<name>" and * in this path. 130 + extractions = [ 131 + { 132 + relevant = config.systemd.network.enable; 133 + root = config.systemd.network.netdevs; 134 + peer = (x: x.wireguardPeers); 135 + key = (x: if x.wireguardPeerConfig ? PublicKey then x.wireguardPeerConfig.PublicKey else null); 136 + description = mdDoc "${options.systemd.network.netdevs}.\"<name>\".wireguardPeers.*.wireguardPeerConfig.PublicKey"; 137 + } 138 + { 139 + relevant = config.networking.wireguard.enable; 140 + root = config.networking.wireguard.interfaces; 141 + peer = (x: x.peers); 142 + key = (x: x.publicKey); 143 + description = mdDoc "${options.networking.wireguard.interfaces}.\"<name>\".peers.*.publicKey"; 144 + } 145 + rec { 146 + relevant = root != { }; 147 + root = config.networking.wg-quick.interfaces; 148 + peer = (x: x.peers); 149 + key = (x: x.publicKey); 150 + description = mdDoc "${options.networking.wg-quick.interfaces}.\"<name>\".peers.*.publicKey"; 151 + } 152 + ]; 153 + relevantExtractions = filter (x: x.relevant) extractions; 154 + extract = { root, peer, key, ... }: 155 + filter (x: x != null) (flatten (concatMap (x: (map key (peer x))) (attrValues root))); 156 + configuredKeys = flatten (map extract relevantExtractions); 157 + itemize = xs: concatLines (map (x: " - ${x}") xs); 158 + descriptions = map (x: "`${x.description}`"); 159 + missingKeys = filter (key: !builtins.elem key configuredKeys) (map (x: x.peer) cfg.settings.peers); 160 + unusual = '' 161 + While this may work as expected, e.g. you want to manually configure WireGuard, 162 + such a scenario is unusual. Please double-check your configuration. 163 + ''; 164 + in 165 + (optional (relevantExtractions != [ ] && missingKeys != [ ]) '' 166 + You have configured Rosenpass peers with the WireGuard public keys: 167 + ${itemize missingKeys} 168 + But there is no corresponding active Wireguard peer configuration in any of: 169 + ${itemize (descriptions relevantExtractions)} 170 + ${unusual} 171 + '') 172 + ++ 173 + optional (relevantExtractions == [ ]) '' 174 + You have configured Rosenpass, but you have not configured Wireguard via any of: 175 + ${itemize (descriptions extractions)} 176 + ${unusual} 177 + ''; 178 + 179 + environment.systemPackages = [ cfg.package pkgs.wireguard-tools ]; 180 + 181 + systemd.services.rosenpass = 182 + let 183 + filterNonNull = filterAttrsRecursive (_: v: v != null); 184 + config = settingsFormat.generate "config.toml" ( 185 + filterNonNull (cfg.settings 186 + // 187 + ( 188 + let 189 + credentialPath = id: "$CREDENTIALS_DIRECTORY/${id}"; 190 + # NOTE: We would like to remove all `null` values inside `cfg.settings` 191 + # recursively, since `settingsFormat.generate` cannot handle `null`. 192 + # This would require to traverse both attribute sets and lists recursively. 193 + # `filterAttrsRecursive` only recurses into attribute sets, but not 194 + # into values that might contain other attribute sets (such as lists, 195 + # e.g. `cfg.settings.peers`). Here, we just specialize on `cfg.settings.peers`, 196 + # and this may break unexpectedly whenever a `null` value is contained 197 + # in a list in `cfg.settings`, other than `cfg.settings.peers`. 198 + peersWithoutNulls = map filterNonNull cfg.settings.peers; 199 + in 200 + { 201 + secret_key = credentialPath "pqsk"; 202 + public_key = credentialPath "pqpk"; 203 + peers = peersWithoutNulls; 204 + } 205 + ) 206 + ) 207 + ); 208 + in 209 + rec { 210 + wantedBy = [ "multi-user.target" ]; 211 + after = [ "network-online.target" ]; 212 + path = [ cfg.package pkgs.wireguard-tools ]; 213 + 214 + serviceConfig = { 215 + User = "rosenpass"; 216 + Group = "rosenpass"; 217 + RuntimeDirectory = "rosenpass"; 218 + DynamicUser = true; 219 + AmbientCapabilities = [ "CAP_NET_ADMIN" ]; 220 + LoadCredential = [ 221 + "pqsk:${cfg.settings.secret_key}" 222 + "pqpk:${cfg.settings.public_key}" 223 + ]; 224 + }; 225 + 226 + # See <https://www.freedesktop.org/software/systemd/man/systemd.unit.html#Specifiers> 227 + environment.CONFIG = "%t/${serviceConfig.RuntimeDirectory}/config.toml"; 228 + 229 + preStart = "${getExe pkgs.envsubst} -i ${config} -o \"$CONFIG\""; 230 + script = "rosenpass exchange-config \"$CONFIG\""; 231 + }; 232 + }; 233 + }
+1
nixos/tests/all-tests.nix
··· 703 703 rkvm = handleTest ./rkvm {}; 704 704 robustirc-bridge = handleTest ./robustirc-bridge.nix {}; 705 705 roundcube = handleTest ./roundcube.nix {}; 706 + rosenpass = handleTest ./rosenpass.nix {}; 706 707 rshim = handleTest ./rshim.nix {}; 707 708 rspamd = handleTest ./rspamd.nix {}; 708 709 rss2email = handleTest ./rss2email.nix {};
+217
nixos/tests/rosenpass.nix
··· 1 + import ./make-test-python.nix ({ pkgs, ... }: 2 + let 3 + deviceName = "rp0"; 4 + 5 + server = { 6 + ip = "fe80::1"; 7 + wg = { 8 + public = "mQufmDFeQQuU/fIaB2hHgluhjjm1ypK4hJr1cW3WqAw="; 9 + secret = "4N5Y1dldqrpsbaEiY8O0XBUGUFf8vkvtBtm8AoOX7Eo="; 10 + listen = 10000; 11 + }; 12 + }; 13 + client = { 14 + ip = "fe80::2"; 15 + wg = { 16 + public = "Mb3GOlT7oS+F3JntVKiaD7SpHxLxNdtEmWz/9FMnRFU="; 17 + secret = "uC5dfGMv7Oxf5UDfdPkj6rZiRZT2dRWp5x8IQxrNcUE="; 18 + }; 19 + }; 20 + in 21 + { 22 + name = "rosenpass"; 23 + 24 + nodes = 25 + let 26 + shared = peer: { config, modulesPath, ... }: { 27 + imports = [ "${modulesPath}/services/networking/rosenpass.nix" ]; 28 + 29 + boot.kernelModules = [ "wireguard" ]; 30 + 31 + services.rosenpass = { 32 + enable = true; 33 + defaultDevice = deviceName; 34 + settings = { 35 + verbosity = "Verbose"; 36 + public_key = "/etc/rosenpass/pqpk"; 37 + secret_key = "/etc/rosenpass/pqsk"; 38 + }; 39 + }; 40 + 41 + networking.firewall.allowedUDPPorts = [ 9999 ]; 42 + 43 + systemd.network = { 44 + enable = true; 45 + networks."rosenpass" = { 46 + matchConfig.Name = deviceName; 47 + networkConfig.IPForward = true; 48 + address = [ "${peer.ip}/64" ]; 49 + }; 50 + 51 + netdevs."10-rp0" = { 52 + netdevConfig = { 53 + Kind = "wireguard"; 54 + Name = deviceName; 55 + }; 56 + wireguardConfig.PrivateKeyFile = "/etc/wireguard/wgsk"; 57 + }; 58 + }; 59 + 60 + environment.etc."wireguard/wgsk" = { 61 + text = peer.wg.secret; 62 + user = "systemd-network"; 63 + group = "systemd-network"; 64 + }; 65 + }; 66 + in 67 + { 68 + server = { 69 + imports = [ (shared server) ]; 70 + 71 + networking.firewall.allowedUDPPorts = [ server.wg.listen ]; 72 + 73 + systemd.network.netdevs."10-${deviceName}" = { 74 + wireguardConfig.ListenPort = server.wg.listen; 75 + wireguardPeers = [ 76 + { 77 + wireguardPeerConfig = { 78 + AllowedIPs = [ "::/0" ]; 79 + PublicKey = client.wg.public; 80 + }; 81 + } 82 + ]; 83 + }; 84 + 85 + services.rosenpass.settings = { 86 + listen = [ "0.0.0.0:9999" ]; 87 + peers = [ 88 + { 89 + public_key = "/etc/rosenpass/peers/client/pqpk"; 90 + peer = client.wg.public; 91 + } 92 + ]; 93 + }; 94 + }; 95 + client = { 96 + imports = [ (shared client) ]; 97 + 98 + systemd.network.netdevs."10-${deviceName}".wireguardPeers = [ 99 + { 100 + wireguardPeerConfig = { 101 + AllowedIPs = [ "::/0" ]; 102 + PublicKey = server.wg.public; 103 + Endpoint = "server:${builtins.toString server.wg.listen}"; 104 + }; 105 + } 106 + ]; 107 + 108 + services.rosenpass.settings.peers = [ 109 + { 110 + public_key = "/etc/rosenpass/peers/server/pqpk"; 111 + endpoint = "server:9999"; 112 + peer = server.wg.public; 113 + } 114 + ]; 115 + }; 116 + }; 117 + 118 + testScript = { ... }: '' 119 + from os import system 120 + 121 + # Full path to rosenpass in the store, to avoid fiddling with `$PATH`. 122 + rosenpass = "${pkgs.rosenpass}/bin/rosenpass" 123 + 124 + # Path in `/etc` where keys will be placed. 125 + etc = "/etc/rosenpass" 126 + 127 + start_all() 128 + 129 + for machine in [server, client]: 130 + machine.wait_for_unit("multi-user.target") 131 + 132 + # Gently stop Rosenpass to avoid crashes during key generation/distribution. 133 + for machine in [server, client]: 134 + machine.execute("systemctl stop rosenpass.service") 135 + 136 + for (name, machine, remote) in [("server", server, client), ("client", client, server)]: 137 + pk, sk = f"{name}.pqpk", f"{name}.pqsk" 138 + system(f"{rosenpass} gen-keys --force --secret-key {sk} --public-key {pk}") 139 + machine.copy_from_host(sk, f"{etc}/pqsk") 140 + machine.copy_from_host(pk, f"{etc}/pqpk") 141 + remote.copy_from_host(pk, f"{etc}/peers/{name}/pqpk") 142 + 143 + for machine in [server, client]: 144 + machine.execute("systemctl start rosenpass.service") 145 + 146 + for machine in [server, client]: 147 + machine.wait_for_unit("rosenpass.service") 148 + 149 + with subtest("ping"): 150 + client.succeed("ping -c 2 -i 0.5 ${server.ip}%${deviceName}") 151 + 152 + with subtest("preshared-keys"): 153 + # Rosenpass works by setting the WireGuard preshared key at regular intervals. 154 + # Thus, if it is not active, then no key will be set, and the output of `wg show` will contain "none". 155 + # Otherwise, if it is active, then the key will be set and "none" will not be found in the output of `wg show`. 156 + for machine in [server, client]: 157 + machine.wait_until_succeeds("wg show all preshared-keys | grep --invert-match none", timeout=5) 158 + ''; 159 + 160 + # NOTE: Below configuration is for "interactive" (=developing/debugging) only. 161 + interactive.nodes = 162 + let 163 + inherit (import ./ssh-keys.nix pkgs) snakeOilPublicKey snakeOilPrivateKey; 164 + 165 + sshAndKeyGeneration = { 166 + services.openssh.enable = true; 167 + users.users.root.openssh.authorizedKeys.keys = [ snakeOilPublicKey ]; 168 + environment.systemPackages = [ 169 + (pkgs.writeShellApplication { 170 + name = "gen-keys"; 171 + runtimeInputs = [ pkgs.rosenpass ]; 172 + text = '' 173 + HOST="$(hostname)" 174 + if [ "$HOST" == "server" ] 175 + then 176 + PEER="client" 177 + else 178 + PEER="server" 179 + fi 180 + 181 + # Generate keypair. 182 + mkdir -vp /etc/rosenpass/peers/$PEER 183 + rosenpass gen-keys --force --secret-key /etc/rosenpass/pqsk --public-key /etc/rosenpass/pqpk 184 + 185 + # Set up SSH key. 186 + mkdir -p /root/.ssh 187 + cp ${snakeOilPrivateKey} /root/.ssh/id_ecdsa 188 + chmod 0400 /root/.ssh/id_ecdsa 189 + 190 + # Copy public key to other peer. 191 + # shellcheck disable=SC2029 192 + ssh -o StrictHostKeyChecking=no $PEER "mkdir -pv /etc/rosenpass/peers/$HOST" 193 + scp /etc/rosenpass/pqpk "$PEER:/etc/rosenpass/peers/$HOST/pqpk" 194 + ''; 195 + }) 196 + ]; 197 + }; 198 + 199 + # Use kmscon <https://www.freedesktop.org/wiki/Software/kmscon/> 200 + # to provide a slightly nicer console, and while we're at it, 201 + # also use a nice font. 202 + # With kmscon, we can for example zoom in/out using [Ctrl] + [+] 203 + # and [Ctrl] + [-] 204 + niceConsoleAndAutologin.services.kmscon = { 205 + enable = true; 206 + autologinUser = "root"; 207 + fonts = [{ 208 + name = "Fira Code"; 209 + package = pkgs.fira-code; 210 + }]; 211 + }; 212 + in 213 + { 214 + server = sshAndKeyGeneration // niceConsoleAndAutologin; 215 + client = sshAndKeyGeneration // niceConsoleAndAutologin; 216 + }; 217 + })
+1
pkgs/tools/misc/envsubst/default.nix
··· 22 22 homepage = "https://github.com/a8m/envsubst"; 23 23 license = licenses.mit; 24 24 maintainers = with maintainers; [ nicknovitski ]; 25 + mainProgram = "envsubst"; 25 26 }; 26 27 }
+20 -52
pkgs/tools/networking/rosenpass/default.nix
··· 1 1 { lib 2 - , targetPlatform 3 2 , fetchFromGitHub 3 + , nixosTests 4 4 , rustPlatform 5 + , targetPlatform 6 + , installShellFiles 5 7 , cmake 6 - , makeWrapper 7 - , pkg-config 8 - , removeReferencesTo 9 - , coreutils 10 - , findutils 11 - , gawk 12 - , wireguard-tools 13 - , bash 14 8 , libsodium 9 + , pkg-config 15 10 }: 16 - 17 - let 18 - rpBinPath = lib.makeBinPath [ 19 - coreutils 20 - findutils 21 - gawk 22 - wireguard-tools 23 - ]; 24 - in 25 11 rustPlatform.buildRustPackage rec { 26 12 pname = "rosenpass"; 27 - version = "0.2.0"; 13 + version = "unstable-2023-09-28"; 14 + 28 15 src = fetchFromGitHub { 29 16 owner = pname; 30 17 repo = pname; 31 - rev = "v${version}"; 32 - sha256 = "sha256-r7/3C5DzXP+9w4rp9XwbP+/NK1axIP6s3Iiio1xRMbk="; 18 + rev = "b15f17133f8b5c3c5175b4cfd4fc10039a4e203f"; 19 + hash = "sha256-UXAkmt4VY0irLK2k4t6SW+SEodFE3CbX5cFbsPG0ZCo="; 33 20 }; 34 21 35 - cargoHash = "sha256-g2w3lZXQ3Kg3ydKdFs8P2lOPfIkfTbAF0MhxsJoX/E4="; 22 + cargoHash = "sha256-N1DQHkgKgkDQ6DbgQJlpZkZ7AMTqX3P8R/cWr14jK2I="; 36 23 37 24 nativeBuildInputs = [ 38 25 cmake # for oqs build in the oqs-sys crate 39 - makeWrapper # for the rp shellscript 40 - pkg-config # let libsodium-sys-stable find libsodium 41 - removeReferencesTo 26 + pkg-config 42 27 rustPlatform.bindgenHook # for C-bindings in the crypto libs 43 - ]; 44 - 45 - buildInputs = [ 46 - bash # for patchShebangs to find it 47 - libsodium 28 + installShellFiles 48 29 ]; 49 30 50 - # otherwise pkg-config tries to link non-existent dynamic libs during the build of liboqs 51 - PKG_CONFIG_ALL_STATIC = true; 52 - 53 - # liboqs requires quite a lot of stack memory, thus we adjust the default stack size picked for 54 - # new threads (which is used by `cargo test`) to be _big enough_ 55 - RUST_MIN_STACK = 8 * 1024 * 1024; # 8 MiB 31 + buildInputs = [ libsodium ]; 56 32 57 33 # nix defaults to building for aarch64 _without_ the armv8-a 58 34 # crypto extensions, but liboqs depends on these 59 - preBuild = lib.optionalString targetPlatform.isAarch 60 - ''NIX_CFLAGS_COMPILE="$NIX_CFLAGS_COMPILE -march=armv8-a+crypto"''; 61 - 62 - preInstall = '' 63 - install -D rp $out/bin/rp 64 - wrapProgram $out/bin/rp --prefix PATH : "${ rpBinPath }" 65 - for file in doc/*.1 66 - do 67 - install -D $file $out/share/man/man1/''${file##*/} 68 - done 35 + preBuild = lib.optionalString targetPlatform.isAarch64 '' 36 + NIX_CFLAGS_COMPILE="$NIX_CFLAGS_COMPILE -march=armv8-a+crypto" 69 37 ''; 70 38 71 - # nix propagates the *.dev outputs of buildInputs for static builds, but that is non-sense for an 72 - # executables only package 73 - postFixup = '' 74 - find -type f -exec remove-references-to -t ${bash.dev} \ 75 - -t ${libsodium.dev} {} \; 39 + postInstall = '' 40 + installManPage doc/rosenpass.1 76 41 ''; 77 42 43 + passthru.tests.rosenpass = nixosTests.rosenpass; 44 + 78 45 meta = with lib; { 79 46 description = "Build post-quantum-secure VPNs with WireGuard!"; 80 47 homepage = "https://rosenpass.eu/"; 81 48 license = with licenses; [ mit /* or */ asl20 ]; 82 49 maintainers = with maintainers; [ wucke13 ]; 83 - platforms = platforms.all; 50 + platforms = [ "aarch64-darwin" "aarch64-linux" "x86_64-darwin" "x86_64-linux" ]; 51 + mainProgram = "rosenpass"; 84 52 }; 85 53 }
+30
pkgs/tools/networking/rosenpass/tools.nix
··· 1 + { lib 2 + , stdenv 3 + , makeWrapper 4 + , installShellFiles 5 + , coreutils 6 + , findutils 7 + , gawk 8 + , rosenpass 9 + , wireguard-tools 10 + }: 11 + stdenv.mkDerivation { 12 + inherit (rosenpass) version src; 13 + pname = "rosenpass-tools"; 14 + 15 + nativeBuildInputs = [ makeWrapper installShellFiles ]; 16 + 17 + postInstall = '' 18 + install -D $src/rp $out/bin/rp 19 + installManPage $src/doc/rp.1 20 + wrapProgram $out/bin/rp \ 21 + --prefix PATH : ${lib.makeBinPath [ 22 + coreutils findutils gawk rosenpass wireguard-tools 23 + ]} 24 + ''; 25 + 26 + meta = rosenpass.meta // { 27 + description = "This package contains the Rosenpass tool `rp`, which is a script that wraps the `rosenpass` binary."; 28 + mainProgram = "rp"; 29 + }; 30 + }
+2
pkgs/top-level/all-packages.nix
··· 12827 12827 12828 12828 rosenpass = callPackage ../tools/networking/rosenpass { }; 12829 12829 12830 + rosenpass-tools = callPackage ../tools/networking/rosenpass/tools.nix { }; 12831 + 12830 12832 rot8 = callPackage ../tools/misc/rot8 { }; 12831 12833 12832 12834 rowhammer-test = callPackage ../tools/system/rowhammer-test { };