chore(rosenpass): Add integration tests for basic connectivity, backwards compatability and multi-peer connectivity

This commit is contained in:
David Niehues
2025-07-07 12:19:44 +02:00
parent b5ef5842d9
commit dddadb67b8
12 changed files with 1082 additions and 0 deletions

View File

@@ -0,0 +1,29 @@
# Integration Tests
This directory contains integration tests for rosenpass in the form of a nix flake. Put simply, in order to run the integration tests for the main branch as they are on github right now, just run the following on a linux machine with nix installed and flakes enabled:
```
nix flake check
```
## Overview
The integration tests recognize two rosenpass versions, a new version and an old version. If not adapted, both are set to the version of the current main branch of rosenpass on github. We describe below how to change this.
All integration tests install rosenpass on virtual machines, run the key exchange, create a connection via wireguard that uses rosenpass and then checks whether all peers can ping each other via wireguard. Overall there are four integration tests:
- `basicConnectivity` -- This test only uses the new rosenpass version and checks whether the key exchange between two peers works such that they can ping each other.
- `backwardClient` -- This test is the same as the `basicConnectivity` test, but with the client using the old rosenpass version.
- `backwardServer` -- This test is the same as the `backwardClient` test, but with the server using the old rosenpass version.
- `multiPeer` -- This test again only uses the new rosenpass version, but with three peers. The first peer acts as a server towards the other two peers. The second peer acts as a client towards the first peer and as a server towards the third peer. The third peer acts as a client towards all peers.
## Testing specific versions
You can specify specific versions of rosenpass to test compatability. The proper way to do so is by overriding the respective inputs to the nix flake. As an example, say you want to test the compatability of your local version of rosenpass with the branch `new-feature` on github. You can achieve this by running the following command:
```
nix flake check --override-input rosenpass-old ../../ --override-input rosenpass-new github:rosenpass/rosenpass/new-feature
```
## Usage in the CI
In the CI, the integration tests are used differently, depending on whether the CI run is triggered by a push to the main branch or by a pull request. If the CI run is triggered by a pull request, then the result of merging the main branch and the PR branch is set as the new version and the current state of the main branch is set as the old version. For push events, the CI is only triggered if the push is onto the main branch. In that case, the state before the push event is considered the old version and the state after the push event is considered as the new version.

320
tests/integration/flake.lock generated Normal file
View File

@@ -0,0 +1,320 @@
{
"nodes": {
"flake-parts": {
"inputs": {
"nixpkgs-lib": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1751413152,
"narHash": "sha256-Tyw1RjYEsp5scoigs1384gIg6e0GoBVjms4aXFfRssQ=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "77826244401ea9de6e3bac47c2db46005e1f30b5",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1726560853,
"narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_2": {
"inputs": {
"systems": "systems_2"
},
"locked": {
"lastModified": 1726560853,
"narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nix-vm-test": {
"inputs": {
"nixpkgs": [
"rosenpass-new",
"nixpkgs"
]
},
"locked": {
"lastModified": 1734355073,
"narHash": "sha256-FfdPOGy1zElTwKzjgIMp5K2D3gfPn6VWjVa4MJ9L1Tc=",
"owner": "numtide",
"repo": "nix-vm-test",
"rev": "5948de39a616f2261dbbf4b6f25cbe1cbefd788c",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "nix-vm-test",
"type": "github"
}
},
"nix-vm-test_2": {
"inputs": {
"nixpkgs": [
"rosenpass-old",
"nixpkgs"
]
},
"locked": {
"lastModified": 1734355073,
"narHash": "sha256-FfdPOGy1zElTwKzjgIMp5K2D3gfPn6VWjVa4MJ9L1Tc=",
"owner": "numtide",
"repo": "nix-vm-test",
"rev": "5948de39a616f2261dbbf4b6f25cbe1cbefd788c",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "nix-vm-test",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1751792365,
"narHash": "sha256-J1kI6oAj25IG4EdVlg2hQz8NZTBNYvIS0l4wpr9KcUo=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "1fd8bada0b6117e6c7eb54aad5813023eed37ccb",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1728193676,
"narHash": "sha256-PbDWAIjKJdlVg+qQRhzdSor04bAPApDqIv2DofTyynk=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "ecbc1ca8ffd6aea8372ad16be9ebbb39889e55b6",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-24.05",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_3": {
"locked": {
"lastModified": 1728193676,
"narHash": "sha256-PbDWAIjKJdlVg+qQRhzdSor04bAPApDqIv2DofTyynk=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "ecbc1ca8ffd6aea8372ad16be9ebbb39889e55b6",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-24.05",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-parts": "flake-parts",
"nixpkgs": "nixpkgs",
"rosenpass-new": "rosenpass-new",
"rosenpass-old": "rosenpass-old"
}
},
"rosenpass-new": {
"inputs": {
"flake-utils": "flake-utils",
"nix-vm-test": "nix-vm-test",
"nixpkgs": "nixpkgs_2",
"rust-overlay": "rust-overlay",
"treefmt-nix": "treefmt-nix"
},
"locked": {
"lastModified": 1752081615,
"narHash": "sha256-g9jC1HNCMSMPzArA8RCPGaxCCFH6dzQuq20RDsRwRT8=",
"owner": "rosenpass",
"repo": "rosenpass",
"rev": "3e03e479350551d11b81bde1bb55f5fdf8246f7c",
"type": "github"
},
"original": {
"owner": "rosenpass",
"ref": "main",
"repo": "rosenpass",
"type": "github"
}
},
"rosenpass-old": {
"inputs": {
"flake-utils": "flake-utils_2",
"nix-vm-test": "nix-vm-test_2",
"nixpkgs": "nixpkgs_3",
"rust-overlay": "rust-overlay_2",
"treefmt-nix": "treefmt-nix_2"
},
"locked": {
"lastModified": 1752081615,
"narHash": "sha256-g9jC1HNCMSMPzArA8RCPGaxCCFH6dzQuq20RDsRwRT8=",
"owner": "rosenpass",
"repo": "rosenpass",
"rev": "3e03e479350551d11b81bde1bb55f5fdf8246f7c",
"type": "github"
},
"original": {
"owner": "rosenpass",
"ref": "main",
"repo": "rosenpass",
"type": "github"
}
},
"rust-overlay": {
"inputs": {
"nixpkgs": [
"rosenpass-new",
"nixpkgs"
]
},
"locked": {
"lastModified": 1744513456,
"narHash": "sha256-NLVluTmK8d01Iz+WyarQhwFcXpHEwU7m5hH3YQQFJS0=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "730fd8e82799219754418483fabe1844262fd1e2",
"type": "github"
},
"original": {
"owner": "oxalica",
"repo": "rust-overlay",
"type": "github"
}
},
"rust-overlay_2": {
"inputs": {
"nixpkgs": [
"rosenpass-old",
"nixpkgs"
]
},
"locked": {
"lastModified": 1744513456,
"narHash": "sha256-NLVluTmK8d01Iz+WyarQhwFcXpHEwU7m5hH3YQQFJS0=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "730fd8e82799219754418483fabe1844262fd1e2",
"type": "github"
},
"original": {
"owner": "oxalica",
"repo": "rust-overlay",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"systems_2": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"treefmt-nix": {
"inputs": {
"nixpkgs": [
"rosenpass-new",
"nixpkgs"
]
},
"locked": {
"lastModified": 1743748085,
"narHash": "sha256-uhjnlaVTWo5iD3LXics1rp9gaKgDRQj6660+gbUU3cE=",
"owner": "numtide",
"repo": "treefmt-nix",
"rev": "815e4121d6a5d504c0f96e5be2dd7f871e4fd99d",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "treefmt-nix",
"type": "github"
}
},
"treefmt-nix_2": {
"inputs": {
"nixpkgs": [
"rosenpass-old",
"nixpkgs"
]
},
"locked": {
"lastModified": 1743748085,
"narHash": "sha256-uhjnlaVTWo5iD3LXics1rp9gaKgDRQj6660+gbUU3cE=",
"owner": "numtide",
"repo": "treefmt-nix",
"rev": "815e4121d6a5d504c0f96e5be2dd7f871e4fd99d",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "treefmt-nix",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

137
tests/integration/flake.nix Normal file
View File

@@ -0,0 +1,137 @@
{
description = "Integration tests for rosenpass";
inputs = {
flake-parts.url = "github:hercules-ci/flake-parts";
flake-parts.inputs.nixpkgs-lib.follows = "nixpkgs";
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05";
# Override or change these inputs for testing new Integrations. They are overriden automatically when run in the CI
rosenpass-old.url = "github:rosenpass/rosenpass/main";
rosenpass-new.url = "github:rosenpass/rosenpass/main";
};
outputs =
inputs:
inputs.flake-parts.lib.mkFlake { inherit inputs; } {
systems = [
"i686-linux"
"x86_64-linux"
"aarch64-linux"
"aarch64-darwin"
];
perSystem =
{ system, lib, ... }:
let
# Since other parts of the CI are already doing the unit tests, we deactivate them here.
rosenpass-old = inputs.rosenpass-old.packages.${system}.default.overrideAttrs (old: {
doCheck = false;
});
rosenpass-new = inputs.rosenpass-new.packages.${system}.default.overrideAttrs (old: {
doCheck = false;
});
basicConnectivityOverlay = final: prev: {
rosenpass-peer-a = rosenpass-new;
rosenpass-peer-b = rosenpass-new;
};
backwardServerOverlay = final: prev: {
rosenpass-peer-a = rosenpass-old;
rosenpass-peer-b = rosenpass-new;
};
backwardClientOverlay = final: prev: {
rosenpass-peer-a = rosenpass-new;
rosenpass-peer-b = rosenpass-old;
};
multiPeerOverlay = final: prev: {
rosenpass-peer-a = rosenpass-new;
rosenpass-peer-b = rosenpass-new;
rosenpass-peer-c = rosenpass-new;
};
# The current version of ipython fails to build on i686 linux.
# We therefore pin an older version that works for the time beeing.
ipythonOverlay = final: prev: {
python313 = prev.python313.override {
packageOverrides = python-final: python-prev: {
ipython = python-prev.ipython.overridePythonAttrs (old: {
version = "8.37.0";
src = python-final.fetchPypi {
pname = "ipython";
version = "8.37.0";
hash = "sha256-yoFYQeGkGh5rc6CwjzA4r5siUlZNAfxAU1bTQDMBIhY=";
};
});
};
};
};
pkgsBasicConnectivity = import inputs.nixpkgs {
inherit system;
overlays = [
basicConnectivityOverlay
ipythonOverlay
];
};
pkgsBackwardServer = import inputs.nixpkgs {
inherit system;
overlays = [
backwardServerOverlay
ipythonOverlay
];
};
pkgsBackwardClient = import inputs.nixpkgs {
inherit system;
overlays = [
backwardClientOverlay
ipythonOverlay
];
};
pkgsMultiPeer = import inputs.nixpkgs {
inherit system;
overlays = [
multiPeerOverlay
ipythonOverlay
];
};
in
{
checks.basicConnectivity = pkgsBasicConnectivity.testers.runNixOSTest (
import ./rpsc-test.nix {
pkgs = pkgsBasicConnectivity;
inherit lib;
}
);
checks.backwardServer = pkgsBackwardServer.testers.runNixOSTest (
import ./rpsc-test.nix {
pkgs = pkgsBackwardServer;
inherit lib;
}
);
checks.backwardClient = pkgsBackwardClient.testers.runNixOSTest (
import ./rpsc-test.nix {
pkgs = pkgsBackwardClient;
inherit lib;
}
);
checks.multiPeer = pkgsMultiPeer.testers.runNixOSTest (
import ./rpsc-test.nix {
pkgs = pkgsMultiPeer;
inherit lib;
multiPeer = true;
}
);
};
};
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,38 @@
{
lib,
pkgs,
config,
...
}:
let
cfg = config.services.rosenpassKeyExchange;
in
{
options.services.rosenpassKeyExchange = {
enable = lib.mkEnableOption "rosenpass key-exchange";
config = lib.mkOption {
type = lib.types.path;
description = "Path to rosenpass configuration";
};
rosenpassVersion = lib.mkOption {
type = lib.types.package;
description = "Rosenpass package to use";
};
};
config = lib.mkIf cfg.enable {
systemd.services.rp-exchange = {
description = "Rosenpass Key Exchanger";
wantedBy = [ "multi-user.target" ];
requires = [ "network-online.target" ];
script = ''
${cfg.rosenpassVersion}/bin/rosenpass exchange-config ${cfg.config}
'';
serviceConfig = {
Restart = "always";
RestartSec = 1;
};
};
};
}

View File

@@ -0,0 +1,85 @@
{
pkgs,
lib,
config,
...
}:
let
cfg = config.services.rosenpassKeySync;
servicePrefix = "rp-key-sync-";
timerPrefix = "rp-key-sync-timer-";
rpKeySyncOpts =
{ name, ... }:
{
# Each instance of ths service is defined by the following information:
options = {
enable = lib.mkEnableOption "RP Keysync for ${name}";
wgInterface = lib.mkOption {
type = lib.types.str;
description = "Wireguard interface name";
};
rpHost = lib.mkOption {
type = lib.types.str;
description = "network address of the host that runs rosenpass";
};
peerPubkey = lib.mkOption {
type = lib.types.str;
description = "Public key of wireguard peer";
};
remoteKeyPath = lib.mkOption {
type = lib.types.path;
description = "Location of the .osk file on the key exchange server";
};
};
};
in
{
options.services.rosenpassKeySync = {
instances = lib.mkOption {
type = lib.types.attrsOf (lib.types.submodule rpKeySyncOpts);
default = { };
description = "RP key sync instances";
};
};
config = {
systemd.services = lib.mapAttrs' (instanceName: instanceCfg: {
name = "${servicePrefix}${instanceName}";
value = {
description = "Rosenpass Key Downloader ${instanceName}";
wantedBy = [ "multi-user.target" ];
requires = [ "network-online.target" ];
# The script downloads the key generated by rosenpass from the key exchange node and sets it as the preshared key for the specified wireguard peer.
script = ''
set -euo pipefail
${pkgs.openssh}/bin/ssh ${instanceCfg.rpHost} "cat ${instanceCfg.remoteKeyPath}" \
| ${pkgs.wireguard-tools}/bin/wg \
set ${instanceCfg.wgInterface} \
peer ${instanceCfg.peerPubkey} \
preshared-key /dev/stdin
'';
serviceConfig = {
Restart = "always";
RestartSec = 10;
};
};
}) (lib.filterAttrs (_: cfg: cfg.enable) cfg.instances); # this creates one systemd service (as above) per configured instance.
systemd.timers = lib.mapAttrs' (instanceName: instanceCfg: {
name = "${timerPrefix}${instanceName}";
value = {
wantedBy = [ "timers.target" ];
timerConfig = {
requires = [ "network-online.target" ];
OnUnitActiveSec = "1m";
Unit = "${servicePrefix}${instanceName}.service";
};
};
}) (lib.filterAttrs (_: cfg: cfg.enable) cfg.instances); # this creates one systemd time (as above) per configured instance.
};
}

View File

@@ -0,0 +1,473 @@
{
pkgs,
lib,
multiPeer ? false,
...
}:
let
wgInterface = "mywg";
wgPort = 51820;
rpPort = 51821;
demoRosenpassKeys = ./rosenpass-keys;
rosenpassKeyFolder = "/var/secrets";
keyExchangePathAB = "/root/peer-ab.osk";
keyExchangePathBA = "/root/peer-ba.osk";
keyExchangePathAC = "/root/peer-ac.osk";
keyExchangePathCA = "/root/peer-ca.osk";
keyExchangePathBC = "/root/peer-bc.osk";
keyExchangePathCB = "/root/peer-cb.osk";
staticConfig =
{
peerA = {
innerIp = "10.100.0.1";
privateKey = "cB+EYXqf63F+8Kqn3Q1dr9ds5tQi4PkQU+WfLpZf2nU=";
publicKey = "+gsv8wlhKGKXUOYTw5r2tPpSr7CEeVBgH/kxZzeo9E8=";
rosenpassConfig = builtins.toFile "peer-a.toml" (
''
public_key = "${rosenpassKeyFolder}/self.pk"
secret_key = "${rosenpassKeyFolder}/self.sk"
listen = ["[::]:${builtins.toString rpPort}"]
verbosity = "Verbose"
[[peers]]
public_key = "${rosenpassKeyFolder}/peer-b.pk"
endpoint = "peerbkeyexchanger:${builtins.toString rpPort}"
key_out = "${keyExchangePathAB}"
''
+ (lib.optionalString multiPeer ''
[[peers]]
public_key = "${rosenpassKeyFolder}/peer-c.pk"
endpoint = "peerckeyexchanger:${builtins.toString rpPort}"
key_out = "${keyExchangePathAC}"
'')
);
};
peerB = {
innerIp = "10.100.0.2";
privateKey = "sL+9z4HAzkV01QYTQX5TA645PV8Vprk09vNNWSKjjW4=";
publicKey = "ZErZhjoSTiLCfPXl3TcnWyfvUtjP1mIQUH+2sRxI/wE=";
rosenpassConfig = builtins.toFile "peer-b.toml" (
''
public_key = "${rosenpassKeyFolder}/self.pk"
secret_key = "${rosenpassKeyFolder}/self.sk"
listen = ["[::]:${builtins.toString rpPort}"]
verbosity = "Verbose"
[[peers]]
public_key = "${rosenpassKeyFolder}/peer-a.pk"
endpoint = "peerakeyexchanger:${builtins.toString rpPort}"
key_out = "${keyExchangePathBA}"
''
+ (lib.optionalString multiPeer ''
[[peers]]
public_key = "${rosenpassKeyFolder}/peer-c.pk"
endpoint = "peerckeyexchanger:${builtins.toString rpPort}"
key_out = "${keyExchangePathBC}"
'')
);
};
}
// lib.optionalAttrs multiPeer {
# peerC is only defined if we are in a multiPeer context.
peerC = {
innerIp = "10.100.0.3";
privateKey = "gOrlrKattR+hdpGc/0X2qFXWSbw0hW7AMLzb68cWBmI=";
publicKey = "23S38TaISe+GlrNJL5DyoN+EC6g2fSYbT1Kt1LUxhRA=";
rosenpassConfig = builtins.toFile "peer-c.toml" ''
public_key = "${rosenpassKeyFolder}/self.pk"
secret_key = "${rosenpassKeyFolder}/self.sk"
listen = ["[::]:${builtins.toString rpPort}"]
verbosity = "Verbose"
[[peers]]
public_key = "${rosenpassKeyFolder}/peer-a.pk"
endpoint = "peerakeyexchanger:${builtins.toString rpPort}"
key_out = "${keyExchangePathCA}"
[[peers]]
public_key = "${rosenpassKeyFolder}/peer-b.pk"
endpoint = "peerckeyexchanger:${builtins.toString rpPort}"
key_out = "${keyExchangePathCB}"
'';
};
};
inherit (import (pkgs.path + "/nixos/tests/ssh-keys.nix") pkgs)
snakeOilPublicKey
snakeOilPrivateKey
;
# All hosts in this scenario use the same key pair
# The script takes the host as parameter and prepares passwordless login
prepareSshLogin = pkgs.writeShellScriptBin "prepare-ssh-login" ''
set -euo pipefail
mkdir -p /root/.ssh
cp ${snakeOilPrivateKey} /root/.ssh/id_ecdsa
chmod 0400 /root/.ssh/id_ecdsa
${pkgs.openssh}/bin/ssh -o StrictHostKeyChecking=no "$1" true
'';
in
{
name = "rosenpass with key exchangers";
defaults = {
imports = [
./rp-key-exchange.nix
./rp-key-sync.nix
];
systemd.tmpfiles.rules = [ "d ${rosenpassKeyFolder} 0400 root root - -" ];
};
nodes =
{
# peerA and peerB are the only neccessary peers unless we are in the multiPeer test.
peerA = {
networking.wireguard.interfaces.${wgInterface} = {
listenPort = wgPort;
ips = [ "${staticConfig.peerA.innerIp}/24" ];
inherit (staticConfig.peerA) privateKey;
peers =
[
{
inherit (staticConfig.peerB) publicKey;
allowedIPs = [ "${staticConfig.peerB.innerIp}/32" ];
presharedKey = "AR/yvSvMAzW6eS27PsRHUMWwC8cLhaD96t42cysxrb0=";
} # NOTE: We use mismatching preshared keys on purpose to make the wireguard key exchange fail until the rosenpass key exchange succeeded.
]
++ (lib.optional multiPeer {
inherit (staticConfig.peerC) publicKey;
allowedIPs = [ "${staticConfig.peerC.innerIp}/32" ];
presharedKey = "LfWvJCN8h7NhS+JWRG7GMIY20JxUV4WUs7MJ45ZGoCE=";
} # NOTE: We use mismatching preshared keys on purpose to make the wireguard key exchange fail until the rosenpass key exchange succeeded.
);
};
networking.firewall.allowedUDPPorts = [ wgPort ];
# Each instance of the key sync service loads a symmetric key from a rosenpass keyexchanger node and sets it as the preshared key for the appropriate wireguard tunnel.
services.rosenpassKeySync.instances =
{
AB = {
enable = true;
inherit wgInterface;
rpHost = "peerakeyexchanger";
peerPubkey = staticConfig.peerB.publicKey;
remoteKeyPath = keyExchangePathAB;
};
}
// lib.optionalAttrs multiPeer {
AC = {
enable = true;
inherit wgInterface;
rpHost = "peerakeyexchanger";
peerPubkey = staticConfig.peerC.publicKey;
remoteKeyPath = keyExchangePathAC;
};
};
};
peerB = {
networking.wireguard.interfaces.${wgInterface} = {
listenPort = wgPort;
ips = [ "${staticConfig.peerB.innerIp}/24" ];
inherit (staticConfig.peerB) privateKey;
peers =
[
{
inherit (staticConfig.peerA) publicKey;
allowedIPs = [ "${staticConfig.peerA.innerIp}/32" ];
endpoint = "peerA:${builtins.toString wgPort}";
presharedKey = "o25fjoIOI623cnRyhvD4YEGtuSY4BFRZmY3UHvZ0BCA=";
# NOTE: We use mismatching preshared keys on purpose to make the wireguard key exchange fail until the rosenpass key exchange succeeded.
}
]
++ (lib.optional multiPeer {
inherit (staticConfig.peerC) publicKey;
allowedIPs = [ "${staticConfig.peerC.innerIp}/32" ];
presharedKey = "GsYTUd/4Ph7wMy5r+W1no9yGe0UeZlmCPeiyu4tb6yM=";
# NOTE: We use mismatching preshared keys on purpose to make the wireguard key exchange fail until the rosenpass key exchange succeeded.
});
};
networking.firewall.allowedUDPPorts = [ wgPort ];
# Each instance of the key sync service loads a symmetric key from a rosenpass keyexchanger node and sets it as the preshared key for the appropriate wireguard tunnel.
services.rosenpassKeySync.instances =
{
BA = {
enable = true;
inherit wgInterface;
rpHost = "peerbkeyexchanger";
peerPubkey = staticConfig.peerA.publicKey;
remoteKeyPath = keyExchangePathBA;
};
}
// lib.optionalAttrs multiPeer {
BC = {
enable = true;
inherit wgInterface;
rpHost = "peerbkeyexchanger";
peerPubkey = staticConfig.peerC.publicKey;
remoteKeyPath = keyExchangePathBC;
};
};
};
# The key exchanger node for peerA is the node that actually runs rosenpass. It takes the rosenpass confguration for peerA and runs it.
# The key sync services of peerA will ssh into this node and download the exchanged keys from here.
peerakeyexchanger = {
services.openssh.enable = true;
users.users.root.openssh.authorizedKeys.keys = [ snakeOilPublicKey ];
networking.firewall.allowedUDPPorts = [ rpPort ];
services.rosenpassKeyExchange = {
enable = true;
config = staticConfig.peerA.rosenpassConfig;
rosenpassVersion = pkgs.rosenpass-peer-a;
};
};
# The key exchanger node for peerB is the node that actually runs rosenpass. It takes the rosenpass confguration for peerB and runs it.
# The key sync services of peerB will ssh into this node and download the exchanged keys from here.
peerbkeyexchanger = {
services.openssh.enable = true;
users.users.root.openssh.authorizedKeys.keys = [ snakeOilPublicKey ];
services.rosenpassKeyExchange = {
enable = true;
config = staticConfig.peerB.rosenpassConfig;
rosenpassVersion = pkgs.rosenpass-peer-b;
};
};
}
// lib.optionalAttrs multiPeer {
peerC = {
networking.wireguard.interfaces.${wgInterface} = {
listenPort = wgPort;
ips = [ "${staticConfig.peerC.innerIp}/24" ];
inherit (staticConfig.peerC) privateKey;
peers = [
{
inherit (staticConfig.peerA) publicKey;
allowedIPs = [ "${staticConfig.peerA.innerIp}/32" ];
endpoint = "peerA:${builtins.toString wgPort}";
presharedKey = "s9aIG1pY6nj2lH6p61tP8WRETNgQvoTfgel5BmVjYeI=";
} # NOTE: We use mismatching preshared keys on purpose to make the wireguard key exchange fail until the rosenpass key exchange succeeded.
{
inherit (staticConfig.peerB) publicKey;
allowedIPs = [ "${staticConfig.peerB.innerIp}/32" ];
endpoint = "peerB:${builtins.toString wgPort}";
presharedKey = "DYlFqWg/M6EfnMolBO+b4DFNrRyS6YWr4lM/2xRE1FQ=";
} # NOTE: We use mismatching preshared keys on purpose to make the wireguard key exchange fail until the rosenpass key exchange succeeded.
];
};
networking.firewall.allowedUDPPorts = [ wgPort ];
# Each instance of the key sync service loads a symmetric key from a rosenpass keyexchanger node and sets it as the preshared key for the appropriate wireguard tunnel.
services.rosenpassKeySync.instances = {
CA = {
enable = true;
inherit wgInterface;
rpHost = "peerckeyexchanger";
peerPubkey = staticConfig.peerA.publicKey;
remoteKeyPath = keyExchangePathCA;
};
CB = {
enable = true;
inherit wgInterface;
rpHost = "peerckeyexchanger";
peerPubkey = staticConfig.peerB.publicKey;
remoteKeyPath = keyExchangePathCB;
};
};
};
# The key exchanger node for peerC is the node that actually runs rosenpass. It takes the rosenpass confguration for peerC and runs it.
# The key sync services of peerC will ssh into this node and download the exchanged keys from here.
peerckeyexchanger = {
services.openssh.enable = true;
users.users.root.openssh.authorizedKeys.keys = [ snakeOilPublicKey ];
networking.firewall.allowedUDPPorts = [ rpPort ];
services.rosenpassKeyExchange = {
enable = true;
config = staticConfig.peerC.rosenpassConfig;
rosenpassVersion = pkgs.rosenpass-peer-c;
};
};
};
interactive = {
defaults = {
users.extraUsers.root.initialPassword = "";
services.openssh = {
enable = true;
settings = {
PermitRootLogin = "yes";
PermitEmptyPasswords = "yes";
};
};
security.pam.services.sshd.allowNullPassword = true;
environment.systemPackages = [
prepareSshLogin
(pkgs.writeSellScriptBin "install-rosenpass-keys" (
''
${pkgs.openssh}/bin/scp ${demoRosenpassKeys}/peer-a.sk peerakeyexchanger:${rosenpassKeyFolder}/self.sk
${pkgs.openssh}/bin/scp ${demoRosenpassKeys}/peer-a.pk peerakeyexchanger:${rosenpassKeyFolder}/self.pk
${pkgs.openssh}/bin/scp ${demoRosenpassKeys}/peer-b.pk peerakeyexchanger:${rosenpassKeyFolder}/peer-b.pk
${pkgs.openssh}/bin/scp ${demoRosenpassKeys}/peer-b.sk peerbkeyexchanger:${rosenpassKeyFolder}/self.sk
${pkgs.openssh}/bin/scp ${demoRosenpassKeys}/peer-b.pk peerbkeyexchanger:${rosenpassKeyFolder}/self.pk
${pkgs.openssh}/bin/scp ${demoRosenpassKeys}/peer-a.pk peerbkeyexchanger:${rosenpassKeyFolder}/peer-a.pk
''
+ lib.optionalString multiPeer ''
${pkgs.openssh}/bin/scp ${demoRosenpassKeys}/peer-c.sk peerckeyexchanger:${rosenpassKeyFolder}/self.sk
${pkgs.openssh}/bin/scp ${demoRosenpassKeys}/peer-c.pk peerckeyexchanger:${rosenpassKeyFolder}/self.pk
${pkgs.openssh}/bin/scp ${demoRosenpassKeys}/peer-a.pk peerckeyexchanger:${rosenpassKeyFolder}/peer-a.pk
${pkgs.openssh}/bin/scp ${demoRosenpassKeys}/peer-b.pk peerckeyexchanger:${rosenpassKeyFolder}/peer-b.pk
${pkgs.openssh}/bin/scp ${demoRosenpassKeys}/peer-c.pk peerakeyexchanger:${rosenpassKeyFolder}/peer-c.pk
${pkgs.openssh}/bin/scp ${demoRosenpassKeys}/peer-c.pk peerbkeyexchanger:${rosenpassKeyFolder}/peer-c.pk
''
))
(pkgs.writeShellScriptBin "watch-wg" ''
${pkgs.procps}/bin/watch -n1 \
${pkgs.wireguard-tools}/bin/wg show all preshared-keys
'')
];
};
nodes.peerA = {
virtualisation.forwardPorts = [
{
from = "host";
host.port = 2222;
guest.port = 22;
}
];
};
nodes.peerB = {
virtualisation.forwardPorts = [
{
from = "host";
host.port = 2223;
guest.port = 22;
}
];
};
nodes.peerC = {
virtualisation.forwardPorts = [
{
from = "host";
host.port = 2224;
guest.port = 22;
}
];
};
};
testScript = (''
start_all()
for m in [peerA, peerB, peerakeyexchanger, peerbkeyexchanger]:
m.wait_for_unit("network-online.target")
${lib.optionalString multiPeer ''
for m in [peerC, peerckeyexchanger]:
m.wait_for_unit("network-online.target")
''}
# The wireguard connection can't work because the sync services fail on
# non-recognized SSH host keys, we didn't deploy the secrets and because the preshared keyes don't match.
peerB.fail("ping -c 1 ${staticConfig.peerA.innerIp}")
peerA.fail("ping -c 1 ${staticConfig.peerB.innerIp}")
${lib.optionalString multiPeer ''
peerA.fail("ping -c 1 ${staticConfig.peerC.innerIp}")
peerB.fail("ping -c 1 ${staticConfig.peerC.innerIp}")
peerC.fail("ping -c 1 ${staticConfig.peerA.innerIp}")
peerC.fail("ping -c 1 ${staticConfig.peerB.innerIp}")
''}
# In admin-reality, this should be done with your favorite secret
# provisioning/deployment tool
peerakeyexchanger.succeed(
"cp ${demoRosenpassKeys}/peer-a.sk ${rosenpassKeyFolder}/self.sk"
)
peerakeyexchanger.succeed(
"cp ${demoRosenpassKeys}/peer-a.pk ${rosenpassKeyFolder}/self.pk"
)
peerakeyexchanger.succeed(
"cp ${demoRosenpassKeys}/peer-b.pk ${rosenpassKeyFolder}/peer-b.pk"
)
peerbkeyexchanger.succeed(
"cp ${demoRosenpassKeys}/peer-b.sk ${rosenpassKeyFolder}/self.sk"
)
peerbkeyexchanger.succeed(
"cp ${demoRosenpassKeys}/peer-b.pk ${rosenpassKeyFolder}/self.pk"
)
peerbkeyexchanger.succeed(
"cp ${demoRosenpassKeys}/peer-a.pk ${rosenpassKeyFolder}/peer-a.pk"
)
${lib.optionalString multiPeer ''
peerakeyexchanger.succeed(
"cp ${demoRosenpassKeys}/peer-c.pk ${rosenpassKeyFolder}/peer-c.pk"
)
peerbkeyexchanger.succeed(
"cp ${demoRosenpassKeys}/peer-c.pk ${rosenpassKeyFolder}/peer-c.pk"
)
peerckeyexchanger.succeed(
"cp ${demoRosenpassKeys}/peer-c.sk ${rosenpassKeyFolder}/self.sk"
)
peerckeyexchanger.succeed(
"cp ${demoRosenpassKeys}/peer-c.pk ${rosenpassKeyFolder}/self.pk"
)
peerckeyexchanger.succeed(
"cp ${demoRosenpassKeys}/peer-a.pk ${rosenpassKeyFolder}/peer-a.pk"
)
peerckeyexchanger.succeed(
"cp ${demoRosenpassKeys}/peer-b.pk ${rosenpassKeyFolder}/peer-b.pk"
)
''}
# Until now, the services must have failed due to lack of keys
peerakeyexchanger.succeed("systemctl restart rp-exchange.service")
peerbkeyexchanger.succeed("systemctl restart rp-exchange.service")
${lib.optionalString multiPeer ''
peerckeyexchanger.succeed("systemctl restart rp-exchange.service")
''}
# In reality, admins would carefully manage known SSH host keys with
# their favorite secret provisioning/deployment tool
peerA.succeed("${prepareSshLogin}/bin/prepare-ssh-login peerakeyexchanger")
peerB.succeed("${prepareSshLogin}/bin/prepare-ssh-login peerbkeyexchanger")
${lib.optionalString multiPeer ''
peerC.succeed("${prepareSshLogin}/bin/prepare-ssh-login peerckeyexchanger")
''}
for m in [peerbkeyexchanger, peerakeyexchanger]:
m.wait_for_unit("rp-exchange.service")
${lib.optionalString multiPeer ''
peerckeyexchanger.wait_for_unit("rp-exchange.service")
''}
peerA.wait_for_unit("rp-key-sync-AB.service")
peerB.wait_for_unit("rp-key-sync-BA.service")
${lib.optionalString multiPeer ''
peerA.wait_for_unit("rp-key-sync-AC.service")
peerB.wait_for_unit("rp-key-sync-BC.service")
peerC.wait_for_unit("rp-key-sync-CA.service")
peerC.wait_for_unit("rp-key-sync-CB.service")
''}
# Voila!
peerA.succeed("ping -c 1 ${staticConfig.peerB.innerIp}")
peerB.succeed("ping -c 1 ${staticConfig.peerA.innerIp}")
${lib.optionalString multiPeer ''
peerA.succeed("ping -c 1 ${staticConfig.peerC.innerIp}")
peerB.succeed("ping -c 1 ${staticConfig.peerC.innerIp}")
peerC.succeed("ping -c 1 ${staticConfig.peerA.innerIp}")
peerC.succeed("ping -c 1 ${staticConfig.peerB.innerIp}")
''}
'');
}